Build with the Vouchereto API
A small REST API for one job: taking a voucher, locking it to a customer and turning it into a balance on your platform.
- Base URL
- api.vouchereto.com/v1
- Test and live
- vch_test_… and vch_live_… keys
- Auth
- Bearer key + HMAC-SHA256
- Format
- JSON · amounts in minor units
Your first redemption
Get your keys
Create a test key in the merchant portal. You see the full key once; its last part is also the secret you sign requests with.
Reserve the voucher
Send the code, the currency and your customer reference. The voucher is locked to that customer and cannot be used anywhere else.
Credit, then capture
Credit the balance on your side, then capture. If the top-up fails, cancel instead and the voucher is released.
Listen to webhooks
Register an endpoint to receive signed events for captures, cancellations, expiries and checkouts.
POST /v1/redemptions
Authorization: Bearer vch_test_…
Idempotency-Key: topup-88213
Vouchereto-Signature: t=1790500000,v1=9c1e5f07…
Content-Type: application/json
{ "code": "7K2M-9QXR-4HTB-2WNZ", "customer_ref": "acct_48213", "currency": "USD" }
# 201 Created
{ "id": "5f0c2a7e-…", "object": "redemption", "status": "reserved",
"amount_minor": 5000, "currency": "USD", "voucher_last4": "2WNZ",
"customer_ref": "acct_48213", "livemode": false,
"expires_at": "2026-09-27T14:12:00Z", "captured_at": null }
# Credit the customer first, then capture. Irreversible.
POST /v1/redemptions/5f0c2a7e-…/capture
Authorization: Bearer vch_test_…
Idempotency-Key: topup-88213-capture
Vouchereto-Signature: t=1790500012,v1=4b7d2e91…
# 200 OK
{ "id": "5f0c2a7e-…", "object": "redemption", "status": "captured",
"captured_at": "2026-09-27T14:02:31Z", … }
# The top-up failed: release the voucher back to the customer
POST /v1/redemptions/5f0c2a7e-…/cancel
Authorization: Bearer vch_test_…
Idempotency-Key: topup-88213-cancel
Vouchereto-Signature: t=1790500015,v1=77aa01c3…
# 200 OK
{ "id": "5f0c2a7e-…", "object": "redemption", "status": "cancelled", … }
// Vouchereto-Signature: t=<unix>,v1=<hex HMAC-SHA256(secret, t + "." + rawBody)>
// secret = the whsec_… signing secret of your webhook endpoint
import crypto from "node:crypto";
export function verifyWebhook(rawBody, header, secret) {
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) > 300) return false;
const expected = crypto.createHmac("sha256", secret).update(`${t}.${rawBody}`).digest();
return sigs.some((s) => s.length === expected.length && crypto.timingSafeEqual(s, expected));
}
The redemption lifecycle
- capturedThe voucher is spent. Irreversible.
- cancelledYou cancelled it. The voucher is active again.
- reservation expiredNot captured in time (15 minutes by default). The voucher is active again.
What to build against
Signing requests
Every request carries a Vouchereto-Signature header: a timestamp and an HMAC-SHA256 of the timestamp, the method, the path and the body, keyed with your API key secret. We accept a clock skew of up to 300 seconds.
// Sign every request with the secret part of your API key (the text after the last "_")
const t = Math.floor(Date.now() / 1000);
const payload = `${t}.${method} ${pathWithQuery}\n${body}`;
const v1 = crypto.createHmac("sha256", keySecret).update(payload).digest("hex");
headers["Vouchereto-Signature"] = `t=${t},v1=${v1}`;
Idempotency
Every POST needs an Idempotency-Key header. Repeat a request with the same key and you get the original response back, marked Idempotent-Replayed: true. Keys are kept for at least 30 days.
Sandbox vouchers
With a test key, POST /v1/sandbox/vouchers creates a test voucher in the state you ask for (active, reserved, redeemed, expired, risk_hold or void) and returns its code. Test keys only ever see test data.
Webhook events
| Event | When it is sent |
|---|---|
| redemption.captured | A reserved voucher was captured. Safe to keep the credited balance. |
| redemption.cancelled | A reservation was cancelled. The voucher is active again. |
| redemption.expired | A reservation ran out of time without a capture. |
| checkout.completed | A hosted checkout was paid and the voucher applied. |
| checkout.expired | A hosted checkout session expired without payment. |
| checkout.failed | A paid checkout was reversed, for example after a refund. |
Errors
| Status | Code | What to tell the customer |
|---|---|---|
| 404 | VOUCHER_INVALID | We could not find that code. Check it and try again. |
| 409 | VOUCHER_ALREADY_REDEEMED | This voucher has already been used. |
| 409 | VOUCHER_RESERVED | This voucher is being used in another payment right now. |
| 422 | VOUCHER_EXPIRED | This voucher has expired. |
| 422 | UNIVERSAL_NOT_ACCEPTED | We do not accept universal vouchers. Use a voucher for this site. |
| 422 | CURRENCY_MISMATCH | This voucher is in a different currency. |
| 429 | TOO_MANY_INVALID_ATTEMPTS | Too many attempts. Wait a few minutes and try again. |
Errors follow RFC 7807 (application/problem+json) with a stable code and a request_id.
This page summarises the API. The full reference with every field is published at www.vouchereto.com/docs/reference.
Stuck on something?
Write to [DEVELOPER SUPPORT EMAIL]. A developer reads every message.
- OpenAPI spec
- Postman collection
- API status · [URL]
- Changelog