How to sell event ticket tiers with one capacity on Stripe

Stripe sells tickets as products with prices, one per tier, and its API has no event, ticket, or capacity object. A Payment Link can stop after a set count of completed checkouts, but that counts buyers, not tickets. To sell General and VIP against one 500-seat room, you keep one seat counter across tiers, hold seats while each Checkout Session is open, and release them when it expires.
Verified against official documentationReviewed Send a correction (opens in a new tab)

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:

JavaScript
// 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:

JavaScript
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

  1. A Payment Link limit counts buyers, not tickets#

    You capped the link at 500 for a 500-seat room and sold 1,340 tickets.

    restrictions.completed_sessions.limit is the maximum number of checkout sessions that can be completed. A buyer who takes four General and two VIP tickets completes one session. With adjustable quantities, the limit has no relationship to the number of seats.

    Confirm it

    Sum the line item quantities of every completed session on the link and compare the total with the venue.

    Fix

    Only use the completion limit when every checkout is exactly one ticket. Otherwise count tickets yourself and deactivate the link when the count reaches capacity.

  2. Per-tier counts don't add up to the room#

    General and VIP each stayed under their own limit, and the venue was 40 over.

    Each tier is its own Price, so per-tier counts are the natural thing to build. The fire code limits the room, which every tier draws from. Two counters that each check only themselves can both say yes to the last seats.

    Confirm it

    Add up sold tickets across tiers for each event and compare with the venue's capacity.

    Fix

    Keep one capacity counter per event and check every tier against it in the same transaction. Per-tier limits, if you want them, are a second check on top.

  3. Adjustable quantities hold the maximum for every open checkout#

    The event showed sold out with 200 seats still unsold.

    If buyers can change the quantity on Stripe's page, Stripe's guide says to reserve adjustable_quantity.maximum, not the starting quantity, because you don't know the final number until the session completes. A General tier with a maximum of 10 holds 10 seats per open checkout, and a session stays open at least 30 minutes unless you expire it.

    Confirm it

    Compare seats held by open sessions with the quantities those sessions finally bought.

    Fix

    Ask for tier quantities on your own event page and create the session with fixed quantities, so each hold is exactly what the buyer chose. Set expires_at just past Stripe's 30-minute minimum, and expire the session yourself when the buyer leaves your page.

  4. Ticket numbers, the door list, and ticket emails are your code#

    Paid buyers arrive with a payment receipt and no ticket.

    A completed Checkout Session is a payment with line items. Stripe has no event or ticket object, so numbering tickets, emailing them, and producing the list for the door all happen in your checkout.session.completed handler, which has to be safe to run twice.

    Confirm it

    Look for completed sessions with no tickets issued, and for buyers with duplicate ticket numbers.

    Fix

    Issue tickets in the same transaction that converts the hold to sold, keyed on the session ID, and send the email after it commits.

On Flint, tiers and capacity are one link

One request creates the event, both tiers, and the shared capacity:

cURL
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
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 LinksFlint event payment links
TiersA Price per tierLine items on one event link
Shared venue capacityYour counter and holdsmax_total_quantity
Built-in limitCompleted checkouts per Payment LinkTickets across all tiers
Two buyers, last seatsYour atomic holdChecked at payment start, counting payments in progress
Abandoned checkoutRelease on checkout.session.expiredNothing held until payment starts
Date, time zone, and venue at checkoutProduct name or custom textevent_config
Ticket numbers and emailsYour webhook handlerticket_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.