Order webhooks let YouPay send the current public state of an order to your system in real time, without exposing private customer or payment data.
Where to find it
In YouPay, go to Settings → Orders → Webhooks to configure webhook URLs, status filters, and your signing secret.
At a glance
- Event:
order.updated - Method:
POST - Content type:
application/json - Delivery target: each eligible configured webhook URL on the creator account
- Debounce window:
30seconds per order - Timeout:
5seconds per attempt - Retry behavior: one retry,
200milliseconds after a failed attempt - Signing:
HMAC-SHA256
Delivery requires enabled order webhooks, a signing secret, a selected public status, and a URL that passes YouPay's outbound safety checks when the webhook is sent.
Common use cases
- Show order progress inside a CRM, dashboard, or support tool.
- Trigger a thank-you workflow when an order reaches
fulfilled. - Notify support or operations when an order moves into
processing,action_required, orunder_review. - Keep an internal order table in sync.
How this webhook behaves
This is a state snapshot, not an event ledger.
Order-created, order-captured, and order-updated events schedule a webhook for 30 seconds later. If the same order schedules another webhook before that timer expires, the older scheduled delivery is dropped and only the latest public order state is sent.
In practice:
- You may not receive one webhook per internal change.
- The payload represents the latest public order state at send time.
- Your consumer should upsert the order state, not append blind history.
Fulfillment status updates are included in this flow. A fulfillment status change can change the public order status and trigger delivery.
What your endpoint should do
- Accept
POSTrequests over HTTPS. - Return a
2xxresponse quickly. - Verify the signature before running business logic.
- Handle repeated deliveries safely.
- Upsert by
order.idinstead of assuming one delivery per lifecycle step.
Request contract
HTTP request
POST {your-webhook-url}
Content-Type: application/json
Headers
x-youpay-webhook-event: order.updated
x-youpay-webhook-timestamp: 1718618400
x-youpay-webhook-signature: sha256=<hex digest>
Payload
The payload always has this shape:
{
"event": "order.updated",
"sent_at": "2026-06-17T10:00:00+00:00",
"order": {
"id": 12345,
"status": "Fulfilled",
"status_key": "fulfilled",
"created_at": "2026-06-17T09:55:12+00:00",
"updated_at": "2026-06-17T10:00:00+00:00",
"totals": {
"sub_total": 100,
"fee": 5,
"total": "105.00",
"currency": "USD",
"formatted_sub_total": "$1.00",
"formatted_fee": "$0.05",
"formatted_total": "$1.05"
},
"items_count": 1,
"items": [
{
"id": 10,
"title": "Webhook Gift",
"quantity": 1,
"currency": "USD",
"amount": 100,
"total": 100,
"formatted_total": "$1.00",
"type": "Gift Card"
}
]
}
}
Field guide
Top-level fields
| Field | Meaning |
|---|---|
event | Always order.updated |
sent_at | When YouPay generated this payload |
order | The current public order snapshot |
Order fields
| Field | Type | Meaning |
|---|---|---|
order.id | integer | YouPay order ID |
order.status | string | Human-readable public order status |
order.status_key | string | Stable machine-readable status |
order.created_at | string | null | Order creation timestamp |
order.updated_at | string | null | Latest order update timestamp |
order.items_count | integer | Number of order items |
Totals
Amounts can be JSON numbers or strings, depending on the underlying order values. Use the formatted fields when you need display-ready values.
| Field | Type |
|---|---|
order.totals.sub_total | number | string |
order.totals.fee | number | string |
order.totals.total | number | string |
order.totals.currency | string |
order.totals.formatted_sub_total | string |
order.totals.formatted_fee | string |
order.totals.formatted_total | string |
Items
Each item includes id, title, quantity, currency, amount, total, formatted_total, and type.
type is a display label, such as Gift Card, Creatorflow, Wish, Borderless, or Partner Store.
Statuses
Webhook delivery can be filtered by status_key.
Possible values:
fulfilledprocessingaction_requiredunder_reviewrefundedcancelledchargebackfailed
About fulfilled
fulfilled means the order is complete for public webhook purposes.
An order is fulfilled when:
- The order itself is completed.
- It has at least one fulfillment.
- Every fulfillment is
completed,fulfilled, ormanual.
Completed Shopify orders are also fulfilled when they have no fulfillments. Other completed orders with no fulfillments are processing for webhook purposes.
What is intentionally excluded
The payload does not include payment method, gateway, payment reference, private order details, payer profile data, or shopper profile data.
Treat it as a public operational payload, not a full finance export.
Verifying the signature
Every request is signed with the webhook secret stored on the creator account.
Build the signed message like this:
{timestamp}.{raw_request_body}
Then compute:
HMAC-SHA256(message, webhook_secret)
Compare the result to the x-youpay-webhook-signature value after removing the sha256= prefix.
Verification steps
- Read
x-youpay-webhook-timestamp. - Read the raw request body exactly as received.
- Read
x-youpay-webhook-signature. - Ensure the signature starts with
sha256=. - Compute the expected HMAC digest.
- Compare with a timing-safe equality check.
Node.js example
import crypto from "node:crypto";
export function verifyYouPayWebhook(rawBody, timestamp, signatureHeader, secret) {
if (!timestamp || !signatureHeader || !signatureHeader.startsWith("sha256=")) {
return false;
}
const provided = signatureHeader.slice("sha256=".length);
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex");
const providedBuffer = Buffer.from(provided, "hex");
const expectedBuffer = Buffer.from(expected, "hex");
if (providedBuffer.length !== expectedBuffer.length) {
return false;
}
return crypto.timingSafeEqual(providedBuffer, expectedBuffer);
}
PHP example
<?php
function verifyYouPayWebhook(string $raw_body, string $timestamp, string $signature_header, string $secret): bool
{
if ($timestamp === '' || ! str_starts_with($signature_header, 'sha256=')) {
return false;
}
$provided = substr($signature_header, strlen('sha256='));
$expected = hash_hmac('sha256', $timestamp.'.'.$raw_body, $secret);
return hash_equals($expected, $provided);
}
A practical handler
For most integrations, the simplest safe pattern is:
- Verify the signature.
- Parse the JSON.
- Upsert the order by
order.id. - Use
status_keyfor machine logic. - Use
statusfor display copy.
if (payload.order.status_key === "fulfilled") {
markGiftAsDelivered(payload.order.id);
}
if (payload.order.status_key === "processing") {
queueCreatorNotification(payload.order.id);
}
Because delivery is debounced, a practical idempotency key is:
order.id + order.updated_at
Current limits
The current contract does not include a unique delivery ID, a replay endpoint, or historical event sequencing.
If you need a full audit trail, store each accepted payload on your side using order.id and order.updated_at.
Was this article helpful?
That’s Great!
Thank you for your feedback
Sorry! We couldn't be helpful
Thank you for your feedback
Feedback sent
We appreciate your effort and will try to fix the article