Order Webhooks

Modified on Mon, 13 Jul at 3:08 PM

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: 30 seconds per order
  • Timeout: 5 seconds per attempt
  • Retry behavior: one retry, 200 milliseconds 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, or under_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 POST requests over HTTPS.
  • Return a 2xx response quickly.
  • Verify the signature before running business logic.
  • Handle repeated deliveries safely.
  • Upsert by order.id instead 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

FieldMeaning
eventAlways order.updated
sent_atWhen YouPay generated this payload
orderThe current public order snapshot

Order fields

FieldTypeMeaning
order.idintegerYouPay order ID
order.statusstringHuman-readable public order status
order.status_keystringStable machine-readable status
order.created_atstring | nullOrder creation timestamp
order.updated_atstring | nullLatest order update timestamp
order.items_countintegerNumber 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.

FieldType
order.totals.sub_totalnumber | string
order.totals.feenumber | string
order.totals.totalnumber | string
order.totals.currencystring
order.totals.formatted_sub_totalstring
order.totals.formatted_feestring
order.totals.formatted_totalstring

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:

  • fulfilled
  • processing
  • action_required
  • under_review
  • refunded
  • cancelled
  • chargeback
  • failed

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, or manual.

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

  1. Read x-youpay-webhook-timestamp.
  2. Read the raw request body exactly as received.
  3. Read x-youpay-webhook-signature.
  4. Ensure the signature starts with sha256=.
  5. Compute the expected HMAC digest.
  6. 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:

  1. Verify the signature.
  2. Parse the JSON.
  3. Upsert the order by order.id.
  4. Use status_key for machine logic.
  5. Use status for 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

Let us know how can we improve this article!

Select at least one of the reasons
CAPTCHA verification is required.

Feedback sent

We appreciate your effort and will try to fix the article