A price alert watches one provider's price for one model and POSTs a signed JSON body to your endpoint when that price changes.
https.Limit: 10 alerts per account. Alerts are created in the dashboard only. No endpoint creates, lists or deletes one, and /api/v1 stays read-only.
One event type, price.changed, in three kinds:
| kind | Sent when | old |
|---|---|---|
changed | A published price for the listing moves. | The previous values. |
created | The provider starts listing that model. | null |
restored | The provider lists it again after dropping it. | The values held before it was dropped. |
Watched fields: input, output, cache_read, cache_write, reasoning, context, unit. A rename, a new tag, or a re-read that returns the same number is not a change and sends nothing.
Sources are polled every five minutes. A move large enough to look like a parsing error is held until a second reading confirms it, so a webhook arrives once, after the price is published, and never twice for the same change.
POST /your/endpoint HTTP/1.1
content-type: application/json
user-agent: indextkn-webhooks/1| Header | Value |
|---|---|
| X-Indextkn-Event | price.changed |
| X-Indextkn-Delivery | Delivery id, same as id in the body. Stable across retries. Key your idempotency on it. |
| X-Indextkn-Timestamp | Unix seconds at signing time. |
| X-Indextkn-Signature | t=<timestamp>,v1=<hex>. See Verify the signature. |
{
"id": "0f2b6c48-1f7e-4a02-9a1e-8b0c0f0f0f0f",
"type": "price.changed",
"kind": "changed",
"created_at": "2026-08-31T12:04:07.000Z",
"alert": {
"id": "3a9f2c10-7d61-4f5e-9a2b-1c2d3e4f5a6b",
"provider": "anthropic",
"model": "anthropic/claude-opus-4.5"
},
"model": {
"id": "anthropic/claude-opus-4.5",
"lab": "anthropic",
"name": "Claude Opus 4.5",
"modality": "text"
},
"provider": "anthropic",
"provider_model_id": "claude-opus-4-5",
"changed": ["input", "cache_read"],
"old": {
"name": "Claude Opus 4.5",
"input": 5,
"output": 25,
"cache_read": 0.5,
"cache_write": 6.25,
"reasoning": null,
"unit": "1M_tokens",
"context": 200000,
"max_output": 64000,
"status": "accurate"
},
"new": {
"name": "Claude Opus 4.5",
"input": 4.5,
"output": 25,
"cache_read": 0.45,
"cache_write": 6.25,
"reasoning": null,
"unit": "1M_tokens",
"context": 200000,
"max_output": 64000,
"status": "checking"
},
"source_url": "https://www.anthropic.com/pricing",
"observed_at": "2026-08-31T12:04:07.000Z"
}| Field | Type | Example | Description |
|---|---|---|---|
id | string | 0f2b6c48-... | Delivery id. Same value as the X-Indextkn-Delivery header, and stable across retries. |
type | string | price.changed | Event type. Only one exists today. |
kind | string | changed | changed, created or restored. See Events. |
created_at | string | 2026-08-31T12:04:07.000Z | ISO 8601 UTC. When the change was observed. |
alert.id | string | 3a9f2c10-... | The alert this delivery belongs to. |
alert.provider | string | anthropic | Provider the alert watches. |
alert.model | string | anthropic/claude-opus-4.5 | Model the alert watches. |
model.id | string | anthropic/claude-opus-4.5 | Canonical model id, lab/model. |
model.lab | string | anthropic | Who built the model. |
model.name | string | Claude Opus 4.5 | Display name. |
model.modality | string | text | text, image, audio, embedding, rerank, and so on. |
provider | string | anthropic | Who sells it. Same as alert.provider. |
provider_model_id | string | claude-opus-4-5 | The provider's own id for the model. |
changed | string[] | ["input"] | Which fields moved. Empty only on a restore that changed nothing. |
old | object | null | n/a | Every value held before the change. null when kind is created. |
new | object | n/a | Every value held after it. |
source_url | string | https://www.anthropic.com/pricing | The provider page or endpoint the price was read from. |
observed_at | string | 2026-08-31T12:04:07.000Z | Same as created_at. Present so the body reads on its own. |
The same shape on both sides. old is every value held before the change, new is every value held after it, and changed names the difference. Prices are USD per unit. There is no history in the payload: the two states either side of this change, nothing older.
| Field | Type | Example | Description |
|---|---|---|---|
name | string | Claude Opus 4.5 | The provider's name for the listing. |
input | number | null | 4.5 | USD per unit for input tokens. null when not published. |
output | number | null | 25 | USD per unit for output tokens. |
cache_read | number | null | 0.45 | USD per unit for cached input tokens. |
cache_write | number | null | 6.25 | USD per unit to write the cache. |
reasoning | number | null | null | USD per unit for reasoning tokens, where billed apart from output. |
unit | string | 1M_tokens | What the prices are per: 1M_tokens, 1M_chars, image, second, hour, request. |
context | number | null | 200000 | Context window, in tokens, as this provider serves it. |
max_output | number | null | 64000 | Max output tokens, as this provider serves it. |
status | string | accurate | Confidence in this price. See Confidence. |
old.status and new.status carry the accuracy status each price held when it was published, the same values /api/v1 puts on an offer. The old price keeps the confidence it was published with, so a delivery says both how far the number you were using was trusted and how far its replacement is.
| status | Meaning |
|---|---|
accurate | No open doubt about the number. |
checking | A web verification of this price is in flight. |
suspicious | An open doubt: a large unconfirmed move, a cross-provider outlier, or an inverted input/output pair. |
partial | The number is not in doubt, but the provider publishes only one price leg, so a total costed from it comes out short. |
failed | A web check ran and could not confirm the price. |
Status is not one of the watched fields: a status moving on its own does not send a webhook. Read new.status before acting on a price automatically. suspicious and failed mean the number has not been confirmed.
Compute the HMAC over the raw request bytes. Re-serializing parsed JSON changes the bytes and will not match.
X-Indextkn-Signature: t=1756642, v1=8a1f...c2
signed_payload = "<t>" + "." + "<raw request body>"
v1 = hex(hmac_sha256(alert_secret, signed_payload))t and v1 from the header.t is more than five minutes old.hmac_sha256(secret, t + "." + rawBody) as hex.v1 in constant time.import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody must be the exact bytes we sent, before any JSON parsing.
export function verify(rawBody, signature, secret) {
const parts = Object.fromEntries(
signature.split(",").map((p) => p.split("=")),
);
// Reject anything older than five minutes: that is what the timestamp is for.
if (Math.abs(Date.now() / 1000 - Number(parts.t)) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${parts.t}.${rawBody}`)
.digest("hex");
return (
expected.length === parts.v1.length &&
timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1))
);
}| Your response | What happens |
|---|---|
2xx | Delivered. Nothing further is sent for that change. |
429 or 5xx | Retried up to 3 attempts in total, 0.5s then 1s apart. Same delivery id and same body each time. |
Other 4xx | Not retried. A 4xx says the request itself is wrong, so repeating it changes nothing. |
3xx | Not followed, counted as a failure. Point the alert at the final URL. |
| No response | Timed out after 10s and retried, same as a 5xx. |
https on port 443, no credentials in the URL.X-Indextkn-Delivery as an idempotency key. A retry repeats it.After 10 consecutive failed deliveries the alert is disabled and stops firing. The dashboard shows it as disabled with the last error. Resuming it there clears the counter and starts delivery again. A single successful delivery also resets the counter.
Every attempt is recorded against your account: the time, the listing, the fields that changed, the URL it went to, the exact body sent, and the status your endpoint returned. The dashboard shows the last 30 days under Webhook log. Deleting an alert deletes its log with it.