{"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 a service connection, made on the company's developer page. A connection belongs to one company and is bound to one of its sources, so a call names neither. It expires after a year and can be rotated with an overlap so you deploy without a gap.\n\nUse client credentials where you can. The connection has a client id and a secret that starts `ncs_`. POST `grant_type=client_credentials` to `https://app.norra-co.com/oauth/token` with the id and the secret in HTTP Basic, and send the `access_token` it answers with as `Authorization: Bearer <token>`. A token lasts an hour; ask for a new one a minute before `expires_in` runs out, and once more on a 401. The secret never goes to the endpoints below.\n\nA device or a script that cannot exchange a token uses a static credential instead. It starts `nsk_` and is sent as the bearer itself. Both kinds are shown once, when the connection is made.\n\nA connection holds permissions, chosen when it is made: `sources.ingest` pushes deliveries and `metrics.read` reads the source's samples. The permissions cannot change after it is made; for different ones, make a new connection. Each operation names the permission it needs. A connection without it gets 403 with the code `permission_denied`, and a `WWW-Authenticate` header with `error=\"insufficient_scope\"` naming the permission.\n\nA connection that is not bound to a source names one with `source_id`, and a connection that acts on more than one company names it with `orgnr`.\n\nEvery delivery carries an idempotency key. The same key with the same body returns the same receipt and stores nothing twice. The same key with a different body is refused with 409 and the code `key_reused`, and nothing in it is kept.\n\nA delivery keeps only the fields its source declares. Any other key is dropped before the delivery is stored, and the receipt lists it in `dropped_keys`.\n\nA source delivers events or statements, chosen when the company declares it, and the receipt says which in `delivers`. An events source adds every delivery's rows to what came before. To correct a number, send a row that cancels the wrong one, such as a negative amount or a refund. A statements source takes each delivery as the whole of every period it has a row in, a day, a week or a month as each metric counts it. The delivery received last for a period replaces what earlier ones said about it, and the period is marked restated. To correct a number, send the whole period again. A period sent with only some of its rows reads as only those rows. Send a statement once a week, and on demand in between when a number has to change sooner. Each statement holds every day of each period it touches, so a monthly metric reads the month so far, not one day of it.\n\nRate limit: 60 requests a minute per connection, 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 connection 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 connection or the demo connection. A production connection is refused with 403 and the code `docs_origin_needs_sandbox` before anything is read. The demo connection reads fixture samples on the demo deployment, https://demo.norra-co.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 over a connection: its sender signs each delivery with the source's signing secret. The Webhooks section says how.","x-norra-changelog":[{"version":"v1","date":"2026-10-02","says":"A source delivers events or statements, chosen when it is declared. Events add up, and a correction is a row that cancels the wrong one. A statement is the whole of every period it has a row in, so the delivery received last for a period replaces the earlier ones and the period is marked restated. The receipt says which in `delivers`."},{"version":"v1","date":"2026-10-02","says":"A timestamp field takes a date alone, such as `2026-09-01`, and reads it as midnight on that day in the company's timezone. A time with no offset is still refused. The developer page says how many rows of the last delivery were read that way."},{"version":"v1","date":"2026-09-30","says":"Client credentials. A service connection can hold a client id and secret instead of a static credential, and the developer page makes that kind first. Exchange them at `/oauth/token` for an access token and push with the token. The static credential stays for devices. Webhook sources still sign with HMAC."},{"version":"v1","date":"2026-09-30","says":"Access keys are service connections. A company makes one on its developer page, bound to a source, with a static credential that starts `nsk_`. A connection holds `sources.ingest` to push and `metrics.read` to read; a missing one is 403 `permission_denied`. `nk_` keys no longer open anything. There was no production traffic, so there is no deprecation window."},{"version":"v1","date":"2026-10-02","says":"A delivery keeps only the fields its source declares. Any other key is dropped before the delivery is stored, and the receipt lists it in `dropped_keys`."},{"version":"v1","date":"2026-09-25","says":"A company can pause a source. A push or a webhook to a paused source is refused with 409 and the code `source_paused`, and nothing it carried is kept."},{"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-co.com","description":"The app listener."},{"url":"https://demo.norra-co.com","description":"The demo deployment. Fixture data only, read with the published demo connection.","x-norra-demo-credential":"nsk_d3e00000-0000-4000-8000-000000000002.demo-fixture-read-only"}],"security":[{"clientCredentials":[]},{"serviceConnection":[]}],"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 connection that holds sources.ingest.","security":[{"clientCredentials":["sources.ingest"]},{"serviceConnection":["sources.ingest"]}],"parameters":[{"name":"source_id","in":"query","description":"The source. Only for a connection bound to none; a bound connection may name its own source and no other.","schema":{"type":"string","format":"uuid"}},{"name":"orgnr","in":"query","description":"The company. Only for a connection that acts on more than one.","schema":{"type":"string"}}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Delivery"},"example":{"idempotency_key":"2026-09","payload":{"active_users":"1200","period":"2026-09-01T00:00:00+02:00","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","delivers":"events","dropped_keys":[],"sandbox":false,"shape_version":1,"status":"queued"}}}},"400":{"description":"The body is not well formed, a field is missing, or the credential was put in the URL.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"invalid_argument","field":"payload","message":"The body is not well formed, a field is missing, or the credential was put in the URL."}}}},"401":{"description":"The token or the static credential is missing, unknown, revoked or expired. With client credentials, ask for a new token once and send again. A second 401 is final.","headers":{"WWW-Authenticate":{"description":"Bearer, and where this host's protected resource metadata is.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthenticated","message":"The credential is missing, unknown, revoked or expired."}}}},"403":{"description":"The connection works and does not hold sources.ingest (`permission_denied`, with `WWW-Authenticate: Bearer error=\"insufficient_scope\", scope=\"sources.ingest\"`), or a production connection was sent from the documentation site (`docs_origin_needs_sandbox`), or the source takes signed webhooks only (`webhook_only`).","headers":{"WWW-Authenticate":{"description":"On permission_denied: the permission the connection lacks.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"permission_denied","message":"this connection does not hold that permission on this company"}}}},"404":{"description":"The source is not the company's, or the connection has nothing on the company.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"not_found","field":"source_id","message":"this company has no such source"}}}},"409":{"description":"The company has paused this source (`source_paused`). Nothing in the request is kept, and Norra does not fetch it later. Do not retry on a schedule. Send it again once the source is resumed. Or the idempotency key already stored a different body (`key_reused`). Nothing in the request is kept, and the first delivery stands. Do not retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"source_paused","message":"this source is paused and keeps nothing sent to it; send again once it is resumed"}}}},"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 connection. 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":["sources.ingest"]}},"/v1/samples":{"get":{"operationId":"listSamples","tags":["Read"],"summary":"Read the source's samples over a window.","description":"The samples the connection'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 connection that holds metrics.read.","security":[{"clientCredentials":["metrics.read"]},{"serviceConnection":["metrics.read"]}],"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."}},{"name":"source_id","in":"query","description":"The source. Only for a connection bound to none; a bound connection may name its own source and no other.","schema":{"type":"string","format":"uuid"}},{"name":"orgnr","in":"query","description":"The company. Only for a connection that acts on more than one.","schema":{"type":"string"}}],"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 token or the static credential is missing, unknown, revoked or expired. With client credentials, ask for a new token once and send again. A second 401 is final.","headers":{"WWW-Authenticate":{"description":"Bearer, and where this host's protected resource metadata is.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"unauthenticated","message":"The credential is missing, unknown, revoked or expired."}}}},"403":{"description":"The connection works and does not hold metrics.read (`permission_denied`, with `WWW-Authenticate: Bearer error=\"insufficient_scope\", scope=\"metrics.read\"`), or a production connection was sent from the documentation site (`docs_origin_needs_sandbox`).","headers":{"WWW-Authenticate":{"description":"On permission_denied: the permission the connection lacks.","schema":{"type":"string"}}},"content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"permission_denied","message":"this connection does not hold that permission on this company"}}}},"404":{"description":"The source is not the company's, or the connection has nothing on the company.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"not_found","field":"source_id","message":"this company has no such source"}}}},"429":{"description":"Too many requests on this connection. 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":["metrics.read"]}},"/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 less any key the source does not declare. It is signed with the source's signing secret, and a connection 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. A timestamp field takes RFC 3339 with an offset, such as 2026-09-01T09:12:00Z, or a date alone, such as 2026-09-01, which is read as midnight on that day in the company's timezone. A time with no offset is refused."},"example":{"active_users":"1200","period":"2026-09-01T00:00:00+02:00","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","delivers":"events","dropped_keys":[],"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."}}}},"409":{"description":"The company has paused this source (`source_paused`). Nothing in the request is kept, and Norra does not fetch it later. Do not retry on a schedule. Send it again once the source is resumed. Or the idempotency key already stored a different body (`key_reused`). Nothing in the request is kept, and the first delivery stands. Do not retry.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"},"example":{"code":"source_paused","message":"this source is paused and keeps nothing sent to it; send again once it is resumed"}}}},"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":{"clientCredentials":{"type":"oauth2","description":"A service connection's client id and secret, made on the company's developer page. Exchange them at the token endpoint for an access token, and send the token as a bearer. The secret goes to the token endpoint in HTTP Basic and nowhere else.","flows":{"clientCredentials":{"tokenUrl":"https://app.norra-co.com/oauth/token","scopes":{"metrics.read":"Read the connection's source's samples.","sources.ingest":"Push deliveries to the connection's source."}}}},"serviceConnection":{"type":"http","scheme":"bearer","description":"A service connection's static credential, for a device or a script that cannot exchange a token. 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 with the same body returns the same receipt; with a different body it is refused (`key_reused`). A key that starts with `norra:` is Norra's own and is refused.","maxLength":200},"payload":{"type":"object","description":"The delivery itself, matching the source's declared shape. A timestamp field takes RFC 3339 with an offset, such as 2026-09-01T09:12:00Z, or a date alone, such as 2026-09-01, which is read as midnight on that day in the company's timezone. A time with no offset is refused."},"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","sandbox","dropped_keys","delivers"],"properties":{"artifact_id":{"type":"string","format":"uuid","description":"What was stored. Cite this when you ask why a number is what it is."},"delivers":{"type":"string","description":"How the source counts this delivery. `events` adds its rows to what came before. `statements` makes it the whole of every period it has a row in, replacing what earlier deliveries said about those periods.","enum":["events","statements"]},"dropped_keys":{"type":"array","description":"The keys the delivery carried that the source does not declare, sorted, at most 50. They were not stored. Empty when nothing was dropped.","items":{"type":"string"}},"sandbox":{"type":"boolean","description":"The connection is a sandbox connection, 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 over a sandbox connection, 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 that was read."},"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 over a connection that holds sources.ingest."},{"name":"Read","description":"Read a source's samples over a connection that holds metrics.read."},{"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 and the same body stores nothing twice and answers with the first receipt, however long after the first it comes. The same id with a different body is refused with 409 and the code `key_reused`, and nothing in it is kept. 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 as it arrived, less any key or column the source does not declare, and parsed afterwards. The receipt lists what was dropped. `X-Norra-Shape-Version` names the shape version it is written against; without it, the source's current version.\n\nSignature only. A connection does not open a webhook, and a connection cannot push to a webhook source. A bearer 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. 409 is a source the company has paused (`source_paused`): nothing is kept, so send it again once the source is resumed. It is also a delivery id that already stored a different body (`key_reused`): nothing is kept. 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"}]}