Accept Hyperspend vouchers at your checkout or as an account top-up. The customer enters their 16-digit voucher PIN, the voucher is redeemed, and you credit its full value to the customer.
There are two ways to integrate:
Embedded checkout (recommended): the Hyperspend box sits inside your own checkout page. The customer never leaves your site, and you never see or store PINs. About an hour of work: one API call on your server, two lines on your page, and optionally a webhook so your server hears about every payment directly.
Direct API: you build the PIN field yourself and call check and redeem from your server. Full control over the look, more work, and PINs pass through your systems.
Base URL:
https://hyperspend.io/api/v1Format: JSON request and response bodies, UTF-8
Authentication:
Authorization: Bearer <your API key>
API keys and sandbox
There are two kinds of key. Create and revoke them yourself in the merchant portal (API keys); we give you portal access during onboarding.
| Key | Starts with | What it does |
|---|---|---|
| Test | hs_test_ |
Works with the sandbox test PINs below. Never touches real vouchers. |
| Live | hs_live_ |
Redeems real customer vouchers. Available once we've approved your integration and switched your account to live. |
A key is shown once, when it's created. Keep keys on your server. Never put them in a browser, app or public repository. If a key may have leaked, revoke it in the portal straight away (or contact us) and create a new one.
The portal also shows your redemptions and monthly statements, with a CSV download.
Sandbox test PINs (test keys only)
| PIN | Result |
|---|---|
4000 0000 0000 0001 |
Active, EUR 50.00: redeems successfully |
4000 0000 0000 0019 |
Active, EUR 250.00: redeems successfully |
4000 0000 0000 0027 |
Already redeemed |
4000 0000 0000 0035 |
Expired |
4000 0000 0000 0043 |
Blocked |
| any other PIN | Not found |
Sandbox redemptions aren't stored: the same active test PIN can be redeemed again and again.
Vouchers are single use
A voucher is redeemed once, for its full value. If the customer spends less than the voucher amount, credit the remainder to them (for example as account balance). Don't charge a fee for paying with Hyperspend unless your merchant agreement allows it.
Embedded checkout
1. Open a session (your server)
When the customer reaches your checkout, open a session with your API key. Sessions are valid for 30 minutes and can be paid once.
POST /api/v1/checkout/sessions
Authorization: Bearer hs_test_xxx
Content-Type: application/json
Idempotency-Key: order-12345
{ "reference": "order-12345" }
201 Created:
{
"id": "cs_test_9f2c4b1a7e8d4c3b9a0f1e2d3c4b5a69",
"object": "checkout_session",
"status": "open",
"reference": "order-12345",
"payment": null,
"expires_at": "2026-10-16T13:04:56.789Z",
"created_at": "2026-10-16T12:34:56.789Z",
"test": true
}
reference(optional): your order or customer id, up to 100 characters. It's stored with the redemption and appears on your statements.Idempotency-Key(optional): retrying with the same key returns the same session.
2. Show the box (your page)
Pass the session id to your page and mount the box where the payment method goes:
<div id="hyperspend"></div>
<script src="https://hyperspend.io/embed.js"></script>
<script>
Hyperspend.mount("#hyperspend", {
session: "cs_test_9f2c4b1a7e8d4c3b9a0f1e2d3c4b5a69",
onPaid: function (payment) {
// payment = { redemption_id, amount, currency, reference }
// Ask your server to confirm (step 3), then continue your checkout.
},
onError: function (error) {
// error = { message }: the session expired or can't be used. Open a new one.
}
});
</script>
The box asks for the PIN, shows the voucher's value, and redeems it when the customer confirms. It sizes itself to fit and works on phones. The same events are also dispatched on the element as hyperspend:paid and hyperspend:error DOM events. A paid session stays paid: if the page reloads, the box shows "Paid" and fires onPaid again.
Where it works: boxes for test sessions work on any site, including localhost, so you can build freely. Boxes for live sessions only load on the website domains registered for your account (and their subdomains). Tell us if you add a domain.
3. Confirm the payment (your server)
onPaid runs in the browser, so it's not proof of payment. Before you credit the customer, confirm on your server:
GET /api/v1/checkout/sessions/cs_test_9f2c4b1a7e8d4c3b9a0f1e2d3c4b5a69
Authorization: Bearer hs_test_xxx
{
"id": "cs_test_9f2c4b1a7e8d4c3b9a0f1e2d3c4b5a69",
"object": "checkout_session",
"status": "paid",
"reference": "order-12345",
"payment": { "redemption_id": "rdm_test_…", "amount": "50.00", "currency": "EUR", "paid_at": "2026-10-16T12:35:41.002Z" },
"expires_at": "2026-10-16T13:04:56.789Z",
"created_at": "2026-10-16T12:34:56.789Z",
"test": true
}
Only when status is paid: credit payment.amount in payment.currency to the customer, once per session (store the session id with your order). status is open, paid or expired. If the customer closed the page before your checkout continued, you still find out: set up webhooks and we tell your server directly.
The box protects itself against PIN guessing: 5 wrong PINs lock a session, and there are per-customer limits. Test sessions only accept the sandbox PINs above.
Webhooks
Webhooks tell your server about a payment directly, server to server, so you can credit the order even if the customer closed the page right after paying. Set them up in the merchant portal under Webhooks:
- Enter your endpoint URL (must be
https://). We show your signing secret (whsec_…) once: store it on your server like an API key. - Press Send test event to check your endpoint. The portal shows the result of every event we send, and lets you send one again.
One endpoint receives events for both test and live sessions; check test in the event.
The event
When a session is paid, we POST this JSON to your endpoint. data is the same checkout session object as GET /api/v1/checkout/sessions/{id} returns:
POST /your/webhook/path
Content-Type: application/json
Hyperspend-Signature: t=1792166141,v1=5f8e0c…
Hyperspend-Event-Id: evt_7824e4568e984d919866aa2b0e4a1645
Hyperspend-Event-Type: checkout_session.paid
{
"id": "evt_7824e4568e984d919866aa2b0e4a1645",
"object": "event",
"type": "checkout_session.paid",
"created_at": "2026-10-16T12:35:41.120Z",
"test": true,
"data": {
"id": "cs_test_9f2c4b1a7e8d4c3b9a0f1e2d3c4b5a69",
"object": "checkout_session",
"status": "paid",
"reference": "order-12345",
"payment": { "redemption_id": "rdm_test_…", "amount": "50.00", "currency": "EUR", "paid_at": "2026-10-16T12:35:41.002Z" },
"expires_at": "2026-10-16T13:04:56.789Z",
"created_at": "2026-10-16T12:34:56.789Z",
"test": true
}
}
Answer with any 2xx status within 10 seconds; do the slow work after answering. Anything else (an error status, a timeout, a redirect) counts as failed, and we retry after 5 minutes, 15 minutes, 1, 3, 6, 12 and 24 hours, then give up (about 2 days in all). You can send a failed event again from the portal.
Handle each session once. You may get an event more than once, and the webhook and your own GET from onPaid can arrive in either order. Credit the order only if you haven't already for that session id. The test event from the portal is a sample with "test": true for a session that doesn't exist.
Check the signature
Every event is signed with your signing secret. Before trusting it:
- Take
tandv1from theHyperspend-Signatureheader. - Compute HMAC-SHA256 with your signing secret as the key over
`${t}.${raw request body}`(the exact bytes you received, before parsing the JSON), as lowercase hex. - Compare it with
v1in constant time, and reject events wheretis more than 5 minutes from your clock.
Node.js (Express):
const crypto = require("crypto");
const express = require("express");
const app = express();
app.post("/hyperspend/webhook", express.raw({ type: "application/json" }), (req, res) => {
const header = req.get("Hyperspend-Signature") || "";
const parts = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = crypto
.createHmac("sha256", process.env.HYPERSPEND_WEBHOOK_SECRET)
.update(`${parts.t}.${req.body}`)
.digest("hex");
const valid =
parts.v1 && parts.v1.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(parts.v1), Buffer.from(expected)) &&
Math.abs(Date.now() / 1000 - Number(parts.t)) < 300;
if (!valid) return res.status(400).send("Bad signature");
const event = JSON.parse(req.body);
if (event.type === "checkout_session.paid") {
// Credit the order for event.data.id (reference: event.data.reference) once.
}
res.sendStatus(200);
});
PHP:
<?php
$body = file_get_contents('php://input');
$header = $_SERVER['HTTP_HYPERSPEND_SIGNATURE'] ?? '';
parse_str(str_replace(',', '&', $header), $parts);
$expected = hash_hmac('sha256', ($parts['t'] ?? '') . '.' . $body, getenv('HYPERSPEND_WEBHOOK_SECRET'));
if (!hash_equals($expected, $parts['v1'] ?? '') || abs(time() - (int) ($parts['t'] ?? 0)) > 300) {
http_response_code(400);
exit('Bad signature');
}
$event = json_decode($body, true);
if ($event['type'] === 'checkout_session.paid') {
// Credit the order for $event['data']['id'] (reference: $event['data']['reference']) once.
}
http_response_code(200);
New secret: if your secret may have leaked, press New signing secret in the portal. The old one stops working at once, so update your server straight after (events that fail in between are retried).
Direct API
1. Check a voucher (optional)
Check that a PIN is valid and see its value, without redeeming it. Useful to show the amount before the customer confirms.
POST /api/v1/vouchers/check
Authorization: Bearer hs_test_xxx
Content-Type: application/json
{ "pin": "4000 0000 0000 0001" }
{
"redeemable": true,
"status": "active",
"amount": "50.00",
"currency": "EUR",
"expires_at": "2099-12-31T23:59:59.000Z",
"test": true
}
status is one of active, redeemed, expired, blocked, refunded, not_found. The amount is only returned for active vouchers.
2. Redeem a voucher
POST /api/v1/vouchers/redeem
Authorization: Bearer hs_test_xxx
Content-Type: application/json
Idempotency-Key: 7f3c9a1e-your-unique-attempt-id
{ "pin": "4000 0000 0000 0001", "reference": "your-order-12345" }
Success, 201 Created:
{
"id": "rdm_8c1f0d2e9b3a4c5d6e7f8a9b0c1d2e3f",
"status": "redeemed",
"amount": "50.00",
"currency": "EUR",
"reference": "your-order-12345",
"created_at": "2026-10-16T12:34:56.789Z",
"test": true
}
Only treat the voucher as paid when you receive this response. Then credit amount in currency to the customer, and store id with your order.
reference(optional): your own order or transaction id, up to 100 characters. It appears on your statements.Idempotency-Key(strongly recommended): a unique value per redemption attempt, e.g. a UUID or your order id. If a request times out, retry it with the same key: you get the original result (200 OK) and the voucher is never redeemed twice. Use a new key for every new redemption.
Errors
{ "error": { "code": "voucher_already_redeemed", "message": "This voucher has already been redeemed." } }
| HTTP | code |
Meaning |
|---|---|---|
| 400 | invalid_pin |
The PIN isn't 16 digits |
| 401 | unauthorized |
Missing, invalid or revoked API key |
| 403 | voucher_blocked |
Blocked or refunded voucher |
| 404 | voucher_not_found |
No voucher with this PIN |
| 409 | voucher_already_redeemed |
Already used |
| 410 | voucher_expired |
Past its expiry date |
| 429 | rate_limited |
More than 120 requests per minute |
| 429 | too_many_invalid_pins |
Too many unknown PINs from your key in the last hour (anti-guessing). Wait, or contact us. |
| 500 | server_error |
Our side. Safe to retry with the same Idempotency-Key. |
Show the customer a simple message, e.g. "This voucher can't be used. Check the PIN, or contact Hyperspend support."
3. Look up a redemption
GET /api/v1/redemptions/rdm_8c1f0d2e9b3a4c5d6e7f8a9b0c1d2e3f
Authorization: Bearer hs_live_xxx
Returns the same object as a successful redeem. Only your own live redemptions can be looked up.
Example (curl)
curl -X POST https://hyperspend.io/api/v1/vouchers/redeem \
-H "Authorization: Bearer hs_test_xxx" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-12345" \
-d '{"pin":"4000000000000001","reference":"order-12345"}'
Going live
- Build and test with your test key and the sandbox PINs, including the error cases, and send a test webhook event if you use webhooks. See the embedded checkout working at hyperspend.io/demo-shop/embedded.
- Run a test redemption together with the Hyperspend team.
- We set your account live and list your site on hyperspend.io/check-merchant. Then create your live key in the merchant portal.
Questions: support@bigedirect.com