Skip to content

Receive webhooks

Open .md

A webhook subscription tells YoPrint to POST an event to your URL whenever something happens on the team, such as a sales order being created or a payment arriving. Events fire for changes made in the web app, through the API and by AI apps alike.

  1. Something happensAn order is created, a payment arrives…
  2. YoPrint POSTs the eventAbout 10 seconds later, signed with your secret
  3. You check and answerVerify the signature, then answer 200
  4. Handle it onceUse the event’s id: a retry can repeat it
No 2xx answer in time? YoPrint tries twice more, 10 and then 100 seconds later, and then gives up on that event.

You need a role that can manage settings to create, change or delete subscriptions.

Webhooks need a plan with API access (Pro or higher). See If the team’s plan changes.

Your endpoint receives a POST with a JSON body:

{
"id": "V4keXwwxMQyXw7",
"type": "order.created",
"payload": { "id": "VwYKuTP9vd8Dfo", "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 the same event carry the same id.
  • type is the event name.
  • payload is the record the event is about. Its fields depend on the event. The example above is shortened.

The request has these headers:

Header Value
Content-Type application/json
User-Agent YoPrint Webhooks Agent
X-Signature The signature of the body. See step 4.

Answer within 3 seconds with status 200. Do the real work afterwards, for example on a queue.

POST Create a webhook subscription /v1/api/store/{team_slug}/setting/webhook_subscription
Terminal window
curl 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 '{
"name": "Order sync",
"url": "https://example.com/yoprint/webhooks",
"enabled_events": ["order.created", "payment_received.any_amount"],
"source": "yoprint"
}'
  • name, url, enabled_events and source are required. Send "source": "yoprint". The value zapier is for YoPrint’s Zapier app.
  • url must be a public https:// address. See Which URLs YoPrint accepts.
  • enabled_events lists event names. The single value all subscribes to every event.

The response includes the subscription’s id and its secret, which starts with whsec_. YoPrint makes the secret, and you can’t choose it. Store it with your other secrets. Reading a subscription later shows the secret only to the team owner and people who can manage settings. For anyone else, secret is null.

You can also create subscriptions in the web app, under Webhooks in settings.

YoPrint posts to your URL from its own servers, so it accepts only addresses on the public internet. Creating a subscription, or changing its URL, is refused with 422 validation_failed when:

The URL Message, in error.fields.url error.field_reasons.url.reason
isn’t https://, or its host is, or resolves to, a private, loopback, link-local or other non-public address “Use an https:// URL that reaches a public internet address.” not_public_address
has a host name that couldn’t be looked up “The host name in this URL could not be looked up. Check the address, or try again in a minute.” host_lookup_failed

A failed lookup can be temporary: check the address, then try again. YoPrint checks the address again just before each delivery, and doesn’t send to one that no longer reaches a public address.

GET Show a sample event /v1/api/store/{team_slug}/setting/webhook_subscription/sample/{event_name}
Terminal window
curl https://secure.yoprint.com/v1/api/store/your-team/setting/webhook_subscription/sample/order.created \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Accept: application/json" \
-H "X-Yoprint-Decimal-Format: exact-v1"

The sample is built from your team’s latest matching record, so it shows the real shape of payload. It comes back as the bare event, without the usual status, success and data wrapper. On a team with no matching records, it’s empty.

X-Signature is the HMAC-SHA256 of the raw request body, keyed with the subscription’s secret, as lowercase hex. Check it before you trust a delivery.

  1. Read the raw body bytes, before any JSON parsing.
  2. Compute HMAC-SHA256 of those bytes, keyed with the whole secret, whsec_ prefix included, and hex-encode it.
  3. Compare it with X-Signature using a constant-time comparison. Reject the request if they differ.

A complete receiver checks the signature, answers 200, and skips events it has already handled. Each one reads the secret from the YOPRINT_WEBHOOK_SECRET environment variable.

Node.js 18 or later, with Express.

import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';
const SECRET = process.env.YOPRINT_WEBHOOK_SECRET; // the whole secret, whsec_ included
const handled = new Set(); // keep these in your database in production
const app = express();
// express.raw keeps the body as bytes, so the signature can be checked.
app.post('/yoprint/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.get('X-Signature') ?? '';
const expected = createHmac('sha256', SECRET).update(req.body).digest('hex');
if (signature.length !== expected.length || !timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).end();
}
const event = JSON.parse(req.body.toString('utf8'));
res.status(200).end(); // answer first: YoPrint waits 3 seconds at most
if (handled.has(event.id)) return;
handled.add(event.id);
setImmediate(() => handle(event));
});
function handle(event) {
console.log(event.type, event.payload.id);
}
app.listen(3000);

