# Receive trusted payment events

Verify the sender, reject replays, and handle duplicate deliveries safely.

## Register your endpoint

In Dashboard → Webhooks, create a public HTTPS endpoint in the same environment as your payments and subscribe to payment.succeeded. Store its signing secret as VEYQO_WEBHOOK_SECRET on your server.

The API key and webhook signing secret are different secrets. Verify incoming deliveries with the webhook endpoint’s signing secret.

## Verify the raw request body

Veyqo-Signature is a hexadecimal HMAC-SHA256 digest of {timestamp}.{raw body}. Veyqo-Timestamp provides the timestamp. During secret rotation, the signature header contains comma-separated signatures; accept a match with your configured secret. Verify in constant time and reject timestamps outside a five-minute window.

The following Express example shows signature verification and the boundary where you must add your own durable event persistence. It is not a complete order processing service.

```
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

const app = express();
// Register BEFORE app.use(express.json()); signatures need the original bytes.
app.post('/webhooks/veyqo', express.raw({ type: 'application/json' }), async (req, res) => {
  const signature = req.get('Veyqo-Signature') ?? '';
  const timestamp = req.get('Veyqo-Timestamp') ?? '';
  const secret = process.env.VEYQO_WEBHOOK_SECRET;
  if (!secret) return res.sendStatus(503);
  if (!/^\d+$/.test(timestamp) ||
      Math.abs(Date.now() / 1000 - Number(timestamp)) > 300 ||
      !signature) return res.sendStatus(401);

  const expected = createHmac('sha256', secret)
    .update(timestamp + '.').update(req.body).digest();
  // During secret rotation, accept any matching comma-separated signature.
  const valid = signature.split(',').map(value => value.trim()).some(value =>
    /^[a-f0-9]{64}$/i.test(value) &&
    timingSafeEqual(expected, Buffer.from(value, 'hex'))
  );
  if (!valid) {
    return res.sendStatus(401);
  }

  let event;
  try { event = JSON.parse(req.body.toString('utf8')); }
  catch { return res.sendStatus(400); }
  // Implement this with a durable queue/outbox and UNIQUE(event.id).
  // Acknowledge only after durable storage; retry storage failures with 5xx.
  try {
    await persistEventOnce(event);
    return res.sendStatus(200);
  } catch {
    return res.sendStatus(503);
  }
});

// Your worker handles payment.succeeded. Resolve its payment_id via the API,
// match status, reference, amount and currency to the stored order, then
// atomically mark the order paid once. persistEventOnce is your app's function.
```

## Process once, even when delivered again

1. Verify the signature and timestamp before trusting the payload.
2. Persist the event durably with a unique constraint on event.id. A duplicate delivery should receive 2xx without repeating work.
3. Acknowledge promptly after storage and process the order in a worker.
4. For payment.succeeded, resolve data.payment_id and verify the payment against your order’s reference, amount and currency.
5. Update the order atomically so repeated events cannot fulfil it twice.

> Return a non-2xx response if you could not persist the event. Returning 200 before durable storage can lose an event if the process stops.

## Inspect and recover deliveries

Use Events and Deliveries in the dashboard to investigate failures and retry a delivery. Veyqo retries failed deliveries with backoff. Your handler must tolerate duplicates and must not depend on event arrival order.

The events API also lets your server read normalized events using events:read, so missed notifications can be reconciled.
