Skip to content
Documentation menu

Vouchereto API · v1

Webhooks

We POST a signed JSON event to your endpoint when a redemption or checkout changes state. Use webhooks as the source of truth for crediting customers; redirects can be lost or forged.

Set up an endpoint

  • Merchant portal → Developers → Webhooks, in test mode first. Add a public https:// URL and pick the events.
  • Copy the whsec_… secret (shown once). Each endpoint has its own secret.
  • Up to 5 active endpoints per mode. Test endpoints receive test events only, live endpoints live events only.

Events

Webhook events
EventSent whenTypical action
redemption.capturedA redemption is captured (by you, or by the hosted checkout / widget with auto capture)Credit the customer (if you have not already on the API response)
redemption.cancelledA reservation is cancelledRelease anything you held for it
redemption.expiredA reservation lapsed without captureDo not credit; the voucher is usable again
checkout.completedA hosted checkout was paid and finished (with auto_redeem the voucher is already captured: see data.redemption_id)Credit the customer
checkout.expiredA checkout session passed its 60-minute window unpaidClean up the pending top-up
checkout.failedThe payment failedLet the customer try again

Payload

example delivery
POST /hooks/vouchereto HTTP/1.1
Content-Type: application/json
User-Agent: Vouchereto-Webhooks/1.0
Vouchereto-Event-Id: 7b0c3f9e-41d2-4c55-9a51-0d3e2a8f6b11
Vouchereto-Signature: t=1792663200,v1=0f4c…e91a

{
  "id": "7b0c3f9e-41d2-4c55-9a51-0d3e2a8f6b11",
  "type": "redemption.captured",
  "created_at": "2026-10-22T17:20:00.312Z",
  "livemode": false,
  "data": {
    "id": "f4e8b730-294e-4351-a85d-dc525dcadc18",
    "status": "captured",
    "amount_minor": 2500,
    "currency": "USD",
    "customer_ref": "user_8842",
    "voucher_last4": "2WNZ",
    "captured_at": "2026-10-22T17:20:00.298Z"
  }
}

Redemption events carry id, status, amount_minor, currency, customer_ref, voucher_last4, captured_at. Checkout events carry id, status, amount_minor, currency, customer_ref, redemption_id, metadata (the metadata you sent when creating the session).

Verify the signature

The header is Vouchereto-Signature: t=<unix>,v1=<hex> where v1 is HMAC-SHA256 with your whsec_… secret over t + "." + raw_body. Verify against the raw bytes before any JSON parsing, compare in constant time and reject timestamps older than 5 minutes.

Verify a webhook
// Express: verify before parsing. The signature covers the RAW body.
import express from 'express';
import { createHmac, timingSafeEqual } from 'node:crypto';

function verifyWebhook(rawBody, header, secret, tolerance = 300) {
  const parts = (header ?? '').split(',').map((p) => p.trim().split('='));
  const t = Number(parts.find(([k]) => k === 't')?.[1]);
  const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => Buffer.from(v ?? '', 'hex'));
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > tolerance) throw new Error('stale or malformed signature');
  const expected = createHmac('sha256', secret).update(`${t}.${rawBody}`).digest();
  if (!sigs.some((s) => s.length === expected.length && timingSafeEqual(s, expected))) throw new Error('bad signature');
  return JSON.parse(rawBody);
}

const app = express();
app.post('/hooks/vouchereto', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try { event = verifyWebhook(req.body.toString('utf8'), req.get('Vouchereto-Signature'), process.env.VOUCHERETO_WEBHOOK_SECRET); }
  catch { return res.sendStatus(400); }
  // Deliveries can repeat: skip events you already processed (event.id === Vouchereto-Event-Id header)
  // if (await seen(event.id)) return res.sendStatus(200);
  if (event.type === 'redemption.captured') { /* credit event.data.customer_ref with event.data.amount_minor */ }
  res.sendStatus(200);                       // answer within 10 s; do slow work in a queue
});
Using the Node SDK? verifyWebhook(rawBody, header, secret) does all of this and returns the typed event. SDK

Delivery rules

Delivery rules
SuccessAny 2xx within 10 seconds. Redirects are not followed.
Retries1 min, 5 min, 30 min, 2 h, 6 h, 12 h, 24 h, 24 h after each failure (about 3 days), then the delivery is marked dead.
DuplicatesPossible (a timeout after you processed it, a manual resend). Deduplicate on id / the Vouchereto-Event-Id header.
OrderNot guaranteed across events. Use data.status and timestamps, or read the object with the API.
ResendPortal → Developers → Webhooks → deliveries: see every attempt with the response code and resend any delivery.
User agentVouchereto-Webhooks/1.0

Answer fast and do slow work in a queue. Since an event can arrive before your own API call returns, make crediting idempotent on the redemption id.