Configure webhooks

PUT/api/v1/webhook
Required scope:orders:write

Set, change, or disable the URL we notify when an order reaches a final state · This page covers registering the endpoint only; what the call we send you looks like, how to verify its signature, and how retries work are on the callback page (/developers/webhooks/callback) · An endpoint belongs to one API key, not to your account, so if you use several keys you register one endpoint per key

The signing key is shown once, and rotates on every successful registration

secret is shown exactly once, in the answer to this request. No endpoint can read it back. Store it in your secret store the moment you receive it; if you lose it, the only way forward is to register again and receive a new one · Every successful registration rotates the key, even if you send the very same URL. The previous key stops working at that instant — so do not replay this request to confirm your settings, or your verifier will start rejecting every payload while it still holds the old key · Disabling the webhook clears the key at the same moment, and enabling it again always issues a new one.

Register your endpoint before you start ordering

If you register a URL after placing orders, you will not be told about the ones already in flight — events queued while no endpoint was configured are dropped on the next round and are never replayed once you do register, and there is no way to ask for them again · Disabling the webhook while orders are still open has the same effect · Register your endpoint before you start ordering, and if you only registered later, sweep your own unfinished orders once.

The authentication refusals reach this endpoint too

The five refusals at the authentication gate — invalid_key (401) · key_revoked (401) · insufficient_scope (403) · quota_exceeded (429) · too_many_inflight (429) — can happen on every endpoint, this one included, because they are thrown before the request reaches a single line of this endpoint's own logic (the scope it needs is the one declared at the top of this page). The response tabs above therefore list only what this endpoint itself answers; that is not a claim that those five cannot happen here. All of the detail lives in one place, on the identity endpoint page: the HTTP status of each one, which of them spend your daily quota, which headers come back, and why the two 429s need opposite handling. It is deliberately not copied onto every page, because the second copy is the one nobody remembers to update the day the rules change.

Request parameters

Request body

NameTypeHow to use it
urlRequiredstring or null
The URL we should call, or null to disable the webhook
Details and caveats
  • ⚠️ **The url key must always be present in the body
  • leaving it out is a malformed request, not a command to disable** (see the warning about disabling below)
  • What the URL must satisfy: https only
  • port 443 only (omit it or write :443)
  • the host must be a domain name, never an IP address in any notation
  • that domain must resolve to public addresses only (private or reserved ranges are refused)
  • it cannot point back at our own domains
  • at most 2048 characters
  • paths and query strings are fine
  • We do not follow redirects, so a 3xx answer counts as a failed delivery
  • give us the real URL
  • ⚠️ These rules are re-checked immediately before every delivery, not only when you register: an endpoint that qualifies today but whose domain later resolves to a forbidden address has that event dropped with no retry
  • If webhooks suddenly go quiet after you move hosting providers, check first that the domain still resolves to a public address.

Header

NameTypeHow to use it
AuthorizationRequiredstring
Your key as Bearer <key> — required on every request · The endpoint you register belongs to this key alone.
Content-TypeRequiredstring
Must be application/json because this request carries a body
Details and caveats
  • declare any other type and the body is read as plain text or as a form rather than JSON, and the request fails with 400 bad_request even though the JSON itself is valid to the letter
  • Watch out for curl -d, which declares a form content type of its own accord unless you tell it otherwise.

Response fields

NameTypeHow to use it
urlstring or null
The URL we actually stored and will call
Details and caveats
  • It can differ slightly from what you typed, because we normalise it first (an uppercase host is lowercased, an empty path gets a /)
  • treat the value returned here as the real one
  • It is null in the answer to a disable command.
secretstring
The secret you verify the signature of our calls with (how to use it is on the callback page)
Details and caveats
  • ⚠️ **It is shown once, here, and can never be read back
  • store it in your secret store immediately**
  • Present only in the answer to registering or changing an endpoint; the answer to a disable command does not carry it.
