Change the endpoint URL, the subscribed events, the test/live scope, or pause it with active: false. Send at least one field.
- Credential
- An API key in
x-api-key - Scopes
webhooks:manage
Parameters
idpath · stringrequiredIdempotency-Keyheader · string (uuid)Unique key for idempotent requests. If you retry a request with the same key within 24 hours, you receive the original response. The key is bound to the request payload: reusing it for a different payload returns 422
idempotency_key_reuserather than the original response, so a changed request is never silently dropped. The binding is over the JSON's content, not its bytes — keys in another order or other whitespace are the same request. Generate a new key for each new request. On an operation that moves money (an order, a batch, an item, a shipping or address change, a cancellation's refund), a key that cannot be checked because the idempotency ledger is unreachable is answered 503idempotency_unavailablewithRetry-After, and nothing runs or is charged: retry with the same key.
Request body application/json, required
urlstring (uri)New destination URL. It must use HTTPS, or http:// to localhost or 127.0.0.1 outside production — the hosted API is production, so it accepts HTTPS only.
eventsstring[]On this portal a subscription names
order.created,order.paid,order.in_production,order.shipped,order.delivered,order.cancelled,order.failed,order.updated,order.quality_check,order.issue,order.approval_requested,order.approval_approved,order.approval_declined,order.approval_expired,product.updated,catalog.synced,webhook.disabled,*; any other event is refused with 400 event_not_available. An event this subscription already names is kept, whatever its family.at least 1 entry
activebooleanfalse pauses delivery: events already queued for this endpoint are held, not sent, and go out where their retry schedule left off once it is true again. Events raised while it is paused are not queued for it; replay them with POST /v1/events/{eventId}/replay after resuming.
modestringWhich traffic this endpoint receives. "test" delivers only sandbox events, "live" only real ones, "all" (the default) both. Account-level events — catalog syncs, delivery-failure notices, design proofs — arrive on every subscription regardless. Every mode-bearing event carries
test_modein its payload, stamped from the same decision that routed it.One of
alltestliveDefault all
Request
PATCH /api/v1/webhooks/{id}
curl -X PATCH "https://portal.printedsimply.com/api/v1/webhooks/whk_SV8d5wGm2kUnSwx_RhZfY9OE" \
-H "x-api-key: ps_test_YOUR_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: update-webhook-$(date +%s)" \
-d '{
"events": [
"order.shipped",
"order.delivered",
"order.cancelled"
]
}'Response 200
The subscription after the update.
data
idstringrequiredurlstringrequiredeventsstring[]requiredactivebooleanrequiredmodestringrequiredWhich order mode this subscription accepts. Normalised on the way out, so a stored value this build does not know reads as what it is actually routed as (
all) rather than as the stored string.One of
alltestlivecreated_atstring (date-time)requiredsecret_rotated_atstring (date-time) | nullprevious_secret_expires_atstring (date-time) | nullDuring a rotation both secrets verify until this moment. Sign-check against both, or accept the older one, until it passes.
rotation_pendingbooleanTrue while the previous secret is still inside its window.
inboxbooleanrequiredTrue for the account's hosted test inbox (POST /v1/webhooks/inbox): it receives test events only and keeps them for GET /v1/webhooks/inbox rather than POSTing them, its
urlis the dashboard page that shows them, and its url and mode cannot be changed.
meta
pageintegertotalintegerlimitintegerThe page size actually applied, which is not necessarily the one asked for — the operation's own ceiling wins.
pagesintegertotaloverlimit, rounded up.next_cursorstring | nullOn a cursor-paged operation: send this back as
cursorfor the next page. Null on the last page. Absent on operations that do not page by cursor.has_morebooleanOn a list operation whose
metaisListMeta: whether another page follows this one.
{
"status": "success",
"data": {
"id": "whk_hCyrY24j_gUrQWxJ7ji6s9st",
"url": "https://example.com/hooks/orders",
"events": [
"order.shipped",
"order.delivered",
"order.cancelled"
],
"active": true,
"mode": "test",
"created_at": "2026-10-15T15:00:26.000Z",
"secret_rotated_at": null,
"previous_secret_expires_at": null,
"rotation_pending": false,
"inbox": false
},
"request_id": "4e3753e1-48dd-4313-bfd8-48e4af4e116a"
}Errors
Invalid request body or parameters. When the request failed the operation's schema,
detailscarriesformErrors(problems with the request as a whole),fieldErrors(messages keyed by top-level field) anderrors: one{ pointer, code, message }per refused value,pointeran RFC 6901 JSON pointer into the body (or into the object of query parameters, for a query parameter) —/items/0/sku, with~written~0and/written~1, and""for the request as a whole. POST /v1/orders/bulk answerserrorsas one{ index, details }per refused order, eachdetailswith its own pointers from the request body (/orders/3/items/0/sku); POST /v1/orders/multi-ship keepserrorsas the{ formErrors, fieldErrors }it always published; POST /v1/mockups/generate and POST /v1/products/recommend keep their fields at the top ofdetails, witherrorsbeside them. A refusal of a rule the schema cannot state (an asset that does not exist, an expiry too soon) names it inmessageand carries noerrors. event_not_available —eventsnames an event this portal does not offer (another family's, or one it never raises:order.rerouted); refused before anything is saved.details.eventsnames each one anddetails.availablelists the events a subscription can name here.Missing, invalid or expired API key:
missing_api_key(none sent),invalid_api_key(unknown or revoked),key_expired(past the expiry set when it was created; create a new key).live_key_required — a test key cannot move a subscription to live or all traffic. A test key changes only test-mode subscriptions; any other answers 404.
404Resource not found
payload_too_large — the request body exceeded this route's byte cap (256 KB unless the route raises it with a written reason). It is refused before it is parsed, so nothing was created. Send fewer rows, or use the endpoint's own asynchronous door where it has one.
Rate limit exceeded. Two windows can produce this status and
error.codesays which:rate_limit_exceededfor the per-minute sliding window,daily_quota_exceededfor the plan's daily quota. Both carryRetry-After; the daily one also carriesX-Daily-LimitandX-Daily-Remaining. The per-minute limits are plan-based — see Rate Limits above, or call GET /v1/me for this credential's own figures.