Warming up the decks…
Warming up the decks…
BlendPartner APIblendapp.aiOnce you have charged the buyer, your backend tells Blend “this order is paid”. Blend takes the ticket out of inventory, emails it to the buyer, and records that you owe Blend for it. Because this call moves money on paper, it is signed with a key only your server and Blend know, and it is safe to send twice.
Keyed, server-to-server only. Send x-blend-key, x-blend-secret and x-blend-signature. Rate limit: 60 requests per minute.
Confirm needs your secret and your signing key. It belongs on your backend, after your payment rail has reported a successful charge.
| Body parameter | Type | Description |
|---|---|---|
| orderId required | string | The order from the reserve response. |
| partnerReference required | string | Your own charge id, the reference your payment rail gave you. Stored with the order for reconciliation. |
| amount required | number | What you charged, in USD. Must match the order's total within 0.01. |
The x-blend-signature header is a hex-encoded HMAC-SHA256, computed with your signing key (csig_live_xxxx, delivered by email with your other keys) over exactly this string:
${orderId}.${amount.toFixed(2)}.USD.${partnerReference}
# Example, for an order of 42.5 USD with your charge id tp_8f21c:
# <orderId>.42.50.USD.tp_8f21c42.5 becomes 42.50).USD.., no whitespace, no trailing newline.import { createHmac } from 'node:crypto';
const BLEND_KEY = process.env.BLEND_KEY; // ck_live_xxxx
const BLEND_SECRET = process.env.BLEND_SECRET; // cs_live_xxxx
const BLEND_SIGNING_KEY = process.env.BLEND_SIGNING_KEY; // csig_live_xxxx
export async function confirmBlendOrder(orderId, amount, partnerReference) {
const payload = `${orderId}.${amount.toFixed(2)}.USD.${partnerReference}`;
const signature = createHmac('sha256', BLEND_SIGNING_KEY)
.update(payload)
.digest('hex');
const res = await fetch('https://api.blendapp.ai/api/v1/channel/orders/confirm', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-blend-key': BLEND_KEY,
'x-blend-secret': BLEND_SECRET,
'x-blend-signature': signature,
},
body: JSON.stringify({ orderId, partnerReference, amount }),
});
const body = await res.json();
if (res.status === 503) {
// ACCRUAL_FAILED_RETRY: safe to retry with the same body and signature.
throw new RetryableError(body.error?.code);
}
if (res.status === 409 && body.error?.code === 'CONFIRM_FAILED') {
// Inventory was taken between reserve and confirm. Refund the buyer.
await refundOnYourRail(partnerReference);
return { refunded: true };
}
if (!body.success) throw new Error(body.error?.code ?? 'confirm failed');
return body.data; // { orderId, status: 'paid', ticket: { . } }
}{
"success": true,
"data": {
"orderId": "…",
"status": "paid",
"ticket": {
"shortId": "…",
"ticketUrl": "https://…",
"attendeeName": "Nour Haddad",
"eventTitle": "…",
"ticketType": "…",
"seatLabels": []
}
}
}| Response field | Type | Description |
|---|---|---|
| data.status | string | Always paid on success. |
| data.ticket.shortId | string | The short, human-readable ticket id. |
| data.ticket.ticketUrl | string | A link to the ticket. Safe to show in your app. |
| data.ticket.attendeeName | string | The name on the ticket. |
| data.ticket.eventTitle | string | The event. |
| data.ticket.ticketType | string | The ticket type name. |
| data.ticket.seatLabels | string[] | Seat labels for reserved seating; empty for general admission. |
| Code | Type | Description |
|---|---|---|
| INVALID_REQUEST | 400 | A required field is missing or malformed. |
| SIGNING_KEY_NOT_CONFIGURED | 401 | Your account has no signing key yet. Contact Blend. |
| INVALID_SIGNATURE | 401 | x-blend-signature does not match. Check the signed string format, the two-decimal amount, and that you are using the current signing key. |
| NOT_PARTNER_REMIT | 403 | The order was not created in partner-remit mode, so there is nothing for you to confirm. |
| NOT_FOUND | 404 | No such order, or the order is not yours. |
| AMOUNT_MISMATCH | 409 | amount differs from the order total by more than 0.01. The response includes { serverTotal, currency }. Do not retry with the server total unless that is what you actually charged. |
| CONFIRM_FAILED | 409 | Blend could not claim inventory, the last ticket was taken between your reserve and this confirm. The response includes { orderId, refundRequired: true }. You must refund the buyer. |
| ACCRUAL_FAILED_RETRY | 503 | Blend could not write the settlement record. Nothing was issued. Safe to retry with the same body and signature. |
When you receive refundRequired: true, the buyer has been charged on your rail for a ticket Blend cannot issue. Refund automatically, from the same code path, and show the buyer that the event sold out. See when inventory is claimed for why this can happen.
Confirm is guarded by an atomic status transition on the order. If your retry logic sends the same confirm twice, or two workers race to send it, only the first transitions the order to paid. A duplicate does not issue a second ticket and does not send a second email. Retrying after a timeout is the right thing to do.
Keyed, server-to-server. Use it to reconcile, or to recover after a crash between charge and confirm. Rate limit: 300 requests per minute.
| Body parameter | Type | Description |
|---|---|---|
| orderId | string | Look up by Blend's order id. |
| externalId | string | Look up by your reference instead. Send one of orderId or externalId. |
curl -X POST https://api.blendapp.ai/api/v1/channel/orders/status \
-H "Content-Type: application/json" \
-H "x-blend-key: ck_live_xxxx" \
-H "x-blend-secret: cs_live_xxxx" \
-d '{ "orderId": "<orderId>" }'| Response field | Type | Description |
|---|---|---|
| status | "paid" | "pending" | "failed" | Where the order is. |
| paid | boolean | Shorthand for status === "paid". |
| ticket | object | Present once the order is paid. Same shape as the confirm response. |
Returns 404 NOT_FOUND if the order does not exist or is not yours.
Blend is the merchant of record for the ticket; you collected the money. Each confirmed order therefore writes a settlement record: an amount owed by you to Blend for that order. Those records are reconciled on the cycle agreed in your partner contract. The partnerReference you send on confirm is stored alongside the record so that both sides can match line by line.
If the record cannot be written you get 503 ACCRUAL_FAILED_RETRY and no ticket is issued. Retry.
Match Blend's settlement records against your charges by partnerReference.
Blend can rotate your signing key. Rotation invalidates the old key immediately, there is no overlap window, so confirms signed with the old key return 401 INVALID_SIGNATURE. Load the signing key from configuration at request time rather than at process start so a rotation does not require a redeploy.
ticketUrl, a link to the ticket, which you can show in your app immediately after confirm.