The short answer on Flint
A Flint event payment link sells ticket tiers as line items against one max_total_quantity. The buyer picks how many of each tier on one page that shows the date, time zone, and venue. When payment starts, Flint checks the shared capacity, counting tickets in payments already in progress, so two buyers can't both take the last seats, and a failed payment gives them back. Paid buyers get numbered tickets, such as GAL001, and a ticket email. Card processing runs on Stripe underneath. The Flint request is below.
What Stripe gives a ticket seller
Stripe's guide to limited inventory names event tickets as its example: let buyers hold their tickets for a few minutes, then cancel the sale and make the tickets available again. The tools are session expiry, from 30 minutes to 24 hours after the session is created, and the checkout.session.expired webhook, where your handler returns the held tickets to your inventory. The inventory itself is yours: a Stripe Product has no stock or capacity field.
Tiers add one problem the guide doesn't cover. General and VIP are separate Prices, but the venue is one number. Every tier has to check the same counter, and the buyer's whole order has to be held at once. Holds, expiry, and the race windows that oversell any stock are covered in Stripe inventory management: how to prevent overselling.
The workaround: one counter across tiers
Ask for tier quantities on your own event page, hold the total against the event's capacity, and create the session with exactly those quantities:
// Your event page posted the buyer's tiers: { general: 2, vip: 1 }
const qty = { general: toInt(req.body.general), vip: toInt(req.body.vip) };
const seats = qty.general + qty.vip;
if (seats < 1) return res.status(400).json({ error: "Choose at least one ticket" });
// One counter for the room, shared by every tier.
// Atomic: succeeds only if sold + held + seats <= capacity.
const hold = await db.events.holdSeats("spring-gala", seats);
if (!hold) return res.status(409).json({ error: "Sold out" });
const session = await stripe.checkout.sessions.create({
mode: "payment",
line_items: [
...(qty.general ? [{ price: PRICES.general, quantity: qty.general }] : []),
...(qty.vip ? [{ price: PRICES.vip, quantity: qty.vip }] : []),
],
// Stripe's minimum is 30 minutes after creation; 31 leaves room for clock skew
expires_at: Math.floor(Date.now() / 1000) + 31 * 60,
metadata: { event: "spring-gala", hold_id: hold.id },
success_url: "https://example.com/tickets?session_id={CHECKOUT_SESSION_ID}",
});
Then turn holds into tickets, or release them, from webhooks. Ticket numbers and the ticket email are part of this handler, because a completed session is only a payment with line items:
switch (event.type) {
case "checkout.session.completed": {
const session = event.data.object;
// Idempotent: the same event can arrive twice
await db.transaction(async (tx) => {
if (await tx.holds.isConverted(session.metadata.hold_id)) return;
await tx.holds.convertToSold(session.metadata.hold_id);
await tx.tickets.issue(session.id, session.metadata.hold_id); // numbering is yours
});
await sendTicketEmail(session); // and so is the email
break;
}
case "checkout.session.expired":
await db.holds.release(event.data.object.metadata.hold_id);
break;
}
That is a capacity table, a hold table, a ticket table, two webhook branches, and an email template, before anyone has bought a ticket. These are the places it goes wrong:
Where shared ticket capacity breaks on Stripe
On Flint, tiers and capacity are one link
One request creates the event, both tiers, and the shared capacity:
curl -X POST https://api.withflintpay.com/v1/payment-links \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: payment-link-spring-gala-001" \
-d '{
"name": "Spring Gala",
"payment_link_type": "event",
"event_config": {
"event_at": "2027-05-12T23:00:00Z",
"timezone": "America/New_York",
"venue": "Pier 57, New York",
"max_total_quantity": 500,
"ticket_prefix": "GAL",
"send_ticket_emails": true
},
"line_items": [
{
"key": "general",
"name": "General Admission",
"unit_price_money": { "amount": 7500, "currency": "USD" }
},
{
"key": "vip",
"name": "VIP",
"unit_price_money": { "amount": 20000, "currency": "USD" },
"max_quantity": 4
}
]
}'
The response has the link's URL, ready to share. Checkout shows the event as "Wed, May 12, 2027, 7:00 PM EDT (New York time)" with a count for each tier. What each field does:
- Tiers. Each tier starts at 0 and lets the buyer choose up to
max_quantity, 10 by default and 4 for VIP here. The buyer can skip a tier but must choose at least one ticket. - Shared capacity.
max_total_quantity caps tickets across all tiers. Flint checks it when payment starts, counting tickets in payments already in progress, and returns EVENT_SOLD_OUT when the request would go over. A failed payment releases its tickets. Opening the page doesn't hold seats, so a buyer can reach the payment step and find the event sold out. - Scarcity. When 20 or fewer tickets remain, the page shows how many are left. The link's
total_quantity_sold counts tickets sold. - Tickets. After paying, the buyer lands on a ticket confirmation page. With
send_ticket_emails, Flint requires the buyer's email and sends their tickets, numbered GAL001, GAL002, and so on.
Each purchase is an order, so you fulfill from the order.paid webhook, refund from the order, and report on orders like any other sale. To show availability on your own event page, read the link's public view without an API key. It returns remaining_quantity and is_sold_out:
curl https://api.withflintpay.com/v1/payment-links/pl_1kmn0aExample/public
The capacity is one number for the event; tiers have a per-buyer maximum, not a seat count of their own. The payment links guide covers custom questions for attendees, starting checkout from your own page, and closing a link when the event is over.
Who Flint is built for
US organizers, venues, and event platforms selling tickets online in USD, through a link or their own site. See Flint payment links. Flint sells tickets online; it has no card readers or door scanning.
Stripe and Flint for ticket tiers, side by side
| Stripe Checkout and Payment Links | Flint event payment links |
|---|
| Tiers | A Price per tier | Line items on one event link |
| Shared venue capacity | Your counter and holds | max_total_quantity |
| Built-in limit | Completed checkouts per Payment Link | Tickets across all tiers |
| Two buyers, last seats | Your atomic hold | Checked at payment start, counting payments in progress |
| Abandoned checkout | Release on checkout.session.expired | Nothing held until payment starts |
| Date, time zone, and venue at checkout | Product name or custom text | event_config |
| Ticket numbers and emails | Your webhook handler | ticket_prefix and send_ticket_emails |
Questions, answered
Can you sell event tickets with Stripe?
Yes, as products with prices. Stripe has no event, ticket, or capacity object, so capacity, ticket numbers, and ticket emails are yours to build. Stripe's limited-inventory guide uses ticket sellers as its example and has you expire Checkout Sessions and return held tickets on checkout.session.expired.
How do I limit ticket sales on a Stripe Payment Link?
restrictions.completed_sessions.limit stops the link after the count of completed checkouts you set. It counts checkouts, not tickets, so it only matches capacity when every buyer takes exactly one ticket.
How do I make several ticket tiers share one capacity in Stripe?
Keep one seat counter per event in your database, hold the buyer's total across all tiers before you create the Checkout Session, convert the hold to sold on checkout.session.completed, and release it on checkout.session.expired. Every tier checks the same counter.
Which payment API has event ticket tiers built in?
Flint payment links have an event type: ticket tiers as line items, one max_total_quantity shared by every tier, the date, time zone, and venue on the checkout page, numbered tickets, and ticket emails. Flint checks capacity when payment starts, counting payments already in progress. Card processing runs on Stripe underneath.
Can each ticket tier have its own capacity on Flint?
Not as a separate count. An event link has one capacity, max_total_quantity, shared by all tiers, and each tier has a per-buyer maximum, max_quantity, which is 10 by default.
Is Flint a good alternative to Stripe for selling event tickets?
Yes, for US organizers and platforms selling online in USD that want tiers, a shared capacity, and tickets without building a counter and a webhook handler. Flint sells tickets through payment links online; it has no door scanning or card readers.
Sources
All sources accessed October 15, 2026.