Skip to content

Create a webhook subscription

Open .md

POST/v1/api/store/{team_slug}/setting/webhook_subscription

v1Operation ID v1_public_webhook_subscription_store

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/json
  • User-Agent: YoPrint Webhooks Agent
  • X-Signature: the HMAC-SHA256 of the raw request body, keyed with the subscription's secret, 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

  1. Read the raw body bytes before any JSON parsing.
  2. Compute HMAC-SHA256 of those bytes with the secret (the whole string, whsec_ included) and hex-encode it.
  3. Compare the result with X-Signature in constant time, for example hash_equals(hash_hmac('sha256', $rawBody, $secret), $signature) in PHP or crypto.timingSafeEqual in 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 200 within 3 seconds. Do slow work afterwards.
  • YoPrint checks the TLS certificate of https URLs and does not follow redirects: a 3xx answer 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_code 999. YoPrint connects only to the addresses it checked, never through a proxy.
  • A non-2xx status, 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 2xx stops the retries, but only 200 counts as a success in the delivery log. Other 2xx codes show as error.
  • YoPrint can turn off a subscription whose deliveries keep failing. It then sets status to disabled and disable_reason to auto_disable_too_many_errors, and emails the team owner. YoPrint does not promise when it checks, so pause a broken receiver yourself by setting status to disabled.
  • 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 source zapier, a 410 Gone answer 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, with trashed: 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, plus previous_status_id, previous_status and participant_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 to all gets 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, plus previous_status_id, previous_status and participant_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, plus previous_status_id and previous_status.
  • shipment.status_changed: a shipment's status is changed. Payload: the shipment, plus previous_status_id and previous_status.
  • purchase_order.status_changed: a purchase order's status is set, even to the status it already has. Payload: the purchase order, plus previous_status_id, previous_status and related_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 its order and customer.
  • job_approval.approved, job_approval.change_requested: a job approval is completed. Payload: the approval, with its order, job and customer.

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 its order.
  • payment_received.deposit_amount: after a payment, the order's total paid equals its deposit amount (calc_deposit_total) exactly. Sent as well as any_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 as any_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 its order.
  • customer_credit.deposited, customer_credit.withdrew: credit is added to or taken from a customer's credit balance. Payload: the transaction, with its customer.

Customers and contacts

  • customer.created, customer.updated, customer.deleted: a customer is created, changed or deleted. Payload: the customer, with its contacts.
  • customer_contact.created, customer_contact.updated, customer_contact.deleted: a customer contact is created, changed or deleted. Payload: the contact, with its customer.

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, plus previous_status_id and previous_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.

Permission

Needs the Manage Settings permission.

Business rules

  • url, name, enabled_events (at least one name, no repeats) and source are required.
  • url must be an https:// 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 secret in 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.
  • config can 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 url as described in "What YoPrint sends".

Path parameters

  • team_slugstringrequired

    Your team slug, shown as Team ID next to your API key in Profile.

    • Example your-team

Request body

application/json, required.

  • enabled_eventsarray of stringsrequired

    Event names to subscribe to from the Events table, or all for 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
  • namestringrequired

    A name to recognize it by.

  • sourcestringrequired

    Send yoprint. YoPrint's Zapier app sends zapier, which changes failure handling; don't use it.

  • urlstringrequired

    Where YoPrint sends events. Must be an https:// URL on the public internet. YoPrint refuses plain http:// 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.

  • configobject

    Optional status filters for the status-change events. Without a filter, every status change is sent. The other events ignore config, and so does the older order.status_changed. Status ids come from the status lists (see Statuses).

    Child fields (7)
    • Send job.status_changed only when the new status is one of these job statuses.

      Child fields (1)
      • status_idsarray of strings

        Status ids, exactly as the status lists return them.

        • At least 1 item
    • Send message.status_changed only when the new status is one of these customer message statuses.

      Child fields (1)
      • status_idsarray of strings

        Status ids, exactly as the status lists return them.

        • At least 1 item
    • Send purchase_order.status_changed only when the new status is one of these purchase order statuses.

      Child fields (1)
      • status_idsarray of strings

        Status ids, exactly as the status lists return them.

        • At least 1 item
    • Send quote.status_changed only when the new status is one of these quote statuses.

      Child fields (1)
      • status_idsarray of strings

        Status ids, exactly as the status lists return them.

        • At least 1 item
    • Send sales_order.status_changed only when the new status is one of these sales order statuses.

      Child fields (1)
      • status_idsarray of strings

        Status ids, exactly as the status lists return them.

        • At least 1 item
    • Send shipment.status_changed only when the new status is one of these shipment statuses.

      Child fields (1)
      • status_idsarray of strings

        Status ids, exactly as the status lists return them.

        • At least 1 item
    • Send work_pipeline.status_changed only when the new status is one of these task statuses.

      Child fields (1)
      • status_idsarray of strings

        Status ids, exactly as the status lists return them.

        • At least 1 item
More examples (1)

New sales orders and payments

curl
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.

  • dataobjectrequired
    Child fields (10)
    • created_atstring (date-time)required
    • disable_reasonstring | nullrequired

      auto_disable_too_many_errors when YoPrint turned the subscription off because its deliveries kept failing. Otherwise null. Setting status back to active does not clear it.

    • enabled_eventsarray of stringsrequired

      Subscribed event names, or all for every event.

    • idstringrequired

      The subscription id.

    • namestringrequired
    • secretstring | nullrequired

      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.

    • sourcestringrequired

      The source sent when the subscription was created: the web app sends yoprint, YoPrint's Zapier app sends zapier.

    • statusstringrequired

      Only active subscriptions receive events.

      • One of: "active", "disabled"
    • updated_atstring (date-time)required
    • urlstring | nullrequired

      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.

  • statusintegerrequired
  • successbooleanrequired

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, with unauthorized or user_is_not_authorized.

    When
    Your permission group lacks Manage Settings. This 403 has a short body, {"message": "Insufficient Permission"}, without success or error.
    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_failed lists what to fix in error.fields; other codes name a business rule, explained in error.message. A few checks answer with only {"message": "..."}. An operation that calls another service (a payment provider, a carrier, an accounting app) can answer connect_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, url is not a URL, enabled_events is empty or repeats a name, or a status_ids list is empty. error.code is validation_failed, and error.fields names each field with its messages.
    Fix
    Fix the fields listed in error.fields and send the request again.
    When
    url is refused by the public-address check (validation_failed, with error.fields.url and error.field_reasons.url.reason). not_public_address: the URL is not https://, 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. For host_lookup_failed, check the host name, or try again in a minute if its DNS is new.

Notes

  • Store the secret from 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 payload built from your team's data.
  • Watch deliveries with List deliveries for a webhook subscription.