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
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| runs | array | yes | 1–50, applied in order in one transaction | The runs, applied in order, all or nothing. |
runs[]
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| tags | object | yes | 1–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. |
| timestamp | string | no | RFC 3339, with an offset, at most a day ahead | Series time of the run, at most a day ahead. Defaults to now; set it to backfill. |
| meta | object | no | at most 30 keys, any JSON value | Free-form provenance — commit, CI URL. Never identifies a run. |
| metrics | array | yes | 1–200, names unique per run | Every number this run measured, pushed together. |
| options | object | no | { merge?: boolean }, unknown keys rejected | Flags that change how this run is applied. |
runs[].metrics[]
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| name | string | yes | slug-safe · ≤120 chars | Metric name; the project defines a metric on first sight. |
| value | number | yes | finite | The measured number. |
| unit | string | no | free text · ≤20 chars | Display unit — MB, KB, ms, %. Set on first sight; later pushes must match or omit it. |
| direction | string | no | lower_is_better | higher_is_better | Which way is good. Set on first sight; later pushes must match or omit it. |
runs[].options
| Field | Type | Required | Constraints | Description |
|---|---|---|---|---|
| merge | boolean | no | default false | Versioned 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 anddetailspaths start withruns.<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 (tagsandmetricsat the top) is a400asking to wrap it inruns. - Every run carries the axis tag (optional for
text), and its value must parse as the project's axis kind (version,number,timestamportext); a missing or unparseable value is a400naming the expected format. An axis value that parses the same as a stored one but is spelled differently (v1.2.3vs1.2.3) is a400naming 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.mergeis set, which updates and adds the pushed metrics and keeps the rest. Merge is versioned mode only; a time-series project rejects it with a400. - A metric's
unitanddirectionare set the first time the project sees it. A later push must send the same values or leave them out; a different one is a400naming the stored value. Change them in project settings. Direction defaults tolower_is_better. - A tag key's value type (text or number) is pinned on first sight too. Mixing types later is rejected.
timestampis 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 a400asking for a timestamp on it.- Pushing a deleted run's tags again restores it. In time-series mode only a push with the same
timestampdoes; 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
tagstometabefore the run is stored, so the response'stagsleave it out. - Tag keys named
axis,from,to,size,cursorormergeare 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"]}}
}| Field | Meaning |
|---|---|
| version | Always 2. |
| options.merge_on_push | Default 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:
| Route | Body |
|---|---|
| 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"}
]
}| Status | error | When |
|---|---|---|
| 400 | Invalid request body | A 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. |
| 400 | Invalid JSON body | The body is not JSON. |
| 401 | Invalid or missing project token | No bearer token, or one that is unknown or revoked. |
| 403 | Trial ended | The organization's trial is over; code is "trial". Pushes, /v1/schema and /v1/project are refused until the organization is enabled. |
| 413 | Request body too large | Over 100 KB. |
| 500 | Internal server error | Unexpected failure; nothing was written. |
details is present only on a validation failure; code, when present, is a stable machine-readable reason.