Webhooks
Receive events on your server: endpoint registration, HMAC signature verification, and the retry policy.
Webhooks notify your server when a resource changes, such as a label purchase, tracking update, or wallet movement. Register an HTTPS endpoint and verify each delivery’s signature.
Register an endpoint
Body parameters
urlstring
HTTPS with a direct response. Redirects are not followed, and a 3xx counts as a failure.
stringeventsstring[]
The event types this endpoint subscribes to (see the table below).
string[]description?string
Optional name that identifies the destination.
stringcurl -X POST https://api.sendit.mx/v1/webhook-endpoints \
-H "X-API-Key: sk_live_..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://mitienda.mx/webhooks/sendit",
"description": "Production receiver",
"events": ["shipment.label.created", "shipment.tracking.updated"]
}'
{
"success": true,
"data": {
"id": "whe_a1b2c3d4e5f6",
"url": "https://mitienda.mx/webhooks/sendit",
"description": "Production receiver",
"events": ["shipment.label.created", "shipment.tracking.updated"],
"status": "ACTIVE",
"failureCount": 0,
"disabledAt": null,
"createdAt": "2026-07-18T10:00:00.000Z",
"signingSecret": "whsec_9f8e7d6c5b4a"
}
}
The field is named signingSecret. SendIt generates it and returns it only when you create or rotate the endpoint. Store it in a secrets manager because you cannot retrieve it again. The endpoint list never includes it.
Each plan caps how many active endpoints you can have. Registering one more returns 403 PLAN_LIMIT_REACHED. See plans and quotas.
Manage your endpoints
| Method | Path | Description |
|---|---|---|
GET |
/v1/webhook-endpoints |
List your endpoints |
GET/PATCH/DELETE |
/v1/webhook-endpoints/:id |
Read, update, or delete one |
POST |
/v1/webhook-endpoints/:id/rotate-secret |
Issue a new secret (returned once) |
POST |
/v1/webhook-endpoints/:id/test |
Send a webhook.test event immediately |
GET |
/v1/webhook-endpoints/:id/events |
Delivery history for that endpoint |
POST |
/v1/webhook-endpoints/:id/events/:eventId/redeliver |
Retry one delivery |
Any authenticated member can read. Writes require the ADMIN role and the webhooks:write scope.
Event types
You can subscribe to these documented events:
| Event | When it fires |
|---|---|
shipment.created |
A shipment was created |
shipment.updated |
A DRAFT shipment was updated |
shipment.label.created |
A label was purchased |
label.purchase.completed |
A durable purchase finished: success, compensated failure, or action_required |
shipment.label.voided |
A label was voided and the wallet refunded |
shipment.tracking.updated |
Carrier tracking caused a status transition |
wallet.credited |
A wallet credit was confirmed |
wallet.debited |
A label-purchase debit was confirmed |
wallet.low_balance |
A LIVE debit crossed the configured threshold |
tracker.created |
An external tracker was registered |
tracker.updated |
An external tracker changed status |
tracker.expired |
An external tracker hit its TTL without a terminal status |
Subscribe each endpoint only to the events it cares about. Your handler must ignore types it does not recognize.
The payload
{
"id": "evt_1a2b3c4d5e",
"type": "shipment.tracking.updated",
"created": "2026-07-17T12:00:00.000Z",
"livemode": true,
"data": {
"object": {
"id": "clxq1w2e3r4t5y6u7i8o9p0a",
"status": "IN_TRANSIT",
"trackingNumber": "1234567890",
"carrierCode": "DHL",
"organizationId": "clxorg123"
}
}
}
createdis an ISO-8601 timestamp, not epoch seconds.data.objectis the full resource projection, not a flat subset. Shipment events embed theirfromAddress,toAddress, andlabelprojections.- There is no top-level
apiVersionororganizationId. A shipment’sdata.objectdoes carry itsorganizationId; a tracker’s does not. livemode: falsemarks test-mode events. Route them to your staging.idis unique per event. Use it to deduplicate if you receive a repeated delivery.
Verify the signature
Every delivery arrives signed with HMAC-SHA256 in the X-SendIt-Signature header, formatted t=<timestamp>,v1=<signature>:
POST /webhooks/sendit HTTP/1.1
X-SendIt-Signature: t=1752750000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
Content-Type: application/json
Always verify before processing. The signature is the only proof the event came from SendIt:
import crypto from "node:crypto";
function verifySenditSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
const parts = Object.fromEntries(
signatureHeader.split(",").map((kv) => kv.split("="))
);
const { t, v1 } = parts;
// 1. Reject stale timestamps (replay protection)
if (Math.abs(Date.now() / 1000 - Number(t)) > toleranceSeconds) return false;
// 2. Recompute the signature over `${t}.${rawBody}`
const expected = crypto
.createHmac("sha256", secret)
.update(`${t}.${rawBody}`)
.digest("hex");
// 3. Constant-time compare
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(v1));
}
Respond fast, process later
Return 2xx as soon as you have persisted the event, ideally under a second. Process in the background. Anything other than 2xx counts as a failure, including redirects.
Retry policy
| Attempt | Wait |
|---|---|
| 1 | Immediate |
| 2 | 2 min |
| 3 | 4 min |
| 4 | 8 min |
| 5 | 16 min |
After the fifth attempt the event becomes EXHAUSTED and is not retried again.
An endpoint that keeps failing for 24 hours is automatically disabled. When that happens, we email your organization’s admins. To re-enable it, fix your server and send a test delivery with POST /v1/webhook-endpoints/:id/test. A successful test re-enables the endpoint.
Since deliveries can repeat, make your processing idempotent using the event id.
Try it risk-free
In test mode, every status advance fires real webhooks with livemode: false. Buy a test label, advance its status, and watch the deliveries arrive at your endpoint.
Handle errors
| Code | When it happens | How to resolve it |
|---|---|---|
400 INVALID_INPUT |
The URL or an event type is invalid | Use HTTPS and a documented event |
403 INSUFFICIENT_SCOPE |
The key lacks webhooks:write for a write |
Issue a key with the required scope |
403 PLAN_LIMIT_REACHED |
The organization reached its endpoint limit | Delete an unused endpoint or change plans |
404 RESOURCE_NOT_FOUND |
The endpoint or event does not exist | Check the identifiers |