Documentation Index
Fetch the complete documentation index at: https://www.zinc.com/docs/llms.txt
Use this file to discover all available pages before exploring further.
Webhooks allow you to receive real-time HTTP notifications when events occur on your orders or returns. Instead of polling the API for updates, configure a webhook URL to receive automatic notifications.
Configuration
Configure your webhook URL in the Zinc dashboard under Settings. You can also generate a webhook secret for signature verification.
Webhook Secret
Your webhook secret is used to verify that incoming webhook requests are from Zinc. The secret format is:
zn_whsec_XXXXXXXXXXXXXXXXXXXXXXXX
Keep your webhook secret secure. If compromised, rotate it immediately in the dashboard. Rotating the secret invalidates the previous one.
Events
Order events
| Event | Description |
|---|
order.started | Order has been created and queued for processing |
order.placed | Order was successfully placed with the retailer |
order.failed | Order failed after all retry attempts were exhausted |
order.tracking_received | Tracking number(s) were received from the retailer |
order.delivered | All packages on the order have been delivered |
order.cancelled | Retailer cancelled the order after placement (your wallet is refunded automatically) |
Return events
Return events carry an additional return_id field alongside order_id so you can route by return without parsing the data object. The status field on these events reflects the return-request status (not the order status).
| Event | Description |
|---|
return.created | A return was filed against one of your orders |
return.approved | A return was approved — typically accompanied by a merchant RMA / label URL |
return.denied | A return was denied (e.g. outside the return window) |
return.credited | A return was resolved by crediting the customer’s wallet rather than working a merchant RMA |
Payload Structure
All webhook payloads follow this structure:
{
"event": "order.placed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "order_placed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {}
}
Payload Fields
| Field | Type | Description |
|---|
event | string | The event type (e.g., order.started, order.placed, return.approved) |
order_id | string | The UUID of the order |
return_id | string | null | The UUID of the return request. Only populated on return.* events; null for order events. |
status | string | Current status of the subject. For order events this is the order status; for return events it’s the return-request status. |
timestamp | string (ISO 8601) | When the event occurred |
data | object | Additional event-specific data |
Event-Specific Data
order.placed includes price components:
{
"event": "order.placed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "order_placed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"price_components": {
"subtotal": 1999,
"shipping": 499,
"tax": 150,
"total": 2648
}
}
}
order.failed includes error information:
{
"event": "order.failed",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "failed",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"error_type": "product_not_found",
"error": "The product is no longer available"
}
}
order.tracking_received includes the tracking numbers we received:
{
"event": "order.tracking_received",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "shipped",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"tracking_numbers": [
{
"carrier": "UPS",
"tracking_number": "1Z999AA10123456784"
}
]
}
}
order.delivered fires when every package on the order has been delivered:
{
"event": "order.delivered",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "delivered",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"tracking_numbers": [
{
"id": "tn_01HXYZ...",
"carrier": "UPS",
"tracking_number": "1Z999AA10123456784",
"delivered_at": "2026-01-15T13:42:00Z"
}
]
}
}
order.cancelled fires when the retailer cancels the order after placement. The order’s wallet hold is refunded at the same time:
{
"event": "order.cancelled",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "cancelled_by_retailer",
"timestamp": "2026-01-15T14:30:00Z",
"data": {
"reason": "cancelled_by_retailer",
"merchant_order_id": "112-1234567-1234567",
"refund_amount": 2648
}
}
return.created fires when a customer files a return against one of your orders. The data object echoes the return reason, free-text notes, and the line items being returned:
{
"event": "return.created",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"return_id": "a1b2c3d4-e29b-41d4-a716-446655440099",
"status": "open",
"timestamp": "2026-01-20T10:00:00Z",
"data": {
"reason": "damaged",
"notes": "Box arrived crushed",
"items": [
{ "order_item_id": "item-abc", "quantity": 1 }
]
}
}
return.approved fires when the return is approved. Where applicable, the data object carries the merchant-issued return id and a printable label URL so you can hand them to the customer:
{
"event": "return.approved",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"return_id": "a1b2c3d4-e29b-41d4-a716-446655440099",
"status": "approved",
"timestamp": "2026-01-20T14:30:00Z",
"data": {
"reason": "damaged",
"resolution_notes": "RMA filed with Amazon",
"merchant_return_id": "RMA-1234",
"external_label_url": "https://carrier.example.com/labels/abc.pdf"
}
}
return.denied fires when the return is denied (e.g. outside the retailer’s return window). merchant_return_id and external_label_url are null on denied returns:
{
"event": "return.denied",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"return_id": "a1b2c3d4-e29b-41d4-a716-446655440099",
"status": "denied",
"timestamp": "2026-01-20T14:30:00Z",
"data": {
"reason": "no_longer_needed",
"resolution_notes": "Outside return window",
"merchant_return_id": null,
"external_label_url": null
}
}
return.credited fires when the return is resolved by crediting the customer’s wallet rather than working a merchant RMA. There’s no carrier label or merchant return id in this flow:
{
"event": "return.credited",
"order_id": "550e8400-e29b-41d4-a716-446655440000",
"return_id": "a1b2c3d4-e29b-41d4-a716-446655440099",
"status": "credited",
"timestamp": "2026-01-20T14:30:00Z",
"data": {
"reason": "damaged",
"resolution_notes": "Item unrecoverable, no RMA needed",
"merchant_return_id": null,
"external_label_url": null
}
}
Security
Webhook requests include headers for verification:
| Header | Description |
|---|
Content-Type | Always application/json |
X-Webhook-Signature | HMAC-SHA256 signature of the payload |
X-Webhook-Event | The event type |
Verifying Signatures
To verify a webhook is from Zinc, compute the HMAC-SHA256 signature of the raw request body using your webhook secret and compare it to the X-Webhook-Signature header.
Python Example:
import hmac
import hashlib
def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
expected = hmac.new(
secret.encode(),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected, signature)
# In your webhook handler
@app.post("/webhook")
async def handle_webhook(request: Request):
payload = await request.body()
signature = request.headers.get("X-Webhook-Signature")
if not verify_webhook(payload, signature, WEBHOOK_SECRET):
raise HTTPException(status_code=401, detail="Invalid signature")
data = json.loads(payload)
# Process the webhook event
Node.js Example:
const crypto = require('crypto');
function verifyWebhook(payload, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expected),
Buffer.from(signature)
);
}
// In your webhook handler
app.post('/webhook', (req, res) => {
const signature = req.headers['x-webhook-signature'];
if (!verifyWebhook(req.rawBody, signature, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = req.body;
// Process the webhook event
});
Always verify webhook signatures before processing the payload to ensure the request originated from Zinc.
Best Practices
-
Respond quickly - Return a 2xx status code as soon as possible. Process the webhook asynchronously if needed.
-
Handle duplicates - Webhooks may occasionally be delivered more than once. Use the
order_id to deduplicate.
-
Verify signatures - Always validate the
X-Webhook-Signature header before trusting the payload.
-
Use HTTPS - Configure an HTTPS endpoint to ensure webhook data is encrypted in transit.
-
Log events - Keep records of received webhooks for debugging and auditing.