Create a webhook subscription
AI agents: read /llms.txt; add .md to any page URL. This page as Markdown: /reference/webhook-subscriptions/v1-webhook-subscription-store.md. Index of every page and the rules for every call: /llms.txt. Guides and concepts in one file: /llms-full.txt. OpenAPI: /openapi.json.
POST/
Subscribes a URL to team events. The subscription starts active, and YoPrint generates its signing secret and returns it in the response.
Events fire for changes made in the web app, through the API and by AI apps alike. A change that triggers several events sends each of them.
What YoPrint sends
About 10 seconds after an event, YoPrint sends one POST to the url of every active subscription whose enabled_events holds the event name or all.
Headers:
Content-Type: application/jsonUser-Agent: YoPrint Webhooks AgentX-Signature: the HMAC-SHA256 of the raw request body, keyed with the subscription'ssecret, as 64 lowercase hex characters.
Body:
{
"id": "VwYKuTP9vd8Dfo",
"type": "order.created",
"payload": {
"id": "BkzhmfgkK2HFTs",
"scoped_id": "SO1001",
"...": "..."
},
"created_at": "2026-11-03T15:00:00+00:00",
"updated_at": "2026-11-03T15:00:00+00:00"
}
id identifies the event: retries of an event carry the same id. payload is the record the event is about (see "Events" below).
Verify the signature
- Read the raw body bytes before any JSON parsing.
- Compute HMAC-SHA256 of those bytes with the secret (the whole string,
whsec_included) and hex-encode it. - Compare the result with
X-Signaturein constant time, for examplehash_equals(hash_hmac('sha256', $rawBody, $secret), $signature)in PHP orcrypto.timingSafeEqualin Node.js. Reject the request when they differ.
Don't parse and re-serialize the JSON before checking: YoPrint's JSON escapes / as \/ and non-ASCII characters as \uXXXX, so the bytes change. The signature has no timestamp, so remember the event ids you have processed and ignore repeats.
Answer, retries and timeouts
- Answer with status
200within 3 seconds. Do slow work afterwards. - YoPrint checks the TLS certificate of
httpsURLs and does not follow redirects: a3xxanswer is a failure. - Before every attempt YoPrint looks up the URL's host again. If it doesn't resolve, or any address it resolves to is not a public internet address (private, loopback, link-local, carrier-grade NAT, multicast or reserved), nothing is sent and the attempt fails with
status_code999. YoPrint connects only to the addresses it checked, never through a proxy. - A non-
2xxstatus, a timeout or a connection error fails the attempt. YoPrint tries an event up to 3 times: once, again 10 seconds after a failure, and a last time 100 seconds after the second failure. Then it gives up on that event. - Any
2xxstops the retries, but only200counts as a success in the delivery log. Other2xxcodes show aserror. - YoPrint can turn off a subscription whose deliveries keep failing. It then sets
statustodisabledanddisable_reasontoauto_disable_too_many_errors, and emails the team owner. YoPrint does not promise when it checks, so pause a broken receiver yourself by settingstatustodisabled. - A team whose plan no longer includes API access gets no deliveries, and no delivery rows are logged. Subscriptions are kept and deliver again after an upgrade. Every API request then answers 403
api_access_not_in_plan, listing and deleting included. In YoPrint's settings the team can still list and delete its subscriptions. - Each delivery is sent on its own, so events can arrive out of order, for example after a retry.
- For a subscription with
sourcezapier, a410 Goneanswer stops the retries at once.
Events
Sales orders and quotes
order.created: a sales order is published (it leaves draft). Nothing is sent for drafts. Payload: the sales order.quote.created: a quote is published. Payload: the quote.order.deleted,quote.deleted: a sales order or quote is deleted. Payload: the order or quote, withtrashed: true.order.restored,quote.restored: a deleted sales order or quote is restored. Payload: the order or quote.quote.converted_to_sales_order: a quote is converted to a sales order. Payload: the converted document.order.converted_to_quote: a sales order is converted to a quote. Payload: the converted document.order.marked_as_rush,order.marked_as_not_rush: a sales order's rush flag is turned on or off. Payload: the sales order.sales_order.status_changed: a sales order moves to a different status. Payload: the sales order, plusprevious_status_id,previous_statusandparticipant_emails(the message participants who receive email).quote.status_changed: the same, for a quote.order.status_changed: either of the two above. It is an older event kept for Zapier and sent alongside them, so a subscriber toallgets two events for one status change. Payload: the same.
Jobs, tasks, shipments and purchase orders
job.status_changed: a job moves to a different status. Payload: the job, plusprevious_status_id,previous_statusandparticipant_emails.job.marked_as_rush,job.marked_as_not_rush: a job update turns its rush flag on or off. Payload: the job.work_pipeline.status_changed: a task moves to a different status. Payload: the task, plusprevious_status_idandprevious_status.shipment.status_changed: a shipment's status is changed. Payload: the shipment, plusprevious_status_idandprevious_status.purchase_order.status_changed: a purchase order's status is set, even to the status it already has. Payload: the purchase order, plusprevious_status_id,previous_statusandrelated_order(its linked sales orders, or null).
Approvals
order_approval.approved,order_approval.change_requested: an order approval is completed, approved or with changes requested. Payload: the approval, with itsorderandcustomer.job_approval.approved,job_approval.change_requested: a job approval is completed. Payload: the approval, with itsorder,jobandcustomer.
Payments and customer credit
payment_received.any_amount: any payment is recorded on a sales order: a manual payment, a gateway or payment-link payment, or a payment from customer credit. Payload: the payment, with itsorder.payment_received.deposit_amount: after a payment, the order's total paid equals its deposit amount (calc_deposit_total) exactly. Sent as well asany_amount. Not sent when a payment goes past the deposit, for example a first payment of the full amount. Payload: the same.payment_received.full_amount: after a payment, the total paid equals the invoiced total exactly (the balance due is zero). Sent as well asany_amount. Not sent for an overpayment, or while the order has no invoice. Payload: the same.payment.refund_issued: a refund is recorded on a sales order. Payload: the refund, with itsorder.customer_credit.deposited,customer_credit.withdrew: credit is added to or taken from a customer's credit balance. Payload: the transaction, with itscustomer.
Customers and contacts
customer.created,customer.updated,customer.deleted: a customer is created, changed or deleted. Payload: the customer, with itscontacts.customer_contact.created,customer_contact.updated,customer_contact.deleted: a customer contact is created, changed or deleted. Payload: the contact, with itscustomer.
Messages
message.received_customer: a customer sends a message from the customer portal. Payload: the message.message.received_purchase_order: a vendor sends a message from the vendor portal. Payload: the message.message.customer_bounced,message.bounced_purchase_order: an email about a sales order, or about a purchase order, cannot be delivered. Payload: the message.message.status_changed: a customer message's status is set, even to the status it already has. Payload: the message, plusprevious_status_idandprevious_message_status.
Reading payloads
Don't rely on deposit_amount or full_amount alone to start work: both need an exact match, and both can be sent again after a refund and a new payment. Subscribe to payment_received.any_amount, then read the order back (Get a sales order or quote) and compare what was paid with the deposit and the balance.
Every order and quote payload says which it is with is_quote (true or false) and object_type (sales_order or quote). The payloads of order.created and of the deleted, restored, converted and rush events also have type (order or quote); the quote.created and status-change payloads don't. All of them carry older fields such as is_draft and is_created; use mode instead.
Order and quote payloads give calc_total and the other calc_* amounts in thousandths, an older encoding (calc_total: 125000 is 125.00; see Money and quantities). The payloads of order.created and of the deleted, restored, converted and rush events also give them as formatted strings with the currency symbol (formatted_calc_total: "$125.00"). The quote.created and status-change payloads have only some formatted amounts, such as formatted_invoiced_total and formatted_order_balance_due.
Payment payloads give amount as a formatted string for display (such as "$125.00"; do not parse it—read the order for exact amounts), and name the payment method in payment_method_name. A payment a customer made through the customer portal's checkout also carries funding_payment_gateway (the gateway), funding_transaction_id, currency_iso, is_external_cash_movement and customer_payment_* fields, and YoPrint sets its payment_method_name (such as Credit Card/Debit Card or PayPal). Status ids in payloads are the same ids the status lists return.
Order and quote payloads include customer_portal_order_link and customer_portal_payment_link. These links open the customer's portal without a sign-in, so treat payloads as sensitive: do not log or forward them.
To see the exact payload of an event for your team, call Show a sample event.
Business rules
url,name,enabled_events(at least one name, no repeats) andsourceare required.urlmust be anhttps://URL whose host resolves, and resolves only to public internet addresses. YoPrint looks the host up when you save; it sends nothing to it.- A
secretin the body is ignored. YoPrint makes the secret, and it never changes. To get a new secret, create a new subscription and delete the old one. configcan be set only here. Updating a subscription does not change it, and no response shows it.- A team can have any number of subscriptions, and two subscriptions may use the same URL. Each gets its own copy of every event.
Side effects
- From now on, matching events are sent to
urlas described in "What YoPrint sends".
Path parameters
Your team slug, shown as Team ID next to your API key in Profile.
- Example
your-team
- Example
Request body
application/json, required.
Event names to subscribe to from the Events table, or
allfor every event (including future events). YoPrint does not validate event names: misspelled names are saved without error, but will never match any event.- At least 1 item
- Items must be unique
A name to recognize it by.
Send
yoprint. YoPrint's Zapier app sendszapier, which changes failure handling; don't use it.Where YoPrint sends events. Must be an
https://URL on the public internet. YoPrint refuses plainhttp://URLs and any host that resolves to a non-public address (private, loopback, link-local, carrier-grade NAT, multicast or reserved). YoPrint looks up the host when you save, but does not send a test request. Use the final destination URL because redirects are not followed.configobjectOptional status filters for the status-change events. Without a filter, every status change is sent. The other events ignore
config, and so does the olderorder.status_changed. Status ids come from the status lists (see Statuses).Child fields (7)
Send
job.status_changedonly when the new status is one of these job statuses.Child fields (1)
status_idsarray of stringsStatus ids, exactly as the status lists return them.
- At least 1 item
Send
message.status_changedonly when the new status is one of these customer message statuses.Child fields (1)
status_idsarray of stringsStatus ids, exactly as the status lists return them.
- At least 1 item
Send
purchase_order.status_changedonly when the new status is one of these purchase order statuses.Child fields (1)
status_idsarray of stringsStatus ids, exactly as the status lists return them.
- At least 1 item
Send
quote.status_changedonly when the new status is one of these quote statuses.Child fields (1)
status_idsarray of stringsStatus ids, exactly as the status lists return them.
- At least 1 item
Send
sales_order.status_changedonly when the new status is one of these sales order statuses.Child fields (1)
status_idsarray of stringsStatus ids, exactly as the status lists return them.
- At least 1 item
Send
shipment.status_changedonly when the new status is one of these shipment statuses.Child fields (1)
status_idsarray of stringsStatus ids, exactly as the status lists return them.
- At least 1 item
Send
work_pipeline.status_changedonly when the new status is one of these task statuses.Child fields (1)
status_idsarray of stringsStatus ids, exactly as the status lists return them.
- At least 1 item
More examples (1)
New sales orders and payments
curl -X POST 'https://secure.yoprint.com/v1/api/store/your-team/setting/webhook_subscription' \ -H 'Authorization: Bearer YOUR_API_KEY' \ -H 'Accept: application/json' \ -H 'X-Yoprint-Decimal-Format: exact-v1' \ -H 'Content-Type: application/json' \ -d '{ "enabled_events": [ "order.created", "payment_received.any_amount" ], "name": "Order sync", "source": "yoprint", "url": "https://example.com/yoprint/webhooks" }'Response
200Successful response.
Child fields (10)
auto_disable_too_many_errorswhen YoPrint turned the subscription off because its deliveries kept failing. Otherwise null. Settingstatusback toactivedoes not clear it.Subscribed event names, or
allfor every event.The subscription id.
The secret used to sign event payloads:
whsec_followed by 32 letters and digits. Use it to verify incoming messages. YoPrint creates it when the subscription is created and never changes it. Keep it private. Only the team owner and members whose permission group has Manage Settings can see it; for every other member it is null. It is also null for AI apps reading through YoPrint's MCP server.The
sourcesent when the subscription was created: the web app sendsyoprint, YoPrint's Zapier app sendszapier.Only
activesubscriptions receive events.- One of:
"active","disabled"
- One of:
The destination URL where YoPrint delivers events. On this API, this is the full URL. AI apps reading subscriptions through YoPrint's MCP server see only the origin (scheme and host, such as
https://example.com) to protect webhook secrets.
Errors
401The API key is missing or wrong (
unauthenticated).- When
- The API key is missing or wrong.
- Fix
- Send your personal API key as
Authorization: Bearer YOUR_API_KEY.
403You are not allowed to do this.
api_access_not_in_plan: your team's plan doesn't include API access (Pro and above, with an active subscription or trial), so every request answers this until the team upgrades. When your role lacks the permission the operation needs, the body is only{"message": "Insufficient Permission"}. Other refusals use the error envelope, withunauthorizedoruser_is_not_authorized.- When
- Your permission group lacks Manage Settings. This 403 has a short body,
{"message": "Insufficient Permission"}, withoutsuccessorerror. - Fix
- Ask the team owner to give your permission group Manage Settings.
404Not found.
page_not_found: the team slug is unknown, you are not a member of the team, or an id in the path is not one of this team's.code: null, with a message that starts "No query results for": there is no record with that id.422The request was refused and changed nothing.
validation_failedlists what to fix inerror.fields; other codes name a business rule, explained inerror.message. A few checks answer with only{"message": "..."}. An operation that calls another service (a payment provider, a carrier, an accounting app) can answerconnect_timeout("Please retry again") when that service does not answer in time: the change may or may not have happened there, so read the record back before you retry.- When
- A required field is missing,
urlis not a URL,enabled_eventsis empty or repeats a name, or astatus_idslist is empty.error.codeisvalidation_failed, anderror.fieldsnames each field with its messages. - Fix
- Fix the fields listed in
error.fieldsand send the request again. - When
urlis refused by the public-address check (validation_failed, witherror.fields.urlanderror.field_reasons.url.reason).not_public_address: the URL is nothttps://, has no usable host, or its host reaches an address that is not on the public internet ("Use an https:// URL that reaches a public internet address.").host_lookup_failed: the host name could not be looked up ("The host name in this URL could not be looked up. Check the address, or try again in a minute.").- Fix
- Use an
https://URL on a public host. Forhost_lookup_failed, check the host name, or try again in a minute if its DNS is new.
Notes
- Store the
secretfrom the response with your other secrets. You can read it again later with Show a webhook subscription, as the team owner or with Manage Settings. - Check what you will receive before going live: Show a sample event returns a real
payloadbuilt from your team's data. - Watch deliveries with List deliveries for a webhook subscription.
Related
- GETShow a sample event
/v1/ api/ store/ {team_slug}/ setting/ webhook_subscription/ sample/ {event_name} - GETList deliveries for a webhook subscription
/v1/ api/ store/ {team_slug}/ setting/ webhook_subscription/ {id}/ log - PATCHTouch a webhook subscription
/v1/ api/ store/ {team_slug}/ setting/ webhook_subscription/ {hash_id} - GETList job statuses
/v1/ api/ store/ {team_slug}/ setting/ status/ job - GETList sales order statuses
/v1/ api/ store/ {team_slug}/ setting/ status/ sales_order - GETGet a sales order or quote
/v2/ api/ store/ {team_slug}/ v2/ sales_order/ {order_unique_key}
