How webhook signatures are built and how to verify them.
Listo sends webhooks to a URL you gave it, with no credential in the request.
The signature is the credential. It is what tells you an event came from Listo,
arrived unaltered, and is not one somebody captured earlier and sent again.Every event carries one header:
X-Listo-Signature: t=1753728063,v1=8f2c9d4e...
t is the Unix time, in seconds, at which the event was signed.
v1 is a hex HMAC-SHA256, keyed on your signing secret, over the string
"<t>.<raw body>".
Note that t is inside the signed string, not merely sitting beside it. That
is the whole point of this scheme over a plain HMAC of the body: a plain one
stays valid forever, so anyone who ever saw a single valid request could resend
it whenever they liked. Binding the timestamp into the digest means a replay
has to either keep the original t, and fall outside the window below, or
change it, and invalidate the signature.
# Verifies the signature on an incoming Listo webhook before trusting a byte of# it. Reads the raw request body on stdin, the way your web framework hands it# to you, and refuses anything that does not check out.## LISTO_SIGNATURE='t=...,v1=...' LISTO_WEBHOOK_SECRETS=whsec_a,whsec_b \# ruby verify_webhook_signature.rb < body.json#require "json"require "openssl"# The window a signature stays good for, matching the one Listo signs against.# It is what stops a request someone captured from being replayed at you later.TOLERANCE_SECONDS = 300# `secrets` is a LIST because a rotation has two secrets live at once: accept# either while you swap over, then drop the old one.def listo_signature_valid?(payload, header, secrets, now: Time.now) match = /\At=(\d+),v1=(\h+)\z/.match(header.to_s) return false if match.nil? timestamp = match[1].to_i signature = match[2] # Checked in BOTH directions. A timestamp far in the past is the replay this # scheme exists to stop; one far in the future is the same attack against a # clock nobody looked at. return false if (now.to_i - timestamp).abs > TOLERANCE_SECONDS secrets.any? do |secret| expected = OpenSSL::HMAC.hexdigest("SHA256", secret, "#{timestamp}.#{payload}") OpenSSL.secure_compare(expected, signature) endend# The RAW bytes, before any JSON parsing. Parsing and re-serializing changes# them (key order, escapes, whitespace) and the signature covers what was sent,# not what your JSON library would have sent.payload = $stdin.readheader = ENV.fetch("LISTO_SIGNATURE", nil)secrets = ENV.fetch("LISTO_WEBHOOK_SECRETS").split(",")unless listo_signature_valid?(payload, header, secrets) # Answer 400 and stop. Do not parse it, do not act on it, do not log it as an # event you received. abort("signature rejected")endevent = JSON.parse(payload)# Events arrive at least once and in no guaranteed order, so treat `id` as the# thing you deduplicate on and the object's own timestamps as the thing you# order by. Answer 2xx quickly and do the work afterwards.puts "#{event["type"]} #{event["id"]}"case event["type"]when "delivery.status_changed", "delivery.updated" delivery = event["data"] puts "order #{delivery["external_id"]} is #{delivery["status"]}"when "delivery.courier_location" location = event.dig("data", "location") puts "courier at #{location["lat"]},#{location["lng"]}"else # A new type is an additive change, so ignore what you do not recognise # rather than failing on it. puts "ignored"end
Four rules, and each of them is a way the check can be wrong while still looking
right.Hash the raw bytes. Sign what arrived on the wire, before any JSON parsing.
Parsing and re-serialising changes the bytes, because your JSON library has its
own ideas about key order, escape sequences and whitespace, and the signature
covers what was sent rather than what your library would have sent. Most web
frameworks give you the raw body if you ask; find out how yours does before
writing the check.Check the timestamp in both directions. Reject anything more than five
minutes old, which is the replay this scheme exists to stop, and anything more
than five minutes in the future. Skipping the second half hands an attacker an
arbitrarily long window by simply choosing a large t.Compare in constant time. Use your language’s constant-time comparison, not
==. A comparison that returns early on the first differing byte leaks how much
of a guess was right, which is enough to walk out a valid signature one byte at
a time.Fail closed. A malformed header, a missing header, an unparseable timestamp
and a signature that does not match are all the same answer: not authentic.
Reject with a 400, and do not parse or act on the body. Never let the verifier
raise; an exception in a verifier is a 500 where a refusal belonged.
You can have two secrets live at once, which is what makes a rotation possible
without dropping events. Listo signs with the primary one; you accept either.
Add a second secret in the partner dashboard.
Nothing changes yet: Listo keeps signing with the first.
Teach your verifier both. This is why the check above takes a list:
accept an event that validates under either secret.
Promote the new secret. Listo starts signing with it. Your endpoint keeps
working through the switch, because it already accepts both.
Drop the old one, in the dashboard and in your own configuration.
Do them in that order. Promote before step 2 and every event in flight is
rejected by an endpoint that has never seen the new secret.
A secret is a secretStore signing secrets the way you store passwords: server side, out of your
repository, out of anything a browser can reach. Anyone holding one can forge an
event your verifier will accept, which is the same as being able to tell your
system that an order was delivered.
Live and test each have their own endpoint URL and their own signing secret. A
secret from one mode never validates an event from the other, which is what
stops a staging deploy from being able to accept production traffic. Rotate them
independently.Putting all of this to work, including responding, deduplicating and retries, is
receive and verify webhooks.
{"docsConfig":{"theme":"mint","$schema":"https://mintlify.com/docs.json","name":"Listo Partner API","description":"Quote, create and track deliveries, and manage your balance, through the Listo Partner Delivery API.","colors":{"primary":"#15171C","light":"#F4F4F5","dark":"#15171C"},"logo":{"light":"https://mintcdn.com/listo-c5a11a00/mreCwXzcEA2bSZog/logo/light.svg?fit=max\u0026auto=format\u0026n=mreCwXzcEA2bSZog\u0026q=85\u0026s=da2b28d464dc6987329dfd5971085dd7","dark":"https://mintcdn.com/listo-c5a11a00/mreCwXzcEA2bSZog/logo/dark.svg?fit=max\u0026auto=format\u0026n=mreCwXzcEA2bSZog\u0026q=85\u0026s=ad013a6a0d979eb46c1cea550ec6a460","href":"/"},"favicon":"https://mintcdn.com/listo-c5a11a00/mreCwXzcEA2bSZog/favicon.svg?fit=max\u0026auto=format\u0026n=mreCwXzcEA2bSZog\u0026q=85\u0026s=f790496dfe0c2ad9971d8de95e45be8d","api":{"playground":{"display":"interactive"}},"thumbnails":{"preset":"minimal","appearance":"dark","background":"https://mintcdn.com/listo-c5a11a00/lF-Gm03tnDMquh_z/images/og-background.jpg?fit=max\u0026auto=format\u0026n=lF-Gm03tnDMquh_z\u0026q=85\u0026s=d73469201c8ffe28afc598d23fde5d84","fonts":{"family":"Archivo ExtraBold","source":"fonts/archivo-800.ttf","format":"ttf"},"eyebrow":{"source":"none"}},"markdown":{"instructions":["Notes on the Listo Partner Delivery API:","- Every endpoint is under `https://api.listo.delivery/v1/` and authenticates with a bearer API key.","- This documentation site is at https://docs.listo.delivery. The full machine-readable reference is the OpenAPI document at https://api.listo.delivery/docs/openapi.yaml (served by the API host, not by this site).","- Any page of this site is also available as raw markdown by appending `.md` to its URL, for example https://docs.listo.delivery/en/guides/webhooks.md. The index of every page is https://docs.listo.delivery/llms.txt and all of them in one file is https://docs.listo.delivery/llms-full.txt.","- Every page is published in English under https://docs.listo.delivery/en/ and in Spanish under https://docs.listo.delivery/es/, with the same path after the language. The API reference is under https://docs.listo.delivery/api-reference/ in English and https://docs.listo.delivery/es/api-reference/ in Spanish.","- To debug an integration yourself: `GET /v1/request_logs` lists the calls your keys made with what you sent and what was answered, `GET /v1/events` lists the events Listo emitted, `GET /v1/webhook_deliveries` lists every attempt to deliver them, `POST /v1/events/{id}/resend` sends one event again and `POST /v1/test_webhooks` sends a sample event (`\"test\": true`). The page https://docs.listo.delivery/en/guides/debugging describes them. A resent event is a snapshot from when it happened; `GET /v1/deliveries` is still how you recover current state.","- Every error response has `error.doc_url`, a page that explains that error code and what to do, and `error.request_id`, the same value as the `X-Request-Id` response header. All the pages are listed at https://docs.listo.delivery/en/errors. Filter `GET /v1/request_logs` with `request_id` to read the logged call.","- A `409 account_exists` from `POST /v1/stores` means the restaurant already has its own Listo account: connect it instead of creating it, as the page https://docs.listo.delivery/en/guides/connect-existing-account describes.","- Every live store carries one Plan Listo, charged to the partner. The exception is a connected restaurant that had already paid its current month: it keeps paying its own plan until its next renewal, shown as `paid_by: \"store\"` with the handover date in `pending_paid_by_on`. A `402 plan_required` from a quote, a delivery create or a retry means the store's plan is not paid or has ended; the page https://docs.listo.delivery/en/guides/plan-and-billing describes the plan, the `plan` block on a store and how to end or restart it.","- An AI assistant (Claude Code, Cursor, Codex) can act through the same API key at `https://api.listo.delivery/mcp`, an MCP server over Streamable HTTP authenticated with `Authorization: Bearer \u003cAPI key>`. Every tool runs the matching `/v1/` endpoint, and each one that moves money first previews and then needs a confirmation token; the page https://docs.listo.delivery/en/guides/connect-ai-client lists the tools and describes the two steps."]},"styling":{"latex":true},"navigation":{"languages":[{"language":"en","default":true,"navbar":{"links":[{"href":"https://status.listo.delivery","label":"Status"}]},"href":"en/quickstart"},{"language":"es","navbar":{"links":[{"href":"https://status.listo.delivery","label":"Estado del servicio"}]},"href":"es/quickstart"}]}},"deployment":{"deploymentMetadataValue":{"subdomain":"listo-c5a11a00","hasAudioNarration":false,"actualSubdomain":"listo-c5a11a00","customDomains":["docs.listo.delivery"],"gitSource":{"type":"github","owner":"davidalejandroaguilar","repo":"listo","deployBranch":"main","contentDirectory":"docs-site","isPrivate":true,"mountPath":"","label":"","_id":"6aed6954347cccfbc77ed1c5"},"entitlements":{"AI_CHAT":{"status":"ENABLED"},"WEB_MCP":{"status":"ENABLED"},"NARRATION":{"status":"ENABLED"}},"buildId":"6aeeb167ae7fce64eae954de","clientVersion":"0.0.4076","searchFilterCounts":[{"language":"en","tag":"Errors","count":43},{"language":"en","tag":"Reference","count":43},{"language":"es","tag":"Errores","count":43},{"language":"es","tag":"Referencia","count":43},{"language":"en","tag":"Guides","count":9},{"language":"es","tag":"Guías","count":9},{"language":"en","tag":"Stores","count":8},{"language":"es","tag":"Tiendas","count":8},{"language":"en","tag":"Deliveries","count":7},{"language":"es","tag":"Envíos","count":7},{"language":"en","tag":"Webhooks","count":6},{"language":"es","tag":"Webhooks","count":6},{"language":"en","tag":"Concepts","count":5},{"language":"es","tag":"Conceptos","count":5},{"language":"en","tag":"Card onboarding","count":4},{"language":"en","tag":"Events","count":4},{"language":"es","tag":"Alta de cobros con tarjeta","count":4},{"language":"es","tag":"Eventos","count":4},{"language":"en","tag":"Withdrawals","count":3},{"language":"es","tag":"Retiros","count":3}]},"assistantConfig":{"enableAskAiButton":false,"enableStarterQuestions":false,"enableContextualStarterQuestions":false,"enableAskAiSearchBar":true},"basePath":"","hasChatPermissions":true,"hasWebMcpPermissions":true,"toggles":[{"name":"kill-isr","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-mintlify-slack","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"agent-integrations-settings","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"rbac-expansion","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-pr-grouping","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"custom-prefix","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"analytics-v7","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"user-flows-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"no-publish-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-onboarding","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"llms-txt-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving-live","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"stripTemperatureMiddleware","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"support-platform-pylon-zendesk","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"snapshot-serving","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"instant-rollback","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications-server","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"granular-notifications","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automations-fable","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fixed-ai-action-pricing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"automation-resolve-merge-conflict","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"scoped-publishing","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"mintql","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"page-level-analytics","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-version-history-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"billing-page-v2","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"full-page-search","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workspace-projects","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"assistant-v5","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"fork-pr-previews","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"revalidation-jwt","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-sso-scim","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"hobby-abuse-scan","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"dashboard-inbox","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-signup","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-dual-write","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"workos-enabled","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"approve-review-flow","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"editor-metadata","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"sso-setup-deadline-banner","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"turbopuffer-serve","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"live-agents","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false},{"name":"comment-mode","enabled":true,"variant":{"name":"disabled","enabled":false,"feature_enabled":true},"impressionData":false}],"googleFontsInlined":false},"customCss":[{"filePath":"style.css","content":":is(#sidebar-content,#mobile-nav) li[data-active=\"true\"]>a{background-color:#ffc400;color:#15171c}"}],"page":{"pagePath":"en/concepts/signing","slug":"/en/concepts/signing","pageMetadata":{"title":"Signing","description":"How webhook signatures are built and how to verify them.","href":"/en/concepts/signing"},"description":{"compiledSource":"\"use strict\";\nconst {jsx: _jsx} = arguments[0];\nconst {useMDXComponents: _provideComponents} = arguments[0];\nfunction _createMdxContent(props) {\n const _components = {\n p: \"p\",\n ..._provideComponents(),\n ...props.components\n };\n return _jsx(_components.p, {\n children: \"How webhook signatures are built and how to verify them.\"\n });\n}\nfunction MDXContent(props = {}) {\n const {wrapper: MDXLayout} = {\n ..._provideComponents(),\n ...props.components\n };\n return MDXLayout ? _jsx(MDXLayout, {\n ...props,\n children: _jsx(_createMdxContent, {\n ...props\n })\n }) : _createMdxContent(props);\n}\nreturn {\n default: MDXContent\n};\n","frontmatter":{},"scope":{}},"pageType":"default","mdx":{"mdxSource":{"compiledSource":"\"use strict\";const{Fragment:_Fragment,jsx:_jsx,jsxs:_jsxs}=arguments[0];const{useMDXComponents:_provideComponents}=arguments[0];function _createMdxContent(props){const _components={a:`a`,code:`code`,em:`em`,li:`li`,ol:`ol`,p:`p`,pre:`pre`,span:`span`,strong:`strong`,ul:`ul`,..._provideComponents(),...props.components},{CodeBlock,Heading,Warning}=_components;if(!CodeBlock)_missingMdxReference(`CodeBlock`,true);if(!Heading)_missingMdxReference(`Heading`,true);if(!Warning)_missingMdxReference(`Warning`,true);return _jsxs(_Fragment,{children:[_jsx(_components.p,{children:`Listo sends webhooks to a URL you gave it, with no credential in the request.\nThe signature is the credential. It is what tells you an event came from Listo,\narrived unaltered, and is not one somebody captured earlier and sent again.`}),`\n`,_jsx(_components.p,{children:`Every event carries one header:`}),`\n`,_jsx(CodeBlock,{filename:``,numberOfLines:`1`,language:`http`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`http`,children:_jsxs(_components.code,{language:`http`,numberOfLines:`1`,children:[_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#116329`,\"--shiki-dark\":`#569CD6`},children:`X-Listo-Signature`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`:`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` t=1753728063,v1=8f2c9d4e...`})]}),`\n`]})})}),`\n`,_jsxs(_components.ul,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`t`}),` is the Unix time, in seconds, at which the event was signed.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.code,{children:`v1`}),` is a hex HMAC-SHA256, keyed on your signing secret, over the string\n`,_jsx(_components.code,{children:`\"\u003ct>.\u003craw body>\"`}),`.`]}),`\n`]}),`\n`,_jsxs(_components.p,{children:[`Note that `,_jsx(_components.code,{children:`t`}),` is `,_jsx(_components.em,{children:`inside`}),` the signed string, not merely sitting beside it. That\nis the whole point of this scheme over a plain HMAC of the body: a plain one\nstays valid forever, so anyone who ever saw a single valid request could resend\nit whenever they liked. Binding the timestamp into the digest means a replay\nhas to either keep the original `,_jsx(_components.code,{children:`t`}),`, and fall outside the window below, or\nchange it, and invalidate the signature.`]}),`\n`,_jsx(Heading,{level:`2`,id:`verifying-one`,children:`Verifying one`}),`\n`,_jsx(CodeBlock,{filename:`verify_webhook_signature.rb`,numberOfLines:`66`,language:`ruby`,children:_jsx(_components.pre,{className:`shiki shiki-themes github-light-default dark-plus`,style:{backgroundColor:`#ffffff`,\"--shiki-dark-bg\":`#0B0C0E`,color:`#1f2328`,\"--shiki-dark\":`#D4D4D4`},language:`ruby`,children:_jsxs(_components.code,{language:`ruby`,numberOfLines:`66`,children:[_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# Verifies the signature on an incoming Listo webhook before trusting a byte of`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# it. Reads the raw request body on stdin, the way your web framework hands it`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# to you, and refuses anything that does not check out.`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`#`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# LISTO_SIGNATURE='t=...,v1=...' LISTO_WEBHOOK_SECRETS=whsec_a,whsec_b \\\\`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# ruby verify_webhook_signature.rb \u003c body.json`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`#`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`require`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"json\"`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`require`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"openssl\"`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# The window a signature stays good for, matching the one Listo signs against.`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# It is what stops a request someone captured from being replayed at you later.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4FC1FF`},children:`TOLERANCE_SECONDS`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#B5CEA8`},children:` 300`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:\"# `secrets` is a LIST because a rotation has two secrets live at once: accept\"})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# either while you swap over, then drop the old one.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`def`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:` listo_signature_valid?`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`payload`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, `}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`header`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, `}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`secrets`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, `}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#D4D4D4`},children:`now:`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4EC9B0`},children:` Time`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`now`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#9CDCFE`},children:` match`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D16969`},children:` /`}),_jsx(_components.span,{style:{color:`#116329`,\"--shiki-light-font-weight\":`bold`,\"--shiki-dark\":`#D7BA7D`,\"--shiki-dark-font-weight\":`inherit`},children:`\\\\A`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D16969`},children:`t=(`}),_jsx(_components.span,{style:{color:`#116329`,\"--shiki-light-font-weight\":`bold`,\"--shiki-dark\":`#D7BA7D`,\"--shiki-dark-font-weight\":`inherit`},children:`\\\\d`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D16969`},children:`+),v1=(`}),_jsx(_components.span,{style:{color:`#116329`,\"--shiki-light-font-weight\":`bold`,\"--shiki-dark\":`#D7BA7D`,\"--shiki-dark-font-weight\":`inherit`},children:`\\\\h`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D16969`},children:`+)`}),_jsx(_components.span,{style:{color:`#116329`,\"--shiki-light-font-weight\":`bold`,\"--shiki-dark\":`#D7BA7D`,\"--shiki-dark-font-weight\":`inherit`},children:`\\\\z`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D16969`},children:`/`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`match`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(header.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`to_s`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` return`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` false`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` if`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` match.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`nil?`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#9CDCFE`},children:` timestamp`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` match[`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#B5CEA8`},children:`1`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`].`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`to_i`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#9CDCFE`},children:` signature`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` match[`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#B5CEA8`},children:`2`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`]`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` # Checked in BOTH directions. A timestamp far in the past is the replay this`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` # scheme exists to stop; one far in the future is the same attack against a`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` # clock nobody looked at.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` return`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:` false`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` if`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` (now.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`to_i`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` -`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` timestamp).`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`abs`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` >`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4FC1FF`},children:` TOLERANCE_SECONDS`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` secrets.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`any?`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` do`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` |`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:`secret`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`|`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#9CDCFE`},children:` expected`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4EC9B0`},children:` OpenSSL`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#4EC9B0`},children:`::`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4EC9B0`},children:`HMAC`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`hexdigest`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"SHA256\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, secret, `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`#{`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`timestamp`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`}`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`.`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`#{`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`payload`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`}`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4EC9B0`},children:` OpenSSL`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`secure_compare`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(expected, signature)`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:` end`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`end`})}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# The RAW bytes, before any JSON parsing. Parsing and re-serializing changes`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# them (key order, escapes, whitespace) and the signature covers what was sent,`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# not what your JSON library would have sent.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#9CDCFE`},children:`payload`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#9CDCFE`},children:` $stdin`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`read`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#9CDCFE`},children:`header`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4EC9B0`},children:` ENV`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`fetch`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"LISTO_SIGNATURE\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, `}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#569CD6`},children:`nil`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#9CDCFE`},children:`secrets`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4EC9B0`},children:` ENV`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`fetch`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"LISTO_WEBHOOK_SECRETS\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`).`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`split`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\",\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`unless`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:` listo_signature_valid?`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(payload, header, secrets)`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` # Answer 400 and stop. Do not parse it, do not act on it, do not log it as an`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` # event you received.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#DCDCAA`},children:` abort`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"signature rejected\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`end`})}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#9CDCFE`},children:`event`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#4EC9B0`},children:` JSON`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`parse`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(payload)`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:\"# Events arrive at least once and in no guaranteed order, so treat `id` as the\"})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# thing you deduplicate on and the object's own timestamps as the thing you`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:`# order by. Answer 2xx quickly and do the work afterwards.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#DCDCAA`},children:`puts`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`#{`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`event[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"type\"`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`]`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`}`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:` #{`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`event[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"id\"`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`]`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`}`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"`})]}),`\n`,_jsx(_components.span,{className:`line`}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`case`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` event[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"type\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`]`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`when`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"delivery.status_changed\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"delivery.updated\"`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#9CDCFE`},children:` delivery`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` event[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"data\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`]`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#DCDCAA`},children:` puts`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"order `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`#{`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`delivery[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"external_id\"`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`]`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`}`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` is `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`#{`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`delivery[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"status\"`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`]`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`}`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`when`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"delivery.courier_location\"`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#953800`,\"--shiki-dark\":`#9CDCFE`},children:` location`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#D4D4D4`},children:` =`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:` event.`}),_jsx(_components.span,{style:{color:`#8250DF`,\"--shiki-dark\":`#DCDCAA`},children:`dig`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`(`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"data\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`, `}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"location\"`}),_jsx(_components.span,{style:{color:`#1F2328`,\"--shiki-dark\":`#D4D4D4`},children:`)`})]}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#DCDCAA`},children:` puts`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"courier at `}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`#{`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`location[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"lat\"`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`]`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`}`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`,`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`#{`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`location[`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"lng\"`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#D4D4D4`},children:`]`}),_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#569CD6`},children:`}`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:`\"`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`else`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` # A new type is an additive change, so ignore what you do not recognise`})}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#6E7781`,\"--shiki-dark\":`#6A9955`},children:` # rather than failing on it.`})}),`\n`,_jsxs(_components.span,{className:`line`,children:[_jsx(_components.span,{style:{color:`#0550AE`,\"--shiki-dark\":`#DCDCAA`},children:` puts`}),_jsx(_components.span,{style:{color:`#0A3069`,\"--shiki-dark\":`#CE9178`},children:` \"ignored\"`})]}),`\n`,_jsx(_components.span,{className:`line`,children:_jsx(_components.span,{style:{color:`#CF222E`,\"--shiki-dark\":`#C586C0`},children:`end`})}),`\n`]})})}),`\n`,_jsx(_components.p,{children:`Four rules, and each of them is a way the check can be wrong while still looking\nright.`}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Hash the raw bytes.`}),` Sign what arrived on the wire, before any JSON parsing.\nParsing and re-serialising changes the bytes, because your JSON library has its\nown ideas about key order, escape sequences and whitespace, and the signature\ncovers what was sent rather than what your library would have sent. Most web\nframeworks give you the raw body if you ask; find out how yours does before\nwriting the check.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Check the timestamp in both directions.`}),` Reject anything more than five\nminutes old, which is the replay this scheme exists to stop, `,_jsx(_components.em,{children:`and`}),` anything more\nthan five minutes in the future. Skipping the second half hands an attacker an\narbitrarily long window by simply choosing a large `,_jsx(_components.code,{children:`t`}),`.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Compare in constant time.`}),` Use your language’s constant-time comparison, not\n`,_jsx(_components.code,{children:`==`}),`. A comparison that returns early on the first differing byte leaks how much\nof a guess was right, which is enough to walk out a valid signature one byte at\na time.`]}),`\n`,_jsxs(_components.p,{children:[_jsx(_components.strong,{children:`Fail closed.`}),` A malformed header, a missing header, an unparseable timestamp\nand a signature that does not match are all the same answer: not authentic.\nReject with a `,_jsx(_components.code,{children:`400`}),`, and do not parse or act on the body. Never let the verifier\nraise; an exception in a verifier is a `,_jsx(_components.code,{children:`500`}),` where a refusal belonged.`]}),`\n`,_jsx(Heading,{level:`2`,id:`rotating-a-secret`,children:`Rotating a secret`}),`\n`,_jsx(_components.p,{children:`You can have two secrets live at once, which is what makes a rotation possible\nwithout dropping events. Listo signs with the primary one; you accept either.`}),`\n`,_jsxs(_components.ol,{children:[`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Add a second secret`}),` in the `,_jsx(_components.a,{href:`https://partners.listo.delivery`,children:`partner dashboard`}),`.\nNothing changes yet: Listo keeps signing with the first.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Teach your verifier both.`}),` This is why the check above takes a `,_jsx(_components.em,{children:`list`}),`:\naccept an event that validates under either secret.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Promote the new secret.`}),` Listo starts signing with it. Your endpoint keeps\nworking through the switch, because it already accepts both.`]}),`\n`,_jsxs(_components.li,{children:[_jsx(_components.strong,{children:`Drop the old one`}),`, in the dashboard and in your own configuration.`]}),`\n`]}),`\n`,_jsx(_components.p,{children:`Do them in that order. Promote before step 2 and every event in flight is\nrejected by an endpoint that has never seen the new secret.`}),`\n`,_jsxs(Warning,{children:[_jsx(_components.p,{children:_jsx(_components.strong,{children:`A secret is a secret`})}),_jsx(_components.p,{children:`Store signing secrets the way you store passwords: server side, out of your\nrepository, out of anything a browser can reach. Anyone holding one can forge an\nevent your verifier will accept, which is the same as being able to tell your\nsystem that an order was delivered.`})]}),`\n`,_jsx(Heading,{level:`2`,id:`modes-have-their-own-secrets`,children:`Modes have their own secrets`}),`\n`,_jsx(_components.p,{children:`Live and test each have their own endpoint URL and their own signing secret. A\nsecret from one mode never validates an event from the other, which is what\nstops a staging deploy from being able to accept production traffic. Rotate them\nindependently.`}),`\n`,_jsxs(_components.p,{children:[`Putting all of this to work, including responding, deduplicating and retries, is\n`,_jsx(_components.a,{href:`/en/guides/webhooks`,children:`receive and verify webhooks`}),`.`]})]})}function MDXContent(props={}){const{wrapper:MDXLayout}={..._provideComponents(),...props.components};return MDXLayout?_jsx(MDXLayout,{...props,children:_jsx(_createMdxContent,{...props})}):_createMdxContent(props)}return{default:MDXContent};function _missingMdxReference(id,component){throw new Error(`Expected `+(component?`component`:`object`)+\" `\"+id+\"` to be defined: you likely forgot to import, pass, or provide it.\")}","frontmatter":{},"scope":{"config":{},"pageMetadata":{"title":"Signing","description":"How webhook signatures are built and how to verify them.","href":"/en/concepts/signing"}}},"mdxSourceWithNoJs":null},"mdxExtracts":{"tableOfContents":[{"title":"Verifying one","slug":"verifying-one","depth":2,"children":[]},{"title":"Rotating a secret","slug":"rotating-a-secret","depth":2,"children":[]},{"title":"Modes have their own secrets","slug":"modes-have-their-own-secrets","depth":2,"children":[]}],"codeExamples":{}},"jsLinks":[],"subdomain":"listo-c5a11a00","siteName":"Listo Partner API","title":"Signing - Listo Partner API","navMaps":{"firstHrefInVersion":{},"firstHrefInLanguage":{"en":{"href":"/en/concepts/signing","matchLevel":0},"es":{"href":"/es/concepts/signing","matchLevel":1}}},"lastModified":"2026-11-07T01:49:05.180Z","originalFileLocation":"en/concepts/signing.mdx","locale":"en"},"headHtml":"\u003cscript data-mint-head>(function(){var d=document.documentElement,p=null;if(!p){try{p=localStorage.getItem(\"isDarkMode\")}catch(e){}if(p==='true')p='dark';else if(p==='false')p='light';if(p!=='light'\u0026\u0026p!=='dark'\u0026\u0026p!=='system')p=\"system\"}var r=p==='system'?(matchMedia(\"(prefers-color-scheme: dark)\").matches?'dark':'light'):p;d.classList.remove(r==='dark'?'light':'dark');d.classList.add(r);d.setAttribute(\"data-theme-preference\",p)})();\u003c/script>\u003cmeta data-mint-head name=\"description\" content=\"How webhook signatures are built and how to verify them.\">\u003cmeta data-mint-head name=\"application-name\" content=\"Listo Partner API\">\u003cmeta data-mint-head name=\"generator\" content=\"Mintlify\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/listo-c5a11a00/GMBEN5uWEX-r6cRJ/_generated/favicon/android-chrome-192x192.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"apple-touch-icon\" type=\"image/png\" sizes=\"180x180\" href=\"/mintlify-assets/_mintlify/favicons/listo-c5a11a00/GMBEN5uWEX-r6cRJ/_generated/favicon/apple-touch-icon.png\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/listo-c5a11a00/GMBEN5uWEX-r6cRJ/_generated/favicon/favicon-16x16.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/listo-c5a11a00/GMBEN5uWEX-r6cRJ/_generated/favicon/favicon-32x32.png\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/listo-c5a11a00/GMBEN5uWEX-r6cRJ/_generated/favicon/favicon.ico\" media=\"(prefers-color-scheme: light)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"192x192\" href=\"/mintlify-assets/_mintlify/favicons/listo-c5a11a00/GMBEN5uWEX-r6cRJ/_generated/favicon-dark/android-chrome-192x192.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"16x16\" href=\"/mintlify-assets/_mintlify/favicons/listo-c5a11a00/GMBEN5uWEX-r6cRJ/_generated/favicon-dark/favicon-16x16.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"icon\" type=\"image/png\" sizes=\"32x32\" href=\"/mintlify-assets/_mintlify/favicons/listo-c5a11a00/GMBEN5uWEX-r6cRJ/_generated/favicon-dark/favicon-32x32.png\" media=\"(prefers-color-scheme: dark)\">\u003clink data-mint-head rel=\"shortcut icon\" type=\"image/x-icon\" href=\"/mintlify-assets/_mintlify/favicons/listo-c5a11a00/GMBEN5uWEX-r6cRJ/_generated/favicon-dark/favicon.ico\" media=\"(prefers-color-scheme: dark)\">\u003cmeta data-mint-head property=\"og:title\" content=\"Signing - Listo Partner API\">\u003cmeta data-mint-head property=\"og:description\" content=\"How webhook signatures are built and how to verify them.\">\u003cmeta data-mint-head property=\"og:site_name\" content=\"Listo Partner API\">\u003cmeta data-mint-head property=\"og:url\" content=\"https://docs.listo.delivery/en/concepts/signing\">\u003cmeta data-mint-head property=\"og:type\" content=\"website\">\u003cmeta data-mint-head property=\"og:image\" content=\"https://listo-c5a11a00.mintlify.site/_mintlify/api/og/v2?title=Signing\u0026amp;description=How+webhook+signatures+are+built+and+how+to+verify+them.\u0026amp;theme=77fcfc9066d8b9eb9850b729\u0026amp;signature=5dcc7d06c5ae9a4399d47f512a1a23ba37d1fc89c9b768e4c0883414c3f151b6\">\u003cmeta data-mint-head property=\"og:image:width\" content=\"1200\">\u003cmeta data-mint-head property=\"og:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"twitter:card\" content=\"summary_large_image\">\u003cmeta data-mint-head name=\"twitter:title\" content=\"Signing - Listo Partner API\">\u003cmeta data-mint-head name=\"twitter:description\" content=\"How webhook signatures are built and how to verify them.\">\u003cmeta data-mint-head name=\"twitter:image\" content=\"https://listo-c5a11a00.mintlify.site/_mintlify/api/og/v2?title=Signing\u0026amp;description=How+webhook+signatures+are+built+and+how+to+verify+them.\u0026amp;theme=77fcfc9066d8b9eb9850b729\u0026amp;signature=5dcc7d06c5ae9a4399d47f512a1a23ba37d1fc89c9b768e4c0883414c3f151b6\">\u003cmeta data-mint-head name=\"twitter:image:width\" content=\"1200\">\u003cmeta data-mint-head name=\"twitter:image:height\" content=\"630\">\u003cmeta data-mint-head name=\"msapplication-config\" content=\"/mintlify-assets/_mintlify/favicons/listo-c5a11a00/GMBEN5uWEX-r6cRJ/_generated/favicon/browserconfig.xml\">\u003cmeta data-mint-head name=\"apple-mobile-web-app-title\" content=\"Listo Partner API\">\u003cmeta data-mint-head name=\"msapplication-TileColor\" content=\"#15171C\">\u003clink data-mint-head rel=\"canonical\" href=\"https://docs.listo.delivery/en/concepts/signing\">\u003clink data-mint-head rel=\"alternate\" type=\"application/xml\" href=\"/sitemap.xml\">\u003clink data-mint-head rel=\"alternate\" type=\"text/markdown\" href=\"/en/concepts/signing.md\">\u003clink data-mint-head rel=\"alternate\" hreflang=\"en\" href=\"https://docs.listo.delivery/en/concepts/signing\">\u003clink data-mint-head rel=\"alternate\" hreflang=\"es\" href=\"https://docs.listo.delivery/es/concepts/signing\">\u003clink data-mint-head rel=\"alternate\" hreflang=\"x-default\" href=\"https://docs.listo.delivery/en/concepts/signing\">\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@graph\":[{\"@type\":\"Organization\",\"@id\":\"https://docs.listo.delivery/#organization\",\"name\":\"Listo Partner API\",\"url\":\"https://docs.listo.delivery\",\"logo\":{\"@type\":\"ImageObject\",\"url\":\"https://mintcdn.com/listo-c5a11a00/mreCwXzcEA2bSZog/logo/light.svg?fit=max\u0026auto=format\u0026n=mreCwXzcEA2bSZog\u0026q=85\u0026s=da2b28d464dc6987329dfd5971085dd7\"}},{\"@type\":\"WebSite\",\"@id\":\"https://docs.listo.delivery#website\",\"name\":\"Listo Partner API\",\"url\":\"https://docs.listo.delivery\",\"publisher\":{\"@id\":\"https://docs.listo.delivery/#organization\"}},{\"@type\":\"WebPage\",\"@id\":\"https://docs.listo.delivery/en/concepts/signing#webpage\",\"url\":\"https://docs.listo.delivery/en/concepts/signing\",\"name\":\"Signing\",\"description\":\"How webhook signatures are built and how to verify them.\",\"dateModified\":\"2026-11-07T01:49:05.180Z\",\"isPartOf\":{\"@id\":\"https://docs.listo.delivery#website\"},\"breadcrumb\":{\"@id\":\"https://docs.listo.delivery/en/concepts/signing#breadcrumb\"}},{\"@type\":\"BreadcrumbList\",\"@id\":\"https://docs.listo.delivery/en/concepts/signing#breadcrumb\",\"itemListElement\":[{\"@type\":\"ListItem\",\"position\":1,\"name\":\"Concepts\",\"item\":\"https://docs.listo.delivery/en/concepts/money\"},{\"@type\":\"ListItem\",\"position\":2,\"name\":\"Signing\",\"item\":\"https://docs.listo.delivery/en/concepts/signing\"}]},{\"@type\":[\"Article\",\"TechArticle\"],\"@id\":\"https://docs.listo.delivery/en/concepts/signing#article\",\"headline\":\"Signing\",\"name\":\"Signing\",\"description\":\"How webhook signatures are built and how to verify them.\",\"url\":\"https://docs.listo.delivery/en/concepts/signing\",\"mainEntityOfPage\":{\"@id\":\"https://docs.listo.delivery/en/concepts/signing#webpage\"},\"dateModified\":\"2026-11-07T01:49:05.180Z\",\"publisher\":{\"@id\":\"https://docs.listo.delivery/#organization\"},\"isPartOf\":{\"@id\":\"https://docs.listo.delivery#website\"}}]}\u003c/script>\u003cscript data-mint-head type=\"application/ld+json\">{\"@context\":\"https://schema.org\",\"@type\":\"WebSite\",\"name\":\"Listo Partner API\",\"creator\":{\"@type\":\"Organization\",\"name\":\"Mintlify\",\"url\":\"https://mintlify.com\"}}\u003c/script>","siteName":"Listo Partner API","nav":{"global":null,"languages":[{"language":"en","default":true,"navbar":{"links":[{"href":"https://status.listo.delivery","label":"Status"}]},"groups":[{"group":"Get started","pages":[{"title":"Quickstart","href":"/en/quickstart"}]},{"group":"Guides","pages":[{"title":"Quote and create a delivery","href":"/en/guides/quote-and-create"},{"title":"Receive and verify webhooks","href":"/en/guides/webhooks"},{"title":"Debugging your integration","href":"/en/guides/debugging"},{"title":"Build with AI","href":"/en/guides/build-with-ai"},{"title":"Fund your balance and withdraw","href":"/en/guides/balance-and-withdrawals"},{"title":"Go live","href":"/en/guides/go-live"},{"title":"Connect a restaurant that already uses Listo","sidebarTitle":"Connect an existing account","href":"/en/guides/connect-existing-account"},{"title":"Plan and billing","href":"/en/guides/plan-and-billing"},{"title":"Connect Claude, Cursor or Codex","href":"/en/guides/connect-ai-client"}]},{"group":"Errors","expanded":false,"pages":[{"title":"Errors","href":"/en/errors/index"},{"title":"account_exists","href":"/en/errors/account_exists"},{"title":"address_not_found","href":"/en/errors/address_not_found"},{"title":"address_undeliverable","href":"/en/errors/address_undeliverable"},{"title":"cancel_failed","href":"/en/errors/cancel_failed"},{"title":"card_below_minimum","href":"/en/errors/card_below_minimum"},{"title":"card_not_ready","href":"/en/errors/card_not_ready"},{"title":"cash_over_cap","href":"/en/errors/cash_over_cap"},{"title":"charge_in_progress","href":"/en/errors/charge_in_progress"},{"title":"code_expired","href":"/en/errors/code_expired"},{"title":"console_access_required","href":"/en/errors/console_access_required"},{"title":"event_not_resendable","href":"/en/errors/event_not_resendable"},{"title":"idempotency_conflict","href":"/en/errors/idempotency_conflict"},{"title":"insufficient_balance","href":"/en/errors/insufficient_balance"},{"title":"internal_error","href":"/en/errors/internal_error"},{"title":"invalid_api_key","href":"/en/errors/invalid_api_key"},{"title":"no_couriers_available","href":"/en/errors/no_couriers_available"},{"title":"not_cancellable","href":"/en/errors/not_cancellable"},{"title":"not_editable","href":"/en/errors/not_editable"},{"title":"not_found","href":"/en/errors/not_found"},{"title":"not_reactivatable","href":"/en/errors/not_reactivatable"},{"title":"not_refundable","href":"/en/errors/not_refundable"},{"title":"not_reschedulable","href":"/en/errors/not_reschedulable"},{"title":"not_retriable","href":"/en/errors/not_retriable"},{"title":"order_too_large","href":"/en/errors/order_too_large"},{"title":"out_of_service_area","href":"/en/errors/out_of_service_area"},{"title":"payout_account_not_ready","href":"/en/errors/payout_account_not_ready"},{"title":"pickup_point_pending","href":"/en/errors/pickup_point_pending"},{"title":"plan_ended","href":"/en/errors/plan_ended"},{"title":"plan_required","href":"/en/errors/plan_required"},{"title":"provider_unavailable","href":"/en/errors/provider_unavailable"},{"title":"quote_consumed","href":"/en/errors/quote_consumed"},{"title":"quote_expired","href":"/en/errors/quote_expired"},{"title":"rate_limited","href":"/en/errors/rate_limited"},{"title":"read_only_key","href":"/en/errors/read_only_key"},{"title":"refund_amount_too_large","href":"/en/errors/refund_amount_too_large"},{"title":"resend_limit_reached","href":"/en/errors/resend_limit_reached"},{"title":"same_day_below_minimum","href":"/en/errors/same_day_below_minimum"},{"title":"same_day_cap_exceeded","href":"/en/errors/same_day_cap_exceeded"},{"title":"test_webhook_limit_reached","href":"/en/errors/test_webhook_limit_reached"},{"title":"unsupported_country","href":"/en/errors/unsupported_country"},{"title":"validation_failed","href":"/en/errors/validation_failed"},{"title":"webhook_endpoint_not_configured","href":"/en/errors/webhook_endpoint_not_configured"}]},{"group":"Reference","pages":[{"group":"Balance","pages":[{"title":"Read your prepaid balance","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/balance","href":"/api-reference/balance/read-your-prepaid-balance"},{"title":"Read a store's balance","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/stores/{store_id}/balance","href":"/api-reference/balance/read-a-stores-balance"}]},{"group":"Account connection","pages":[{"title":"Ask a restaurant to connect its account","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/connect_requests","href":"/api-reference/account-connection/ask-a-restaurant-to-connect-its-account"},{"title":"Exchange an approval for the store","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/connections","href":"/api-reference/account-connection/exchange-an-approval-for-the-store"}]},{"group":"Deliveries","pages":[{"title":"List deliveries","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/deliveries","href":"/api-reference/deliveries/list-deliveries"},{"title":"Create a delivery","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/deliveries","href":"/api-reference/deliveries/create-a-delivery"},{"title":"Cancel a delivery","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/deliveries/{delivery_id}/cancellations","href":"/api-reference/deliveries/cancel-a-delivery"},{"title":"Refund a delivery","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/deliveries/{delivery_id}/refunds","href":"/api-reference/deliveries/refund-a-delivery"},{"title":"Retry a failed dispatch","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/deliveries/{delivery_id}/retries","href":"/api-reference/deliveries/retry-a-failed-dispatch"},{"title":"Fetch a delivery","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/deliveries/{id}","href":"/api-reference/deliveries/fetch-a-delivery"},{"title":"Update a delivery","deprecated":null,"version":null,"openapi":"/openapi/en.yaml patch /v1/deliveries/{id}","href":"/api-reference/deliveries/update-a-delivery"}]},{"group":"Events","pages":[{"title":"List your webhook events","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/events","href":"/api-reference/events/list-your-webhook-events"},{"title":"Send a webhook event again","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/events/{event_id}/resend","href":"/api-reference/events/send-a-webhook-event-again"},{"title":"Retrieve a webhook event","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/events/{id}","href":"/api-reference/events/retrieve-a-webhook-event"},{"title":"Send a test webhook","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/test_webhooks","href":"/api-reference/events/send-a-test-webhook"}]},{"group":"Ledger","pages":[{"title":"List your ledger entries","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/ledger","href":"/api-reference/ledger/list-your-ledger-entries"},{"title":"List a store's ledger entries","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/stores/{store_id}/ledger","href":"/api-reference/ledger/list-a-stores-ledger-entries"}]},{"group":"Authentication","pages":[{"title":"Check that your API key works","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/ping","href":"/api-reference/authentication/check-that-your-api-key-works"}]},{"group":"Quotes","pages":[{"title":"Price a delivery","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/quotes","href":"/api-reference/quotes/price-a-delivery"}]},{"group":"Request logs","pages":[{"title":"List your request log","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/request_logs","href":"/api-reference/request-logs/list-your-request-log"},{"title":"Retrieve a request log entry","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/request_logs/{id}","href":"/api-reference/request-logs/retrieve-a-request-log-entry"}]},{"group":"Stores","pages":[{"title":"List your stores","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/stores","href":"/api-reference/stores/list-your-stores"},{"title":"Create a store","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/stores","href":"/api-reference/stores/create-a-store"},{"title":"Fetch a store","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/stores/{id}","href":"/api-reference/stores/fetch-a-store"},{"title":"Update a store","deprecated":null,"version":null,"openapi":"/openapi/en.yaml patch /v1/stores/{id}","href":"/api-reference/stores/update-a-store"},{"title":"Invite a store owner","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/stores/{store_id}/owner","href":"/api-reference/stores/invite-a-store-owner"},{"title":"Reactivate a store's plan","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/stores/{store_id}/plan","href":"/api-reference/stores/reactivate-a-stores-plan"},{"title":"Cancel a store's plan","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/stores/{store_id}/plan_cancellation","href":"/api-reference/stores/cancel-a-stores-plan"},{"title":"Undo a store's plan cancellation","deprecated":null,"version":null,"openapi":"/openapi/en.yaml delete /v1/stores/{store_id}/plan_cancellation","href":"/api-reference/stores/undo-a-stores-plan-cancellation"}]},{"group":"Card onboarding","pages":[{"title":"Start card onboarding for a store","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/stores/{store_id}/stripe_onboarding","href":"/api-reference/card-onboarding/start-card-onboarding-for-a-store"},{"title":"Read a store's card readiness","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/stores/{store_id}/stripe_status","href":"/api-reference/card-onboarding/read-a-stores-card-readiness"},{"title":"Start card onboarding for your company","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/stripe_onboarding","href":"/api-reference/card-onboarding/start-card-onboarding-for-your-company"},{"title":"Read your company's card readiness","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/stripe_status","href":"/api-reference/card-onboarding/read-your-companys-card-readiness"}]},{"group":"Webhook deliveries","pages":[{"title":"List webhook delivery attempts","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/webhook_deliveries","href":"/api-reference/webhook-deliveries/list-webhook-delivery-attempts"}]},{"group":"Withdrawals","pages":[{"title":"List your withdrawals","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/withdrawals","href":"/api-reference/withdrawals/list-your-withdrawals"},{"title":"Request a withdrawal","deprecated":null,"version":null,"openapi":"/openapi/en.yaml post /v1/withdrawals","href":"/api-reference/withdrawals/request-a-withdrawal"},{"title":"Fetch a withdrawal","deprecated":null,"version":null,"openapi":"/openapi/en.yaml get /v1/withdrawals/{id}","href":"/api-reference/withdrawals/fetch-a-withdrawal"}]},{"group":"Webhooks","pages":[{"title":"A delivery reached a new status","version":null,"deprecated":null,"openapi":"/openapi/en.yaml webhook delivery.status_changed","href":"/api-reference/webhooks/a-delivery-reached-a-new-status"},{"title":"A delivery changed without changing status","version":null,"deprecated":null,"openapi":"/openapi/en.yaml webhook delivery.updated","href":"/api-reference/webhooks/a-delivery-changed-without-changing-status"},{"title":"The courier moved","version":null,"deprecated":null,"openapi":"/openapi/en.yaml webhook delivery.courier_location","href":"/api-reference/webhooks/the-courier-moved"},{"title":"A withdrawal was settled","version":null,"deprecated":null,"openapi":"/openapi/en.yaml webhook withdrawal.updated","href":"/api-reference/webhooks/a-withdrawal-was-settled"},{"title":"A store changed","version":null,"deprecated":null,"openapi":"/openapi/en.yaml webhook store.updated","href":"/api-reference/webhooks/a-store-changed"},{"title":"A restaurant connected its existing account to you","version":null,"deprecated":null,"openapi":"/openapi/en.yaml webhook store.connected","href":"/api-reference/webhooks/a-restaurant-connected-its-existing-account-to-you"}]}]},{"group":"Concepts","pages":[{"title":"The money model","href":"/en/concepts/money"},{"title":"Where a delivery came from","href":"/en/concepts/delivery-origin"},{"title":"Test mode","description":"What a test key does differently, and what it does not.","href":"/en/concepts/test-mode"},{"title":"Signing","href":"/en/concepts/signing"},{"title":"Cash at the door","description":"Collecting cash from the customer on delivery, and what it settles to.","href":"/en/concepts/cash-at-the-door"}]},{"group":"Changelog","pages":[{"title":"Changelog","href":"/en/changelog"}]},{"group":"Legal","pages":[{"title":"Terms for Partners","url":"https://listo.delivery/terminos-partners","href":"/en/terms-for-partners"}]}]},{"language":"es","navbar":{"links":[{"href":"https://status.listo.delivery","label":"Estado del servicio"}]},"pages":[{"title":"es","href":"/es/concepts/signing"}]}]},"basePath":"","title":"Signing - Listo Partner API","buildId":"dpl_8LJbrAiZvr1K98WQNZXCyY5AvT34","deploymentId":"dpl_8LJbrAiZvr1K98WQNZXCyY5AvT34","dynamics":[],"chunks":[]}