Five-minute integration
1. Get a key
Section titled “1. Get a key”Ask us for a partner key for your brand. It looks like bm_live_…, and it comes with
scopes:
| scope | lets you |
|---|---|
voucher.read | look up a voucher and read its sealed event chain |
voucher.redeem | reserve, redeem and release |
2. Install
Section titled “2. Install”npm install @bullmark/sdk3. Two calls
Section titled “3. Two calls”import { Bullmark } from '@bullmark/sdk';
const bm = new Bullmark({ apiKey: process.env.BULLMARK_API_KEY! });
// A customer gives you a code at checkout.const { voucher } = await bm.vouchers.inspect(code);
if (voucher.status !== 'Active') { throw new Error('that voucher cannot be used right now');}
// Charge them voucher.priceUsd instead of your list price, then:const receipt = await bm.vouchers.redeem({ code, externalRef: order.id, // YOUR order id});That is the whole integration. receipt.verifyUrl is a public link that proves the
redemption is on the chain — store it with the order if you like.
4. The one thing not to get wrong
Section titled “4. The one thing not to get wrong”externalRef is your own order or invoice reference, and it is required, because it is
the idempotency key.
// ✅ the SAME reference on every attemptawait retry(() => bm.vouchers.redeem({ code, externalRef: order.id }));
// ❌ a fresh reference per attempt — a timeout now costs your customer a second voucherawait retry(() => bm.vouchers.redeem({ code, externalRef: randomUUID() }));If your HTTP client times out and retries, the same reference returns the first result with
alreadyConfirmed: true. That is a success, not an error — do not surface it as one.
- Handle failed payments — reserving, releasing, and why
finally - Verify the seal yourself
- Partner API reference