Webhook Sandbox

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.

Sandbox environment only

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.

Before you trigger anything

The sandbox delivers through the real delivery path, so it needs everything the real path needs. Three conditions must hold before a trigger succeeds:

  1. You have registered a callback URI.
  2. You have verified that callback URI.
  3. Your credentials have a consumer secret, which WHCC uses to sign the request.

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.

GET /api/Webhooks/Sandbox/Events

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

Example Request

1
2
curl https://sandbox.apps.whcc.com/api/Webhooks/Sandbox/Events \
	-H "Authorization: Bearer 726670514499"
Copy to Clipboard

Example Response

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.

The three event slugs

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 unavailable

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

POST /api/Webhooks/Sandbox/Trigger

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

Trigger a Shipped

Shipped 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
Copy to Clipboard

Example Response

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.

Triggering two Shipped events in quick succession

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

Trigger a Processed

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

Accepted

1
2
3
4
{
	"event": "Processed",
	"status": "Accepted"
}

Rejected

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

Trigger an Order Cancelled

Order Cancelled takes no other fields.

1
2
3
{
	"event": "Order Cancelled"
}

OrderNumber on sandbox payloads

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

The sandbox marker

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.

Tracking numbers are generated, not supplied

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.

Accepted carrier values

carrier is validated against five values:

  • UPS
  • USPS
  • UPS MI
  • FedEx
  • DHL GM

Input 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 characters

Real 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."}

What's Next

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.

Back to Top 👆
Get in Touch

Interested in integrating with WHCC? Tell us more about what you’re looking for and we’ll be in touch.