> Part of [WHCC Developer Documentation](https://www.whcc.com/developer/llms.txt)

# 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](https://www.whcc.com/developer/docs/order-submit-api/index.html.md) 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:

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](https://www.whcc.com/developer/docs/order-submit-api/request-access-token/index.html.md) 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

```shell
curl https://sandbox.apps.whcc.com/api/Webhooks/Sandbox/Events \
	-H "Authorization: Bearer 726670514499"
```

### Example Response

```json
[
  {
    "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 &mdash; see [`OrderNumber`](https://www.whcc.com/#ordernumber), [the sandbox marker](https://www.whcc.com/#marker) and [generated tracking numbers](https://www.whcc.com/#tracking) 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:
>
>
> ```json
> 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](https://www.whcc.com/developer/docs/order-submit-api/webhook-v1-format/index.html.md) 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`.

```shell
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
```

#### Example Response

```json
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:

```json
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`

```json
{
	"event": "Processed",
	"status": "Accepted"
}
```

#### `Rejected`

`Rejected` requires a non-empty `errors` array. Each entry takes an `errorCode`, an `error` message, and an `assetPath`.

```json
{
	"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:

```json
{"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.

```json
{
	"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`:

```json
"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 &mdash; 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` &mdash; 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 &mdash; `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:

```json
"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:

```json
"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:

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:

```json
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 &mdash; `400.01`, `400.02`, `400.03`, `400.08`, `400.13`, `400.17`. See [Errors](https://www.whcc.com/developer/docs/order-submit-api/errors/index.html.md) for the full list.

A longer value is rejected. Sending an invented code such as `IMG_DOWNLOAD_FAILED` returns:

```json
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](https://www.whcc.com/developer/docs/order-submit-api/webhook-types/index.html.md) documents the full V2 payload for each event.
>
> [Delivery, Retries & Idempotency](https://www.whcc.com/developer/docs/order-submit-api/webhook-delivery/index.html.md) covers the retry schedule, the response timeout, and why your handler must deduplicate.