Skip to main content

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

EventDescription
order.startedOrder has been created and queued for processing
order.placedOrder was successfully placed with the retailer
order.failedOrder failed after all retry attempts were exhausted
order.tracking_receivedTracking number(s) were received from the retailer
order.deliveredAll packages on the order have been delivered
order.cancelledRetailer 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).
EventDescription
return.createdA return was filed against one of your orders
return.approvedA return was approved — typically accompanied by a merchant RMA / label URL
return.deniedA return was denied (e.g. outside the return window)
return.creditedA 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

FieldTypeDescription
eventstringThe event type (e.g., order.started, order.placed, return.approved)
order_idstringThe UUID of the order
return_idstring | nullThe UUID of the return request. Only populated on return.* events; null for order events.
statusstringCurrent status of the subject. For order events this is the order status; for return events it’s the return-request status.
timestampstring (ISO 8601)When the event occurred
dataobjectAdditional 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:
HeaderDescription
Content-TypeAlways application/json
X-Webhook-SignatureHMAC-SHA256 signature of the payload
X-Webhook-EventThe 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

  1. Respond quickly - Return a 2xx status code as soon as possible. Process the webhook asynchronously if needed.
  2. Handle duplicates - Webhooks may occasionally be delivered more than once. Use the order_id to deduplicate.
  3. Verify signatures - Always validate the X-Webhook-Signature header before trusting the payload.
  4. Use HTTPS - Configure an HTTPS endpoint to ensure webhook data is encrypted in transit.
  5. Log events - Keep records of received webhooks for debugging and auditing.