The signature has no timestamp. To guard against a replayed delivery, remember the event ids you’ve processed and ignore repeats.

YoPrint sends each event about 10 seconds after it happens. A delivery succeeds when your endpoint answers 2xx in time. Otherwise YoPrint tries again:

Attempt When
1 About 10 seconds after the event
2 10 seconds after attempt 1 failed
3 100 seconds after attempt 2 failed

After the third failed attempt, YoPrint gives up on that event.

Because of retries, your endpoint can get the same event more than once. Use id to process each event once.

GET List deliveries for a webhook subscription /v1/api/store/{team_slug}/setting/webhook_subscription/{id}/log

The log lists deliveries newest first, with the attempt number, the status code your endpoint returned, and the event itself. For a failed delivery, error says only what YoPrint knows, such as “The receiver answered with HTTP 500.” or that it couldn’t reach the receiver (a connection, DNS, TLS or timeout error). YoPrint doesn’t keep or show what your endpoint answered, so log failures on your side to debug a receiver.

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 doesn’t promise when it checks, so don’t rely on it to stop a broken receiver: pause the subscription yourself.

Read a subscription to check its status and disable_reason:

GET Show a webhook subscription /v1/api/store/{team_slug}/setting/webhook_subscription/{hash_id}

Turn it back on, change its URL or events, or pause it with "status": "disabled". Send url with every update, even when it doesn’t change: an unchanged URL isn’t checked again, so a subscription whose receiver is broken can still be paused.

PUT Update a webhook subscription /v1/api/store/{team_slug}/setting/webhook_subscription/{hash_id}

To stop for good, delete it. YoPrint then sends one DELETE request to the subscription’s URL.

DELETE Delete a webhook subscription /v1/api/store/{team_slug}/setting/webhook_subscription/{hash_id}

There’s no button to send a test event. To get a real delivery without touching real orders:

  1. Subscribe a test URL to order.created and order.deleted.
  2. Create a draft sales order for a test customer, add a line, and publish it. That sends order.created.
  3. Delete the order. That sends order.deleted.
  4. Read the delivery log to see what your endpoint answered.

To see an event’s shape without sending anything, use Show a sample event (step 3). See Test your integration.

Webhooks need a plan with API access (Pro or higher), with an active subscription or trial. While a team doesn’t have it:

  • YoPrint sends no deliveries. Events from that time are never sent later, and the delivery log shows nothing for them.
  • Every API request is refused with 403 api_access_not_in_plan, including listing, changing and deleting subscriptions. Zapier can’t remove its subscription when someone turns a Zap off.
  • In YoPrint’s settings, the team can still list and delete its subscriptions. The settings menu hides the Webhooks link, but the page still opens from a direct link.

Subscriptions are kept, with their URLs and secrets, and deliver again once the team is back on a plan with API access.

The status-change events can be narrowed to some statuses. Add the status ids under config when you create the subscription:

{
"name": "Ready to ship",
"url": "https://example.com/yoprint/webhooks",
"enabled_events": ["job.status_changed"],
"source": "yoprint",
"config": {
"job_status_changed_options": { "status_ids": ["BkzhmfgkK2HFTs"] }
}
}

The options are job_status_changed_options, sales_order_status_changed_options, quote_status_changed_options, shipment_status_changed_options, purchase_order_status_changed_options, work_pipeline_status_changed_options and message_status_changed_options. Get status ids from the status lists, such as:

