Warming up the decks…
Warming up the decks…
BlendPartner APIblendapp.aiTwo buyers can tap “Buy” on the last ticket at the same instant. Only one of them can get it. This page explains how Blend makes that decision, for events with a simple ticket count and for events with a seat map, and, the part integrators most often get wrong, exactly when a ticket stops being available to everyone else.
For paid orders, Blend claims inventory when the payment is confirmed, not when the order is reserved. Reserve tells you the price and gives you an orderId; confirm is the moment the ticket is actually taken. Read when inventory is claimed before designing your payment flow.
A general admission (GA) ticket type has three counters: availableCount (the capacity; 0 means unlimited), soldCount, and heldCount (stock the organiser has held back and is not selling). A claim for quantity tickets succeeds only when:
soldCount + quantity <= availableCount - heldCount
# availableCount of 0 means unlimited: the guard is skipped.The important property is not the arithmetic; it is where it runs. The guard is part of the query of a single MongoDB findOneAndUpdate. The database evaluates the condition and applies the increment as one operation. There is no moment where Blend reads the count, decides, and then writes, no read-then-write window in which a second buyer can slip through.
If two buyers race for the last ticket, the database serialises the two updates. The first one satisfies the guard and increments soldCount. The second one is evaluated against the new value, fails the guard, and matches nothing. It cannot “also win”.
An order can hold up to 50 lines (for example three Standard and two VIP). Lines are claimed one at a time, each with its own atomic guard. If any line fails, every line already claimed for that order is rolled back, and the order fails as a whole. You never end up with a buyer who got their Standard tickets but not their VIP ones.
For on-sales that expect a large burst, Blend can put the counters in Redis ahead of MongoDB. Each ticket type has a key of the form inv:{eventId}:t:<ticketId> holding { cap, rem }, capacity and remaining. Every line in an order is decremented in one Lua script, which is all-or-nothing across the whole order: either every line had enough remaining and all of them are decremented, or nothing changes.
| Property | Type | Description |
|---|---|---|
| Key | string | inv:{eventId}:t:<ticketId>, holding cap and rem. |
| Atomicity | Lua script | All lines of the order succeed or fail together, in one script execution. |
| TTL | 30 days | Refreshed on each claim, so an event that keeps selling never loses its counters. |
| Fallback | fail open | If Redis is unavailable, or a key was never seeded for that ticket type, the claim goes to the MongoDB path described above. |
“Fail open” here means falling back to the slower path, not skipping the check. The MongoDB guard is always there underneath. Redis only makes the answer faster; it is never the sole line of defence.
This is the counterintuitive part, so it is stated plainly: for a paid order, the counters above are not touched when you reserve. They are touched when you confirm.
Blend validates the cart, prices it, and creates a pending order with an orderId and an amount. Nothing has been taken from the event's counts yet.
Toters Pay, or whatever you use. Blend is not involved in this step.
Your backend calls POST /v1/channel/orders/confirm. This is when Blend runs the atomic claim. If it succeeds, the order becomes paid and the ticket is issued.
That is a genuine race: someone else's confirm took the last ticket between your reserve and your confirm. Blend cancels the registration and the payment must be refunded. Because you took the payment, the refund is yours to make; the confirm endpoint tells you so explicitly with 409 CONFIRM_FAILED and refundRequired: true.
Design for this. Keep the time between reserve and confirm short, and make sure the code path that handles a confirm failure can issue a refund without a human in the loop.
Events with a seat map work differently. A specific seat cannot be “counted”; it is either free, held by someone who is deciding, or booked. While a buyer is choosing seats, Blend places a hold on them so nobody else can pick the same seat in the meantime.
| Value | Type | Description |
|---|---|---|
| Total hold lifetime | 720 s | 600 seconds shown to the buyer plus 120 seconds of hidden grace, so a buyer who taps Pay at 9:59 is not cut off by a slow network. |
| expiresInSeconds | 600 | What the client is told, and what a countdown should display. Do not show the grace period. |
| One-time extension | 600 s | Available once per hold, for a payment attempt. Use it when the buyer has committed and the charge is in progress. |
| Hold token | 16 bytes, hex | Identifies the hold. Keep it for the extend and release calls. |
The hold is written by a single Lua script. Every requested seat must be free (or already held by your own token, re-holding your own seats is allowed) and must not be blocked by the organiser. If any seat fails that test, nothing is written at all. You either hold the whole selection or none of it, and no two buyers can hold the same seat.
Holds expire by Redis key TTL. There is no sweeper job that runs every minute and might lag behind; when the TTL is reached, the seat is free at that exact moment.
Seats become permanently booked only when the payment is confirmed, the same rule as general admission. A hold is a promise that nobody else will take the seat while the buyer decides, not a booking.
These run inside a buyer session (authenticated by the session cookie set when the session URL was opened). In the embedded checkout, Blend's own checkout page calls them; you do not need to call them yourself unless you are building a custom seat picker.
| Endpoint | Type | Description |
|---|---|---|
| seats/config | read | The seat map for the event. |
| seats/availability | read | Which seats are currently free, held, blocked or booked. |
| seats/hold-token | write | Issues a hold token (16 random bytes, hex) to use for subsequent hold calls. |
| seats/hold | write | Takes the hold, all-or-nothing, for the requested seats. Returns expiresInSeconds: 600. |
| seats/hold/:token/extend | write | The one-time 600 second extension for a payment attempt. |
| seats/release | write | Releases the hold. Works even for an event outside your catalog, releasing inventory is never blocked. |
{
"expiresInSeconds": 600
}
// Only the timing field is shown here.
// The hold actually lives 720 seconds. The extra 120 are hidden grace
// so a buyer who pays at the last visible second is not cut off.seats/release succeeds even for an event outside your curated catalog. A buyer backing out must always be able to give the seats back; there is no situation where Blend benefits from refusing.
These are returned by checkout when inventory or a hold does not cooperate.
| Code | Type | Description |
|---|---|---|
| TICKET_SOLD_OUT | 409 | The requested ticket type has no remaining capacity. |
| EVENT_SOLD_OUT | 409 | The event as a whole has no remaining capacity. |
| QUANTITY_EXCEEDS_CAPACITY | 409 | The requested quantity is more than the event can supply. |
| TICKET_QUANTITY_EXCEEDS_CAPACITY | 409 | The requested quantity is more than that ticket type can supply. |
| SEAT_CONFLICT | 409 | One or more requested seats are held or booked by someone else. |
| reason: "hold_expired" | 409 | The seat hold ran out before checkout. Take a new hold and try again. |