Metrics ingest API
The endpoint a metric source posts to. One header, some JSON, done.
POST https://api.watchtower15.com/api/metric-sources/ingestAuthorization: Bearer wt15ms_…Content-Type: application/jsonThe source’s page and the Metrics page in the dashboard show this URL on
app.watchtower15.com. Behind a firewall that allows outbound traffic by IP address,
use api.watchtower15.com and allow 3.11.48.155, 18.171.1.44 and 18.135.154.177
on port 443: those addresses serve api.watchtower15.com only. See the Restricted
egress tab under Install for the list and when
it changes.
Request
Section titled “Request”The body may be any of the following — the format is detected from the content,
not from Content-Type:
| Form | Example |
|---|---|
| Envelope | {"points":[{"metric":"a","value":1}]} |
| JSON array | [{"metric":"a","value":1}] |
| Single object | {"metric":"a","value":1} |
| NDJSON | one JSON object per line |
| gzip | any of the above, gzipped |
A batch — the form to build first
Section titled “A batch — the form to build first”{ "points": [ { "metric": "files.uploaded", "value": 1, "recorded_at": "2026-09-16T14:03:00Z" }, { "metric": "files.uploaded", "value": 1, "recorded_at": "2026-09-16T14:03:07Z" }, { "metric": "queue.depth", "value": 812, "recorded_at": "2026-09-16T14:03:00Z" }, { "metric": "etl.last_status", "value": 1, "recorded_at": 1758031380 }] }A single point is equally valid — {"metric":"files.uploaded","value":1} — and is
fine for a genuinely low-rate publisher. What limits it is the request rate, not
correctness: above roughly 20 events a minute, buffer and send a batch.
Fields
Section titled “Fields”| Field | Required | Notes |
|---|---|---|
metric |
one of these two | The metric key, as it appears in WatchTower15. A-Z a-z 0-9 . _ -, up to 100 characters. A key we do not recognise is handled by your organisation’s declaration setting — see New metric keys. Keys are scoped to the source whose token you are using. |
metric_uuid |
one of these two | The metric’s WatchTower15 UUID, if you would rather not depend on a string. Never creates a metric, and must belong to the source whose token you are using — anything else is an error. |
value |
yes | A number. For a level, the measurement — negative and fractional are both fine. For a count, how many occurrences this point represents. |
recorded_at |
no | RFC 3339 (2026-09-16T14:03:00Z) or a Unix epoch in seconds or milliseconds, as a number or a string. A time without a timezone is read as UTC, and fractions of a second are kept. Defaults to now. Reaches back as far as your data is kept — see Backfilling history. More than 5 minutes in the future is rejected. |
Send either metric or metric_uuid, not both. Sending both with different
values is rejected rather than resolved in your favour — we would rather refuse than
write to the wrong metric.
Counts: zero yes, negative no
Section titled “Counts: zero yes, negative no”| Value on a count metric | |
|---|---|
0 |
Accepted, and useful. It adds nothing to a total and proves your publisher is alive — silence cannot tell us whether nothing happened or the job died. |
| Negative | Rejected. “−1 files uploaded” is not a thing, and allowing it would let a mistake quietly erase occurrences that already happened. |
Levels accept negatives freely — account balances and temperatures need them.
Backfilling history
Section titled “Backfilling history”You can send points with times as far back as we keep the data — three years. That covers a publisher that was down for a week, and it covers importing history from whatever you were using before.
There are two allowances, and the line between them is 30 days:
| Points | Allowance | Why |
|---|---|---|
| Newer than 30 days | 100,000 per metric per rolling 24 hours | Alarms can look back at most 30 days, so these points are the ones an alarm may read. |
| Older than 30 days | 2,000,000 per metric per rolling 24 hours | Nothing this old can be read by any alarm, so it costs storage and nothing else. |
Both are per metric, and both are rolling — use an allowance up at 2pm and it refills at 2pm the next day, not at midnight. A three-year import at one point a minute is about 1.6 million points, so it fits inside a single day’s backfill allowance.
An Idempotency-Key is required for backfill
Section titled “An Idempotency-Key is required for backfill”Any point older than 30 days must arrive with an Idempotency-Key header, or it is
rejected with missing_idempotency_key. Build the key from what you are sending —
the metric and the time range, or a chunk number — never from the attempt:
Idempotency-Key: files.uploaded/2024-03-01/chunk-1That way an import that fails half way and starts again produces the same keys, and the chunks that already landed are ignored instead of written twice. WatchTower15 remembers each key for 30 days: an import restarted within 30 days skips the chunks that landed. After that the key is forgotten, and a chunk sent again under it is written again.
Alarms see backfilled data
Section titled “Alarms see backfilled data”Backfilled points inside the 30-day window are evaluated like any others, so importing history can open incidents. That is deliberate — an alarm that ignored data you supplied would be worse. Import outside business hours if that matters to you, or switch the metric’s alarms off while you import.
Retries
Section titled “Retries”Send an Idempotency-Key header and reuse the same value when you retry. The
common case is a request that worked at our end and timed out at yours, where
retrying is the right instinct; without a key, that retry is counted twice.
Idempotency-Key: 2026-09-16T14:03:00Z/files.uploaded/batch-17Without one, only retry on a non-2xx response. Retrying never affects your bill — metrics are charged per metric per month, not per point.
Response
Section titled “Response”{ "status": "success", "data": { "accepted": true, "batch_id": "…", "points": 3, "rejected": { "over_metric_cap": 0, "bad_metric_key": 0, "unknown_metric_uuid": 0, "ambiguous_identifier": 0, "future_timestamp": 0, "too_old": 0, "non_numeric": 0, "negative_on_count_metric": 0, "over_point_rate": 0, "metric_key_deleted": 0 } } }accepted means the batch is safely queued, not that it is already on a chart. A
value typically becomes chartable within about half a minute of being accepted —
if you push and immediately reload, give it a moment before concluding it did not
arrive.
rejected always lists every reason, at zero if it did not happen. A batch can be
partly accepted: if one metric is over its points limit, its points are dropped and
the rest of the batch is written. Log these counts. They are the difference between
noticing that your clock is five minutes fast and filing a ticket about a missing
graph.
| Reason | What to do |
|---|---|
bad_metric_key |
The key has characters we do not accept. Copy it from the metric’s page. |
unknown_metric_uuid |
The UUID does not exist in your organisation, is switched off, or belongs to a different source than the token you used. |
ambiguous_identifier |
You sent both metric and metric_uuid. Send one. |
future_timestamp |
Your clock is ahead, or you sent a future time. We reject rather than silently move it. |
too_old |
Older than we keep data for this metric — three years. |
non_numeric |
value was not a number, or recorded_at was unparseable (that point alone is rejected; the rest of the batch is accepted). Note a bare year like "2026" is not read as an epoch — only 10-digit-or-longer numbers are. |
negative_on_count_metric |
A count cannot go backwards. |
over_point_rate |
That metric used up its allowance for recent points. Batch, or ask us to raise it. |
over_backfill_budget |
That metric used up its separate, much larger allowance for backfilled points. Wait for it to refill, or ask us to raise it. |
missing_idempotency_key |
You sent backfilled points with no Idempotency-Key header. See Backfilling history — it is required, and it protects you from double-counting. |
over_metric_cap |
You are at your organisation’s metric limit. Delete one, or ask us to raise it. |
metric_create_busy |
A new key arrived while your organisation was busy, or while its metric source was being saved, so its metric was not created this time. Send it again; the next push creates it. Points for existing metrics are not affected. |
metric_key_deleted |
That key belonged to a deleted metric of this source. Create a new metric, or use a new key. |
metric_awaiting_approval |
The metric exists but nobody has accepted it yet. Approve it in Metrics, and points sent after that are recorded. Points sent before are not stored. |
New metric keys
Section titled “New metric keys”What happens when you push a key we have not seen depends on one organisation setting, Require approval for new custom metrics, under Organisation → Settings. It is on by default.
| Setting | What a new key does |
|---|---|
| On (default) | The metric is created pending. It stores nothing and is not billed. Your push is answered 200 with a metric_awaiting_approval count, so nothing is silently dropped. Someone approves it in Metrics, and recording starts from that moment. |
| Off | The metric is created and starts recording immediately, and is billed from creation. |
Billing starts at approval, never at the first push — a metric that waits three days for a decision is not charged for those three days.
Approving is deliberate rather than automatic because each metric is billed monthly. Discarding a pending metric removes it permanently and frees its key for reuse.
Which metrics a token can reach
Section titled “Which metrics a token can reach”A metric belongs to exactly one metric source, and only that source’s token can write to it.
- Metric keys are unique per source, not per organisation. Two sources can each
have a
purchase_orders.created; they are separate metrics, with separate history, billed separately. - A fleet of machines sharing one token all write to the same metric, however many of them there are. This is the supported way to aggregate across instances — nothing is keyed by machine.
- Deleting a source deletes every metric it owns, along with their history and alarms.
Status codes
Section titled “Status codes”| Code | Meaning |
|---|---|
200 |
Accepted. Check points and rejected. |
400 |
The body could not be read, or contained nothing usable. |
403 |
Token missing, unknown, revoked, source paused or deleted, or organisation suspended. Every one of these returns the same response. |
413 |
Too many points, or the body is over the size cap. |
429 |
Rate limited. Retry-After and X-RateLimit-* are set. |
Limits
Section titled “Limits”| Default | |
|---|---|
| Requests per minute, per source | 60 |
| Recent points (newer than 30 days), per metric | 100,000 per rolling 24h |
| Backfilled points (older than 30 days), per metric | 2,000,000 per rolling 24h |
| Points per request | 1,000 |
| Request body | 1 MB (10 MB decompressed) |
How far back recorded_at may reach |
3 years |
| Future tolerance | 5 minutes |
| Metrics per organisation | 5 by default (the Account Limits card shows yours) |
The point allowances are rarely the constraint people expect. A count metric tracking
10,000 events a minute sends one point with "value": 10000 — the point rate has
nothing to do with your event rate. 100,000 points a day is a sample roughly every
second, finer than any chart draws.
Both point allowances are per metric and rolling: use one up at 2pm and it refills at 2pm tomorrow, not at midnight.
All of these are raisable. Ask.