GET List job statuses /v1/api/store/{team_slug}/setting/status/job
Area Events
Customers customer.created, customer.updated, customer.deleted
Contacts customer_contact.created, customer_contact.updated, customer_contact.deleted
Customer credit customer_credit.deposited, customer_credit.withdrew
Sales orders order.created, order.deleted, order.restored, order.converted_to_quote, order.marked_as_rush, order.marked_as_not_rush, sales_order.status_changed
Quotes quote.created, quote.deleted, quote.restored, quote.converted_to_sales_order, quote.status_changed
Approvals order_approval.approved, order_approval.change_requested, job_approval.approved, job_approval.change_requested
Jobs and tasks job.status_changed, job.marked_as_rush, job.marked_as_not_rush, work_pipeline.status_changed
Payments payment_received.any_amount, payment_received.deposit_amount, payment_received.full_amount, payment.refund_issued
Shipping and purchasing shipment.status_changed, purchase_order.status_changed
Messages message.received_customer, message.customer_bounced, message.received_purchase_order, message.bounced_purchase_order, message.status_changed
  • order.created and quote.created fire when the order or quote is published, not when the draft is made.
  • In payment events (payment_received.*), payload.amount is a formatted string for display (such as "$125.00"), not a number. Don’t parse it. For exact values, read the order: paid_amount is in thousandths, and totals are decimal pairs (order_item_cart_total, order_balance_due, deposit_amount_due). See Money and quantities.
{
"id": "VwYKuTP9vd8Dfo",
"type": "payment_received.any_amount",
"payload": {
"id": "p8Eq3vCAHNqhaB",
"amount": "$125.00",
"action": "MANUAL PAYMENT",
"payment_method_name": "Credit Card",
"transact_at": null,
"order": {
"id": "V4keXwwxMQyXw7",
"scoped_id": "SO1001",
"paid_amount": 125000
}
},
"created_at": "2026-11-03T15:00:00+00:00",
"updated_at": "2026-11-03T15:00:00+00:00"
}
  • In customer credit events (customer_credit.*), payload.amount is also a formatted string (such as "$50.00"). For exact funds, read the customer (wallet_fund is in thousandths).

  • Which payment event to listen to:

    To know when… Listen to Then
    Any payment arrives payment_received.any_amount It fires for every payment.
    The order is fully paid payment_received.full_amount It fires when nothing is left to pay.
    The deposit is paid payment_received.any_amount Read the order’s deposit_amount_due: 0 or less means paid. payment_received.deposit_amount fires only when the total paid is exactly the deposit, so a bigger payment never sends it.
  • order.status_changed is an older event for both sales orders and quotes, sent as well as sales_order.status_changed or quote.status_changed. A subscription to all gets two events for one status change. Use the newer two.

Some changes have no event, so don’t wait for one:

  • Invoices: there are no invoice events.
  • Creating a shipment or a job: only their status changes fire.
  • Products, services and pricing: no events at all.

To follow these, read the records after a related event, or on a schedule.

  • Webhook amounts are for display. The amount in payment and customer credit webhooks is formatted text (such as "$125.00"). To do arithmetic or read exact numbers, fetch the order or customer. See Money and quantities.
  • Public https:// URLs only. A local, private or plain http:// address is refused. To try webhooks from your own machine, use a tunnel with a public https:// address.
  • Event names aren’t checked. A misspelled event is saved and never fires. Copy names from the table above.
  • Answer exactly 200. Other 2xx codes stop the retries, but the delivery log records them as errors, and repeated errors can disable the subscription.
  • No redirects. YoPrint doesn’t follow them, so a URL that redirects (from http to https, or to add a trailing slash) fails every time. Use the final URL.
  • 3 seconds. Slow endpoints time out and get retried. Acknowledge first, work later.
  • Drafts are quiet. order.created and quote.created come at publish, not before. Deleting a draft still fires order.deleted or quote.deleted.