The webhook sandbox lets you trigger a webhook on demand, without placing an order and waiting for it to ship. WHCC builds the notification, signs it, and delivers it to your registered callback URI through the normal delivery path. Your handler cannot tell the difference except by the fields described on this page.
These routes exist only on the WHCC sandbox environment. On https://apps.whcc.com they return 404 Not Found, and they always will. Do not build a production code path that calls them.
Use the same base URL and the same credentials you already use for sandbox work: https://sandbox.apps.whcc.com. This is the sandbox environment described in the Order Submit API overview.
The sandbox delivers through the real delivery path, so it needs everything the real path needs. Three conditions must hold before a trigger succeeds:
Developing against a handler on your own machine? The callback URI has to be a public HTTPS URL, so put a tunnel in front of it — see Local Webhook Testing for ngrok and Cloudflare Tunnel setups, and why the URL needs to stay fixed.
The trigger checks all three before it writes anything, and answers each missing state with its own error:
| Response | Meaning |
|---|---|
409 no_callback_registered |
No callback registered. Call /api/callback/create, then /api/callback/verify. |
409 callback_not_verified |
The callback is registered but not verified. Call /api/callback/verify. |
409 no_consumer_secret |
Your consumer has no secret, so the webhook would be delivered unsigned. Contact WHCC. |
Every response on this page is JSON. Send an Authorization: Bearer header with a sandbox access token on every request. An expired or unknown token returns 403.
/api/Webhooks/Sandbox/EventsDiscovery lists the events you can trigger, one sample payload each, and the reason for anything unavailable. Ask this endpoint rather than hard-coding the list.
1
2
curl https://sandbox.apps.whcc.com/api/Webhooks/Sandbox/Events \
-H "Authorization: Bearer 726670514499"
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
[
{
"slug": "Shipped",
"available": true,
"sample": {
"ShippingInfo": [
{
"Carrier": "FedEx",
"ShipDate": "2026-01-15T10:30:00-06:00",
"TrackingNumber": "SYN-A1B2C3D4E5F6",
"TrackingUrl": "https://www.fedex.com/fedextrack/?trknbr=SYN-A1B2C3D4E5F6",
"Weight": 2.95
}
],
"OrderNumber": 12345678,
"Event": "Shipped",
"ConfirmationId": "11111111-1111-1111-1111-111111111111",
"EntryId": "sample-entry-id",
"Reference": "SAMPLE-REFERENCE",
"SequenceNumber": "1"
}
},
{
"slug": "Processed",
"available": true,
"sample": {
"Status": "Accepted",
"Errors": [],
"OrderNumber": 12345678,
"Event": "Processed",
"ConfirmationId": "11111111-1111-1111-1111-111111111111",
"EntryId": "sample-entry-id",
"Reference": "SAMPLE-REFERENCE",
"SequenceNumber": "1"
},
"variants": ["Accepted", "Rejected"]
},
{
"slug": "Order Cancelled",
"available": true,
"sample": {
"Event": "Order Cancelled",
"OrderNumber": 12345678,
"ConfirmationId": "11111111-1111-1111-1111-111111111111",
"EntryId": "sample-entry-id",
"Reference": "",
"SequenceNumber": "1"
}
},
{
"slug": "ImageDownload",
"available": false,
"reason": "No V2 payload exists for this event. Image failures are delivered as Processed with Status=Rejected.",
"seeAlso": "Processed"
}
]
The sample objects show the V2 payload shape, not a recording of a sandbox delivery. The values differ from what the sandbox sends you — see OrderNumber, the sandbox marker and generated tracking numbers below.
Each slug is spelled exactly as the V2 wire Event string. Send the slug in the request body, not in the URL.
| Slug | Delivers |
|---|---|
Shipped |
A shipment notification, with one ShippingInfo entry per shipment you ask for. |
Processed |
A status notification. Two variants: Accepted and Rejected. |
Order Cancelled |
A cancellation notification. |
Order Cancelled contains a space, and the match is case-sensitive. order cancelled, OrderCancelled and order.cancelled are all rejected:
1
2
400 {"error":"invalid_event",
"message":"\"event\" must be exactly one of: \"Shipped\", \"Processed\", \"Order Cancelled\"."}
ImageDownload is unavailableDiscovery lists ImageDownload with "available": false, and triggering it returns 400 event_unavailable. There is no V2 payload for that event, so there is nothing for the sandbox to send.
Image failures reach you as Processed with "Status": "Rejected" and a populated Errors[] array. That is the shape to code against. If you are working from the V1 format documentation, this is one of the places V2 differs.
/api/Webhooks/Sandbox/TriggerContent-type: application/json
The trigger queues one notification and returns 202 Accepted. Delivery happens on the next dispatch pass, a few moments later, exactly as it does for a real order.
ShippedShipped requires a non-empty shipments array. Each entry takes a carrier, an optional weight, and an optional ISO-8601 shipDate.
1
2
3
4
5
6
7
8
9
10
curl https://sandbox.apps.whcc.com/api/Webhooks/Sandbox/Trigger \
-H "Authorization: Bearer 726670514499" \
-H "Content-Type: application/json" \
-d '{
"event": "Shipped",
"shipments": [
{ "carrier": "FedEx", "weight": 2.95, "shipDate": "2026-01-15T10:30:00-06:00" }
]
}' \
-X POST
1
202 {"notifyUid":12203823,"event":"Shipped","orderNumber":906564705}
An empty or missing shipments array returns 400 shipments_required. A synthetic Shipped with no shipment rows cannot build a payload, so the trigger rejects it rather than queueing a notification that fails later.
Shipped events in quick successionA second Shipped raised while the first one is still waiting to be sent returns:
1
2
409 {"error":"shipped_dedup_pending",
"message":"A Shipped notification for this order is still unsent; shipment notifications collapse per order per batch. Retry shortly."}
This is expected, not a fault. Shipment notifications collapse to one per order per batch, so a second Shipped queued behind an unsent one would be discarded silently. The trigger returns 409 instead, so you can see it happen.
Notifications are picked up about once a minute. Two Shipped triggers a few seconds apart will usually hit this; the same two a minute apart will both succeed. If you are scripting a test that raises several Shipped events, either space them out or treat 409 shipped_dedup_pending as a signal to wait and retry.
Only Shipped behaves this way. Processed and Order Cancelled can be triggered repeatedly without any wait.
ProcessedProcessed takes an optional status. It defaults to Accepted. The value is matched case-insensitively, and anything other than Accepted or Rejected returns 400 invalid_status.
Accepted1
2
3
4
{
"event": "Processed",
"status": "Accepted"
}
RejectedRejected requires a non-empty errors array. Each entry takes an errorCode, an error message, and an assetPath.
1
2
3
4
5
6
7
8
9
10
11
{
"event": "Processed",
"status": "Rejected",
"errors": [
{
"errorCode": "400.13",
"error": "synthetic image failure",
"assetPath": "https://example.test/a.jpg"
}
]
}
That request delivers this payload to your callback URI:
1
2
3
4
5
{"Status":"Rejected",
"Errors":[{"ErrorCode":"400.13","Error":"synthetic image failure",
"AssetPath":"https://example.test/a.jpg"}],
"Event":"Processed","ConfirmationId":"…","EntryId":"SANDBOX-550",
"Reference":"SANDBOX-550","SequenceNumber":"1"}
Omitting errors on a Rejected returns 400 errors_required. Omitting errorCode on an entry returns 400 error_code_required.
Order CancelledOrder Cancelled takes no other fields.
1
2
3
{
"event": "Order Cancelled"
}
OrderNumber on sandbox payloadsSandbox payloads carry an OrderNumber, exactly as real webhooks do. The order WHCC seeds for you receives a synthetic order number in a reserved range, so it is always nine digits beginning with 9:
1
"OrderNumber": 906564705
Real order numbers are seven or eight digits, so a sandbox order number is recognisable on sight and can never be confused with one of your real orders. The number is stable — every trigger from the same credentials reports the same one, the way a real order number stays fixed for the life of an order.
One exception: Processed with "status": "Rejected" has no OrderNumber field. It is not null — it is absent.
This matches production. An image download fails during intake, before the order is assigned a number, so a real image-failure webhook has never carried one either. Handle the field as optional on that event.
Correlate a trigger with its delivery on notifyUid, not on the order number. Every sandbox trigger from the same credentials shares one order number, so it cannot tell two triggers apart. notifyUid is returned in the 202 and is unique per trigger.
Both EntryId and Reference read SANDBOX-<ConsumerUID> on every synthetic payload — SANDBOX-550 in the example above, where 550 is the consumer ID of the credentials that triggered it.
This is deliberate. It lets you tell a synthetic webhook from a real one at a glance, in a log or in a handler, without checking anything else.
WHCC generates the tracking number for every synthetic shipment. The format is SYN- followed by 12 uppercase hexadecimal characters:
1
"TrackingNumber": "SYN-078F1F4772F7"
That format matches no real carrier, so a sandbox tracking number can never collide with a real shipment. You cannot supply your own. The tracking and trackingUrl request fields were removed; sending them has no effect.
TrackingUrl is derived from the carrier you asked for:
1
"TrackingUrl": "https://www.fedex.com/fedextrack/?trknbr=SYN-078F1F4772F7"
The tracking link will not resolve at the carrier. The URL is well-formed and points at the real carrier site, but the number is synthetic, so the carrier has no shipment to show. This is expected. Use the link to test that your handler stores and renders it, not to track anything.
carrier valuescarrier is validated against five values:
UPSUSPSUPS MIFedExDHL GMInput is matched case-insensitively, and the payload carries the canonical spelling above rather than the casing you sent. Send fedex and the payload reads FedEx.
Anything else is rejected:
1
2
400 {"error":"invalid_carrier",
"message":"\"carrier\" must be one of: UPS, USPS, UPS MI, FedEx, DHL GM."}
errorCode is limited to 6 charactersReal values are six-character WHCC codes — 400.01, 400.02, 400.03, 400.08, 400.13, 400.17. See Errors for the full list.
A longer value is rejected. Sending an invented code such as IMG_DOWNLOAD_FAILED returns:
1
2
400 {"error":"invalid_error_code",
"message":"\"errorCode\" must be at most 6 characters; real values are six-character WHCC codes such as 400.13."}
Event Types documents the full V2 payload for each event.
Delivery, Retries & Idempotency covers the retry schedule, the response timeout, and why your handler must deduplicate.