# Take a payment

Every paid ticket is funded by a card payment. You create a **payment intent**
for a specific event and tier, collect the card on the client, and the resulting
payment becomes the proof of funds you attach when you issue or upgrade a ticket.

If you've integrated Stripe before, this will feel familiar: a server-created
intent, a client-side confirmation, and a status you can poll. The key
difference is that **TicketConnect computes the amount for you** — you never send
a price, so a client can't tamper with what gets charged.

## Before you start

You'll need:

* An API key with the **`payments:write`** scope to create payments.
* An **event** with at least one priced **tier**.

Reading a payment back (`GET /v1/payments/:id`) is allowed with either
`payments:write` or `payments:read`.

Base URL:

```text
https://api.ticketconnect.example/v1
```

## Step 1 — Create a payment intent

**Endpoint:** `POST /v1/payment_intents` · **Scope:** `payments:write`

Tell the API which event and tier the customer is buying. The amount is derived
server-side from that tier's fiat price.

```bash
curl https://api.ticketconnect.example/v1/payment_intents \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: pay_order_88231" \
  -d '{
    "event_id": "evt_summerfest",
    "tier": "General Admission",
    "customer_id": "user_a1b2c3"
  }'
```

```json
{
  "object": "payment_intent",
  "id": "pi_3PxyzABC",
  "provider": "stripe",
  "client_payload": { "client_secret": "pi_3PxyzABC_secret_..." },
  "amount": 49.00,
  "currency": "USD",
  "status": "requires_payment"
}
```

> **The amount is server-authoritative.** There is no `amount` field in the request
> body, and any extra keys are ignored. The charge always equals the tier price on
> your event, in your account's currency. This is what stops a manipulated client
> from underpaying.


The `client_payload` carries the details your client needs to confirm the card
(for the Stripe provider, a `client_secret`). Pass it to the TicketConnect
client integration the same way you'd hand a client secret to Stripe's SDK, and
confirm the card in the browser or app.

## Step 2 — Confirm the card

Card confirmation happens on the client, with the customer's card details — they
never touch your server. After the customer submits their card, the payment moves
toward `succeeded`.

### Test cards

In test mode (`sk_test_...` keys), use your payment provider's standard test
cards to simulate outcomes — a card that always succeeds, one that always
declines, one that triggers authentication, and so on. Test-mode payments are
fully isolated from live data, so you can run a full issuance end-to-end without
charging anyone.

## Step 3 — Retrieve a payment

Check a payment's current status at any time.

**Endpoint:** `GET /v1/payments/:id` · **Scope:** `payments:read` or `payments:write`

```bash
curl https://api.ticketconnect.example/v1/payments/pi_3PxyzABC \
  -H "Authorization: Bearer sk_test_..."
```

```json
{
  "object": "payment_intent",
  "id": "pi_3PxyzABC",
  "amount": 49.00,
  "currency": "USD",
  "status": "succeeded"
}
```

A payment is only ever visible to the account that created it; requesting a
payment that belongs to another account returns `404 resource_missing`.

## How a payment ties into issuance and upgrades

A payment on its own doesn't put a ticket in anyone's hands — it's the funding
proof you attach to the next step:

* **Issuance.** Pass the payment as `payment_intent_id` to
  `POST /v1/events/:id/tickets`. The payment must be `succeeded`, match the tier
  amount and currency, and not have funded another ticket already. See
  [Issue a ticket](#issue-a-ticket).
* **Upgrades.** When a customer moves to a more expensive tier, you create a
  payment for the **price difference** and attach it to the upgrade call. See
  [Transfer, refund &amp; upgrade](#transfer-refund-upgrade).

> **Waiting-room events: charge after admission.** If the event runs an on-sale
> queue (its event object has a non-null `queue`), admission is enforced when the
> ticket is issued — not when the intent is created. Join the queue and poll
> `GET /v1/events/:id/queue?customer_id=...` until `status` is `admitted` before
> you take the payment; otherwise the charge can succeed and issuance still
> return `403 not_admitted` until the customer is admitted.


> **Reserved seating: hold the seats before you charge.** On events with a
> published seat map, hold the customer's seats first
> (`POST /v1/events/:id/seats/hold` — seats stay yours for 10 minutes), then
> take the payment, then issue with the returned `hold_id`. Pass
> `quantity: <held seats>` when creating the payment intent — the server
> charges tier price × quantity, matching what issuance will verify. If card
> entry runs long,
> extend the hold (`POST /v1/events/:id/seats/hold/:holdId/extend`) so the
> seats don't lapse mid-payment. See
> [Issue a ticket](#issue-a-ticket) for the full flow.

The money flow is entirely card-in, fiat-out: the customer pays by card in your
currency, and your proceeds settle to your bank account as a fiat payout. Your
account always shows a single number — your balance in your currency.

> **Use an `Idempotency-Key` on every create.** Retrying a payment-intent creation
> with the same key returns the original intent instead of creating a duplicate
> charge. Keys are honored for 24 hours.


### Common errors

| Status | `code` | Meaning |
| --- | --- | --- |
| `400` | `parameter_missing` | `event_id` or `tier` was not supplied. |
| `400` | `parameter_invalid` | The named tier doesn't exist on that event. |
| `404` | `resource_missing` | No such event under your account (or payment not found on retrieve). |
| `502` | `payment_provider_error` | The provider couldn't create the payment; retry. |

## See also

* [Issue a ticket](#issue-a-ticket) — turn a successful payment into a ticket.
* [Transfer, refund &amp; upgrade](#transfer-refund-upgrade) — charge the difference on an upgrade.
* [API reference: payments](#api-payments)