rotated_atISO 8601 string (UTC)
When this signing key was issued · Present only in the answer to registering or changing an endpoint, for the same reason.
data.reasonstring
The only field that tells you which check the destination failed
Details and caveats
  • all seven ways a URL can be rejected share one status (400) and one code (webhook_url_rejected); this value is the only thing that differs
  • The possible values: malformed not readable as a URL, or a value that is not a string
  • not_https does not start with https://
  • bad_port a port other than 443
  • ip_literal the host is an IP address rather than a domain name
  • own_domain points back at the shop's own domain, or sits under vercel.app / localhost (see this code's tab
  • all three share it)
  • private_host the name resolves to a private or reserved address
  • unresolvable the name cannot be resolved, or resolved to something we cannot read
  • Branch on this value and never match against the message text
  • wording changes while this value stays put
  • As with every vocabulary in this API, values you do not recognise are always possible in future: treat one the way you treat malformed, as a URL that cannot be used, and put it in front of a human
  • **This is the one row in this table that describes a field of a *failed* response rather than a successful one.** It lives here because it is the only field in the whole API that the standard failure envelope does not carry
  • undocumented here, it is documented nowhere.

Possible statuses

StatusCodeMeaning
200
Endpoint registered or changed — store the secret now
Those three keys are the whole response
Details and caveats
  • there is nothing else in it
  • The secret is shown once, here, and can never be read back, so store it immediately
  • ⚠️ We do not send a test call to your URL when you register. This request only inspects the URL and the addresses its domain resolves to, so a mistyped URL that still satisfies every rule will only reveal itself when the first real webhook fails to arrive.
200
Webhook disabled — no more notifications
Disable it by sending url as null
Details and caveats
  • That one key is the whole response
  • The signing secret is cleared at the same moment and is not kept around, which is why there is no secret or rotated_at here
  • Enabling it again always issues a new secret, and the old one stops working.
400bad_request
Body is not a JSON object
An array, a bare string, a number or an empty body · Every check on this page runs in the order shown, because the first check that fails is the one you get back.
400bad_request
No url key in the body
Sent {}, {"webhook_url": "https://hooks.example.com/naxset"}, or {"URL": "https://hooks.example.com/naxset"}
Details and caveats
  • the key must be url in lower case exactly; a capitalised one does not count as sent
  • This is deliberately not treated as a request to disable.
400bad_request
url longer than 2048 characters
Checked before the URL rules, because it is a complaint about the shape of your request rather than about the endpoint being unsafe
Details and caveats
  • which is why the code is bad_request and not webhook_url_rejected.
400webhook_url_rejected
malformed — not a URL at all
Sent {"url": "not a url"}, {"url": ""}, or a value that is not a string at all such as {"url": 123} (objects and true included)
Details and caveats
  • Note that a non-string value lands here rather than in the length check above.
400webhook_url_rejected
not_https — wrong scheme
Sent http://hooks.example.com/naxset · Anything that does not start with https:// is refused, with no exception for internal networks or for testing.
400webhook_url_rejected
bad_port — a port other than 443
Sent https://hooks.example.com:8443/naxset · Writing :443 explicitly is fine; anything else is not.
400webhook_url_rejected
ip_literal — the host is an address, not a name
Sent https://203.0.113.10/hook
Details and caveats
  • Every notation counts, including decimal, hexadecimal, octal, shortened IPv4 and bracketed IPv6
  • we look at whether you wrote an address, not at which address it is.
400webhook_url_rejected
own_domain — pointing back at the shop, or at a domain the shop itself runs on
This check covers three domains, not just the shop's own: naxset.com, any host under vercel.app, and localhost
Details and caveats
  • The match is made label by label: the hostname must equal one of them exactly, or end in a dot followed by it (hook.example.vercel.app is refused, while myvercel.app is not, being a different domain)
  • ⚠️ vercel.app is refused as a whole because the shop's own system is served on that domain too, not because we have anything against the provider
  • a receiver living on a vercel.app subdomain (which is very common) has to be given your own domain first, and registered under that name instead
  • A trailing dot on the hostname does not get around this check.
400webhook_url_rejected
private_host — the name resolves somewhere private
The hostname is a real name, but at least one of the addresses it resolves to is in a private, loopback, link-local or otherwise reserved range
Details and caveats
  • One bad address among several good ones is enough to be refused.
400webhook_url_rejected
unresolvable — we could not get a usable answer
The name does not resolve, the lookup failed, or an address came back that we could not read
Details and caveats
  • This one can be transient: a domain whose DNS is temporarily broken gives the same answer as one that does not exist
  • A refused request changes nothing. Whatever endpoint and key you had registered before are still in place and still being used
  • a failed PUT never disables anything and never rotates the signing key.

Once an endpoint is registered it starts receiving calls from us as soon as an order closes. What those calls look like, how to verify their signature, and what your endpoint has to answer are all on The callback we send you.

Last updated 2026-09-07