Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.shoppex.io/llms.txt

Use this file to discover all available pages before exploring further.

What are Webhooks?

Webhooks are HTTP callbacks that notify your server when events happen in Shoppex — a paid order, a new subscription, a dispute. Instead of polling the API, your server receives events in real time.
This page covers normal Shoppex event webhooks. dynamic_webhook for DYNAMIC products is a separate fulfillment callback contract. See Dynamic Product Delivery.

Setting Up Webhooks

Configure webhook endpoints in the Dashboard:
1

Open webhook settings

Go to Settings → Webhooks
2

Add a new endpoint

Click Add Endpoint
3

Enter your URL

Enter your endpoint URL
4

Select events

Select which events to receive
For local development, use a tunnel service like ngrok to expose your local server.

Webhook Payload

All webhooks follow this structure:
{
  "event": "order:paid",
  "data": {
    "uniqid": "abc123def456",
    "type": "PRODUCT",
    "status": "COMPLETED",
    "gateway": "STRIPE",
    "total": 49.99,
    "total_display": 49.99,
    "currency": "USD",
    "exchange_rate": 1,
    "crypto_exchange_rate": 0,
    "crypto_gateway": null,
    "apm_method": "CARD",
    "customer_email": "customer@example.com"
  },
  "created_at": 1705318200
}
Dashboard test deliveries use the same event / data / created_at envelope as live deliveries. The payload values are synthetic examples, but Shoppex signs the raw JSON body the same way as a real delivery.
Top-level webhook created_at is a Unix timestamp. Nested timestamps inside data can be ISO 8601 strings.

Headers

Each webhook request includes these headers:
HeaderDescription
Content-Typeapplication/json
User-AgentShoppex-Webhook/1.0
X-Shoppex-EventEvent type (e.g., order:paid)
X-Shoppex-TimestampUnix timestamp in seconds used in the v2 signature
X-Shoppex-Signature-V2Timestamped HMAC-SHA256 signature: v1,t=<timestamp>,h=<hex>
X-Shoppex-SignatureDeprecated legacy HMAC-SHA512 body-only signature
X-Shoppex-DeliveryUnique delivery ID for deduplication

Signature Verification

Always verify webhook signatures to ensure authenticity. New integrations should verify X-Shoppex-Signature-V2. Shoppex signs ${deliveryId}.${timestamp}.${rawBody} with HMAC-SHA256 and rejects timestamps outside a 5-minute window.
import crypto from 'crypto';

function verifySignature(
  payload: string,
  signatureHeader: string,
  deliveryId: string,
  timestampHeader: string,
  secret: string,
): boolean {
  const parts = Object.fromEntries(signatureHeader.split(',').map((part) => {
    const [key, value] = part.trim().split('=');
    return [key, value ?? ''];
  }));
  if (!deliveryId || parts.v1 !== '' || parts.t !== timestampHeader || !parts.h) return false;

  const timestamp = Number(parts.t);
  if (!Number.isFinite(timestamp)) return false;
  if (Math.abs(Math.floor(Date.now() / 1000) - timestamp) > 300) return false;

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${deliveryId}.${parts.t}.${payload}`, 'utf8')
    .digest('hex');

  if (parts.h.length !== expected.length) return false;

  return crypto.timingSafeEqual(
    Buffer.from(parts.h, 'hex'),
    Buffer.from(expected, 'hex'),
  );
}

app.post('/webhooks/shoppex', (req, res) => {
  const signature = req.headers['x-shoppex-signature-v2'] as string;
  const deliveryId = req.headers['x-shoppex-delivery'] as string;
  const timestamp = req.headers['x-shoppex-timestamp'] as string;

  if (!verifySignature(req.rawBody, signature, deliveryId, timestamp, WEBHOOK_SECRET)) {
    return res.status(401).send('Invalid signature');
  }

  const { event, data } = req.body;

  switch (event) {
    case 'order:paid':
      handleOrderPaid(data);
      break;
    case 'order:cancelled':
      handleOrderCancelled(data);
      break;
    case 'subscription:created':
      handleSubscriptionCreated(data);
      break;
  }

  res.status(200).send('OK');
});
X-Shoppex-Signature and X-Shoppex-Unescaped-Signature are legacy body-only HMAC-SHA512 headers. They remain available during the migration period, but new integrations should use X-Shoppex-Signature-V2.
Your webhook secret is available in Settings → Webhooks in the Dashboard. Keep it secure and never expose it in client-side code.
Per-payment callbacks created with the webhook field on POST /dev/v1/payments are different from global webhook endpoints. Their signing secret is returned once as webhook_secret in the payment creation response.

Testing Webhooks

Use the dashboard to send test events:
1

Open webhook settings

Go to Settings → Webhooks
2

Select your endpoint

Click on your endpoint
3

Send a test event

Click Send Test Event
4

Choose an event type

Select an event type
Test order:* deliveries include the main live fields you usually integrate against, for example gateway, total, total_display, currency, exchange_rate, crypto_gateway, apm_method, customer_email, and product context.
For local development:
# Start a tunnel
ngrok http 3000

# Use the generated URL as your webhook endpoint
# e.g., https://abc123.ngrok.io/webhooks/shoppex

Retry Policy

If your endpoint returns an error (non-2xx status) or times out, Shoppex retries automatically:
DeliveryDelay
Initial attemptImmediate
Retry 12 minutes
Retry 24 minutes
Retry 38 minutes
Retry 416 minutes
After the 5th failed attempt, the webhook is marked as failed. You can manually retry from the dashboard.
Your endpoint must respond within 30 seconds or the request will timeout and count as a failure.

Best Practices

Process webhooks asynchronously. Return 200 OK immediately and handle the event in a background job.
Webhooks may be delivered more than once. Use the X-Shoppex-Delivery header to deduplicate.
Always verify webhook signatures in production to prevent spoofing.
Always use HTTPS endpoints in production for security.

Next Steps

Webhook Events

Full list of event types and payload examples

Dynamic Delivery

Deliver digital products in real-time