API reference

POST /v1/push

The only endpoint CI needs. It records a list of runs and answers with each value against the previous run.

Authentication

Send a project write token as Authorization: Bearer <token>. Tokens belong to one project, several can be active at once so you can rotate, and any of them can be revoked. A token is shown once, when it is created.

Request

POST https://api.driftmetrics.watch/v1/push
FieldTypeRequiredConstraintsDescription
runsarrayyes1–50, applied in order in one transactionThe runs, applied in order, all or nothing.

runs[]

FieldTypeRequiredConstraintsDescription
tagsobjectyes1–12 keys, slug-safe; one run per tag set per push (tags plus timestamp in time-series mode)The run's coordinates: they identify it and are what you filter, group and compare on.
timestampstringnoRFC 3339, with an offset, at most a day aheadSeries time of the run, at most a day ahead. Defaults to now; set it to backfill.
metaobjectnoat most 30 keys, any JSON valueFree-form provenance — commit, CI URL. Never identifies a run.
metricsarrayyes1–200, names unique per runEvery number this run measured, pushed together.
optionsobjectno{ merge?: boolean }, unknown keys rejectedFlags that change how this run is applied.

runs[].metrics[]

FieldTypeRequiredConstraintsDescription
namestringyesslug-safe · ≤120 charsMetric name; the project defines a metric on first sight.
valuenumberyesfiniteThe measured number.
unitstringnofree text · ≤20 charsDisplay unit — MB, KB, ms, %. Set on first sight; later pushes must match or omit it.
directionstringnolower_is_better | higher_is_betterWhich way is good. Set on first sight; later pushes must match or omit it.

runs[].options

FieldTypeRequiredConstraintsDescription
mergebooleannodefault falseVersioned mode only: update and add the pushed metrics on the run with these tags, keep the rest.

Example

curl --fail-with-body --retry 3 -X POST https://api.driftmetrics.watch/v1/push \
  -H "Authorization: Bearer $DRIFT_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
  "runs": [
    {
      "tags": {"version":"v2.5.0","platform":"linux-x64","build":"release"},
      "metrics": [
        {"name":"binary_size","value":2.14,"unit":"MB","direction":"lower_is_better"},
        {"name":"peak_ram","value":419,"unit":"KB","direction":"lower_is_better"},
        {"name":"latency_ms","value":41.6,"unit":"ms","direction":"lower_is_better"}
      ],
      "meta": {"commit":"9f2c1ab","ci":"https://github.com/acme/app/actions/runs/812"}
    }
  ]
}'

Response

200 with one result per run, in input order: the run's tags, the axis value it was compared against, and one delta per metric. comparedTo is null for the first run of a series.

{
  "runs": [
    {
      "tags": {"version":"v2.5.0","platform":"linux-x64","build":"release"},
      "comparedTo": "v2.4.0",
      "deltas": [
        {"metric":"binary_size","value":2.14,"previous":1.93,"deltaPct":10.88},
        {"metric":"peak_ram","value":419,"previous":412,"deltaPct":1.7},
        {"metric":"latency_ms","value":41.6,"previous":42.1,"deltaPct":-1.19}
      ]
    }
  ]
}

Semantics

  • The body is always { runs: [...] }, even for one run. Runs apply in order in one transaction, so a later run compares against an earlier one; if any run fails, nothing is stored and details paths start with runs.<i>. Two runs with the same identity in one push are refused. Unknown fields are refused at every level. A lone run sent as the body (tags and metrics at the top) is a 400 asking to wrap it in runs.
  • Every run carries the axis tag (optional for text), and its value must parse as the project's axis kind (version, number, timestamp or text); a missing or unparseable value is a 400 naming the expected format. An axis value that parses the same as a stored one but is spelled differently (v1.2.3 vs 1.2.3) is a 400 naming the stored spelling.
  • Each run carries all of its metrics. Pushing the same tags again replaces that run's measurements, and a metric left out is dropped from it, unless options.merge is set, which updates and adds the pushed metrics and keeps the rest. Merge is versioned mode only; a time-series project rejects it with a 400.
  • A metric's unit and direction are set the first time the project sees it. A later push must send the same values or leave them out; a different one is a 400 naming the stored value. Change them in project settings. Direction defaults to lower_is_better.
  • A tag key's value type (text or number) is pinned on first sight too. Mixing types later is rejected.
  • timestamp is optional and defaults to now. Send it to backfill history at the right point; it may be at most a day ahead of the server clock. In time-series mode, an untimed run that lands on another run's tags and time is a 400 asking for a timestamp on it.
  • Pushing a deleted run's tags again restores it. In time-series mode only a push with the same timestamp does; one left to default to now adds a new run. Pushing a deleted metric again brings back its old points, with the unit and direction that push sends.
  • A tag key the project demoted to meta is still accepted: it is moved from tags to meta before the run is stored, so the response's tags leave it out.
  • Tag keys named axis, from, to, size, cursor or merge are refused: those names are the page URL's own.

Schema

A project can store one schema declaring its tags, metrics and run types. While it is enforced, each push names its run type in the run_kind tag and is checked before anything is stored.

{
  "version": 2,
  "options": {"merge_on_push":true},
  "tags": {"version":{},"module":{},"device":{"values":["cam-v2"]}},
  "metrics": {"inference_speed":{"unit":"ms","direction":"lower_is_better"},"fps":{"direction":"higher_is_better"}},
  "runs": {"bench":{"tags":["version","module"],"metrics":["inference_speed"]},"camera":{"tags":["version","device"],"metrics":["fps","inference_speed"]}}
}
FieldMeaning
versionAlways 2.
options.merge_on_pushDefault false. When true, pushes merge into the run instead of replacing its metrics (versioned mode only); an explicit options.merge wins.
tags.<key>type (text or number), values (closed set), description. run_kind is reserved.
metrics.<name>direction (required), unit (≤20 chars), description.
runs.<kind>tags: every tag this kind carries, the series axis included; metrics: the ones it may carry; description.

A push is rejected with a 400 listing every problem in details when run_kind is missing or unknown, a tag of its run type is missing or another tag is present, a value breaks type or values, or a metric is not in its run type. Saving sets each metric's unit and direction.

Routes

With the same project write token as a push:

RouteBody
GET /v1/schema{ schema, enforced }; schema is null when none is stored.
PUT /v1/schema{ schema, enforced } replaces the document; schema: null removes it (then enforced must be false).
GET /v1/project{ project: { slug, name, seriesAxis, mode, axisKind }, hasRuns }: the token's project and whether it holds live (not deleted) runs.

Errors

{
  "error": "Invalid request body",
  "details": [
    {"path":"runs.0.metrics.0.value","message":"expected number"}
  ]
}
StatuserrorWhen
400Invalid request bodyA field failed validation, the axis value is missing or does not parse as the axis kind, a field conflicts with the project (a metric's unit or direction, a tag's type), or the push does not fit the enforced schema; details names each path. A lone run sent as the body is refused here too.
400Invalid JSON bodyThe body is not JSON.
401Invalid or missing project tokenNo bearer token, or one that is unknown or revoked.
403Trial endedThe organization's trial is over; code is "trial". Pushes, /v1/schema and /v1/project are refused until the organization is enabled.
413Request body too largeOver 100 KB.
500Internal server errorUnexpected failure; nothing was written.

details is present only on a validation failure; code, when present, is a stable machine-readable reason.