{"openapi":"3.1.0","info":{"title":"Norra metrics ingestion","version":"v1","summary":"Push a company's own numbers into Norra.","description":"Send what your systems already know. A delivery is stored before anything reads it, so a number in a report can always be traced back to the bytes it came from.\n\nAuthenticate with `Authorization: Bearer <access key>`. A key belongs to one source of one company. It is shown once when it is made, it expires after a year, and it can be rotated with an overlap so you deploy without a gap.\n\nA key has a scope, chosen when it is made: `read` reads the source's samples, `write` pushes deliveries, and `read_write` does both. The scope cannot change after the key is made; for a different scope, make a new key. Each operation names the scope it needs. A key without it gets 403 with the code `insufficient_scope`.\n\nEvery delivery carries an idempotency key. The same key returns the same receipt and stores nothing twice.\n\nRate limit: 60 requests a minute per key, reads and pushes together, bursting to 20. A refusal is 429 with `Retry-After` in seconds.\n\nA delivery is at most 8388608 bytes.\n\nA sandbox key stores and parses like any other and marks what it stores as test, so nothing it sends reaches a report.\n\nThe documentation site can send a request from its reference. A request from that site takes a sandbox key or the demo key. A production key is refused with 403 and the code `docs_origin_needs_sandbox`, before anything is read and without recording a use. The demo key reads fixture samples on the demo deployment, https://demo.norra.hello-why.com, and cannot push. It is limited to 10 requests a minute per client network, bursting to 5.\n\nVersions live in the path. Additions happen inside a version; a remove or a rename is a new one, and both run for 90 days before the old one stops (see the changelog).\n\nA webhook source is not pushed to with a key: its sender signs each delivery with the source's signing secret. The Webhooks section says how.","x-norra-changelog":[{"version":"v1","date":"2026-09-24","says":"Webhooks: a signed address per webhook source, with a signing secret, rotation and a retry contract. A write key no longer pushes to a webhook source."},{"version":"v1","date":"2026-09-24","says":"Access keys carry a scope: read, write or read_write. A read endpoint for a source's samples. Keys made before this are write."},{"version":"v1","date":"2026-09-22","says":"The first version. The push endpoint, the receipt and the error shape."}]},"servers":[{"url":"https://app.norra.hello-why.com","description":"The app listener."},{"url":"https://demo.norra.hello-why.com","description":"The demo deployment. Fixture data only, read with the published demo key.","x-norra-demo-key":"nk_d3e00000-0000-4000-8000-000000000001.demo-fixture-read-only"}],"security":[{"accessKey":[]}],"paths":{"/v1/ingest":{"post":{"operationId":"ingest","tags":["Push"],"summary":"Push one delivery.","description":"The body is JSON with an idempotency key, the shape version it is written against, and the payload. A document source sends raw bytes with its own media type instead. Needs a key with the write or read_write scope.","security":[{"accessKey":["write","read_write"]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivery"},"example":{"idempotency_key":"2026-09","payload":{"active_users":"1200","period":"2026-09-01","revenue":"4201000"},"shape_version":1}},"application/octet-stream":{"schema":{"type":"string","format":"binary","description":"The bytes, for a source that delivers a document rather than JSON. The idempotency key travels in the Idempotency-Key header."}}}},"responses":{"202":{"description":"Stored. The receipt names the artifact and the version it will be parsed under.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in the window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the window refills.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Receipt"},"example":{"artifact_id":"0199c1f4-6b3a-7c21-9f10-2a5b4c6d8e90","sandbox":false,"shape_version":1,"status":"queued"}}}},"400":{"description":"The body is not well formed, or a field is missing.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_argument","field":"payload","message":"The body is not well formed, or a field is missing."}}}},"401":{"description":"The key is missing, unknown, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_argument","field":"payload","message":"The key is missing, unknown, revoked or expired."}}}},"403":{"description":"The key works and its scope does not allow this call (`insufficient_scope`), or a production key was sent from the documentation site (`docs_origin_needs_sandbox`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"insufficient_scope","message":"this key lacks the write scope"}}}},"413":{"description":"The delivery is larger than this endpoint accepts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_argument","field":"payload","message":"The delivery is larger than this endpoint accepts."}}}},"429":{"description":"Too many deliveries on this key. Wait the number of seconds in Retry-After.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in the window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the window refills.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-norra-scopes":["write","read_write"]}},"/v1/samples":{"get":{"operationId":"listSamples","tags":["Read"],"summary":"Read the source's samples over a window.","description":"The samples the key's source produced, newest first. Superseded samples are left out. The window is at most 731 days (two years). A from further back than that is moved forward and the answer says so. At most 5000 samples come back; truncated says there were more, so ask for a narrower window. A sample holds a metric key, a period, a value and labels, and never a field the source marks as personal. Needs a key with the read or read_write scope.","security":[{"accessKey":["read","read_write"]}],"parameters":[{"name":"from","in":"query","schema":{"type":"string","format":"date","description":"The first day, inclusive. Unset is as far back as the window reaches."}},{"name":"to","in":"query","schema":{"type":"string","format":"date","description":"The day after the last, exclusive. Unset is tomorrow, so today is in it."}}],"responses":{"200":{"description":"The samples, and the window they were read over.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in the window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the window refills.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Samples"},"example":{"samples":[{"artifact_id":"0199c1f4-6b3a-7c21-9f10-2a5b4c6d8e90","labels":{},"metric":"active_users","period_start":"2026-09-01","sandbox":false,"value":"1200"},{"artifact_id":"0199c1f4-6b3a-7c21-9f10-2a5b4c6d8e90","labels":{},"metric":"revenue","period_start":"2026-09-01","sandbox":false,"value":"4201000"}],"source_id":"0199c1f4-0000-7000-8000-000000000001","truncated":false,"window":{"from":"2024-09-25","max_days":731,"narrowed":false,"to":"2026-09-25"}}}}},"400":{"description":"from or to is not a date, or from is not before to.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_argument","field":"payload","message":"from or to is not a date, or from is not before to."}}}},"401":{"description":"The key is missing, unknown, revoked or expired.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_argument","field":"payload","message":"The key is missing, unknown, revoked or expired."}}}},"403":{"description":"The key works and its scope does not allow this call (`insufficient_scope`), or a production key was sent from the documentation site (`docs_origin_needs_sandbox`).","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"insufficient_scope","message":"this key lacks the read scope"}}}},"429":{"description":"Too many requests on this key. Wait the number of seconds in Retry-After.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in the window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the window refills.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}},"x-norra-scopes":["read","read_write"]}},"/v1/webhooks/{source_id}":{"post":{"operationId":"deliverWebhook","tags":["Webhooks"],"summary":"Deliver one webhook to a webhook source.","description":"The body is the delivery itself, JSON or CSV, stored as it arrived. It is signed with the source's signing secret, and an access key does not open this address. The Webhooks section has the signature and the retry contract.","security":[{"webhookSignature":[]}],"parameters":[{"name":"source_id","in":"path","description":"The webhook source, as its address on the sources page names it.","required":true,"schema":{"type":"string","format":"uuid"}},{"name":"Norra-Signature","in":"header","description":"t=<unix seconds>,v1=<hex HMAC-SHA256 of <t>.<delivery id>.<raw body>>.","required":true,"schema":{"type":"string"}},{"name":"Norra-Delivery-Id","in":"header","description":"Your id for this delivery, and its idempotency key. Letters, digits, dash, underscore or colon, at most 200.","required":true,"schema":{"type":"string"}},{"name":"X-Norra-Shape-Version","in":"header","description":"The shape version the body is written against. Absent means the source's current version.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","description":"One object, or an array of objects, carrying the source's declared fields."},"example":{"active_users":"1200","period":"2026-09-01","revenue":"4201000"}},"text/csv":{"schema":{"type":"string","description":"A header row naming the declared fields, then one row per record."}}}},"responses":{"202":{"description":"Stored. The receipt names the artifact. A redelivery with the same delivery id answers with the first receipt and stores nothing.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in the window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the window refills.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Receipt"},"example":{"artifact_id":"0199c1f4-6b3a-7c21-9f10-2a5b4c6d8e90","sandbox":false,"shape_version":1,"status":"queued"}}}},"400":{"description":"The signature header or the delivery id is not well formed, or the signature's time is outside the window. Do not retry as is.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_argument","message":"The signature header or the delivery id is not well formed, or the signature's time is outside the window. Do not retry as is."}}}},"401":{"description":"There is no Norra-Signature header. Do not retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthenticated","message":"There is no Norra-Signature header. Do not retry."}}}},"404":{"description":"Nothing at this address accepts that signature. Do not retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"not_found","message":"Nothing at this address accepts that signature. Do not retry."}}}},"413":{"description":"The body is larger than this endpoint accepts. Do not retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"too_large","message":"The body is larger than this endpoint accepts. Do not retry."}}}},"415":{"description":"The body is not JSON or CSV. Do not retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unsupported_media_type","message":"The body is not JSON or CSV. Do not retry."}}}},"429":{"description":"Too many deliveries on this source. Retry after the number of seconds in Retry-After.","headers":{"RateLimit-Limit":{"description":"Requests allowed in the window.","schema":{"type":"integer"}},"RateLimit-Remaining":{"description":"Requests left in the window.","schema":{"type":"integer"}},"RateLimit-Reset":{"description":"Seconds until the window refills.","schema":{"type":"integer"}},"Retry-After":{"description":"Seconds to wait.","schema":{"type":"integer"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"5XX":{"description":"Norra could not take the delivery. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"internal","message":"Norra could not take the delivery. Retry with backoff."}}}}}}}},"components":{"securitySchemes":{"accessKey":{"type":"http","scheme":"bearer","description":"An access key made on the company's developer page. It goes in the Authorization header and never in the URL."},"webhookSignature":{"type":"apiKey","name":"Norra-Signature","in":"header","description":"An HMAC-SHA256 signature over the delivery, made with the source's signing secret. The Webhooks section says how to compute it."}},"schemas":{"Delivery":{"type":"object","required":["idempotency_key","shape_version","payload"],"properties":{"idempotency_key":{"type":"string","description":"Your own name for this delivery. The same key returns the same receipt.","maxLength":200},"payload":{"type":"object","description":"The delivery itself, matching the source's declared shape."},"shape_version":{"type":"integer","description":"The version of the source's declared shape this payload is written against.","minimum":1}}},"Error":{"type":"object","required":["code","message"],"properties":{"code":{"type":"string","description":"A short machine-readable reason."},"field":{"type":"string","description":"The field it is about, when it is about one."},"message":{"type":"string","description":"One plain sentence."}}},"Receipt":{"type":"object","required":["artifact_id","shape_version","status"],"properties":{"artifact_id":{"type":"string","format":"uuid","description":"What was stored. Cite this when you ask why a number is what it is."},"sandbox":{"type":"boolean","description":"The key was a sandbox key, so what this becomes is test."},"shape_version":{"type":"integer","description":"The version it will be parsed under."},"status":{"type":"string","description":"Where the delivery is.","enum":["queued"]}}},"Sample":{"type":"object","required":["metric","period_start","value","labels","sandbox"],"properties":{"artifact_id":{"type":"string","format":"uuid","description":"The delivery the sample was parsed from, when it came from one."},"labels":{"type":"object","description":"The label set, as the metric declares it.","additionalProperties":{"type":"string"}},"metric":{"type":"string","description":"The metric key."},"period_start":{"type":"string","format":"date","description":"The first day of the period, in the company's timezone."},"sandbox":{"type":"boolean","description":"The delivery came on a sandbox key, so the sample is test."},"value":{"type":"string","description":"An exact decimal, as a string so nothing reads it as a float."}}},"Samples":{"type":"object","required":["source_id","window","samples","truncated"],"properties":{"samples":{"type":"array","description":"Newest first.","items":{"$ref":"#/components/schemas/Sample"}},"source_id":{"type":"string","format":"uuid","description":"The source the key belongs to."},"truncated":{"type":"boolean","description":"There were more samples in the window than one answer holds."},"window":{"$ref":"#/components/schemas/Window"}}},"Window":{"type":"object","required":["from","to","max_days","narrowed"],"properties":{"from":{"type":"string","format":"date","description":"The first day read."},"max_days":{"type":"integer","description":"The widest window a read answers for."},"narrowed":{"type":"boolean","description":"The from asked for was further back than the window reaches, and was moved forward."},"to":{"type":"string","format":"date","description":"The day after the last day read."}}}}},"tags":[{"name":"Push","description":"Push a delivery with a write key."},{"name":"Read","description":"Read a source's samples with a read key."},{"name":"Webhooks","description":"A webhook source is fed by your system calling Norra when something happens. Each webhook source has its own address, `POST /v1/webhooks/{source_id}`, and its own signing secret. The secret is shown once, when the source becomes a webhook source, and can be rotated on the company's sources page.\n\nSign every attempt. Send `Norra-Signature: t=<unix seconds>,v1=<signature>`. The signature is the lowercase hex HMAC-SHA256 of `<t>.<delivery id>.<raw body>`, keyed with the signing secret exactly as it was shown, prefix included. Norra refuses a time more than 300 seconds from its own clock, either way, so sign each attempt when you send it.\n\nName every delivery. `Norra-Delivery-Id` is your id for the event: letters, digits, dash, underscore or colon, at most 200 characters. It is signed, and it is the idempotency key: a redelivery with the same id stores nothing twice and answers with the first receipt, however long after the first it comes. A delivery is also stored by the hash of its body, so two deliveries with byte-identical bodies are one delivery whatever their ids. Put the event's own id or time in the body.\n\nThe body is the delivery itself: JSON, one object or an array of objects, or CSV with a header row, carrying the fields the source declares. It is stored exactly as it arrived and parsed afterwards. `X-Norra-Shape-Version` names the shape version it is written against; without it, the source's current version.\n\nSignature only. An access key does not open a webhook, and a write key cannot push to a webhook source. A key in a sender's configuration travels with every call and works for as long as it lives. A signature covers one body at one moment, so a copied request cannot be changed and stops working after 300 seconds.\n\nRotation. A new signing secret works at once, and the old one keeps working for 24 hours, so you can switch without a gap. A second rotation inside that window ends the older secret at once.\n\n2xx: accepted and stored. The receipt names the artifact. Do not send it again.\n\n4xx: do not retry the same request. 401 is a request with no `Norra-Signature` header at all. 400 is a signature header or delivery id that is not well formed, or a time outside the window: fix it and sign again. 404 is an address and signature that open no webhook source. The source does not exist, is not a webhook source, or the secret is wrong. All three get the same answer, so the address tells somebody without the secret nothing. 413 is a body over 8388608 bytes. 415 is a body that is not JSON or CSV.\n\n429 and 5xx: retry. On 429, wait the seconds in Retry-After. Otherwise back off exponentially from 30 seconds, doubling each time up to one hour between attempts, with jitter. Give up after 3 days. Sign each attempt anew and keep the same delivery id.\n\nRate limit: 60 deliveries a minute per source, bursting to 20."}],"x-norra-metrics":[{"key":"active_users","name":"Active users","unit":"count"},{"key":"revenue","name":"Revenue","unit":"SEK"}]}