Invoicing & payments
Create invoices through the API and collect payment on the gallery's Stripe — get a hosted checkout link in the create response and settle via webhooks.
You can issue invoices and get paid entirely through /api/v1. Buyers pay on a
Stripe-hosted checkout page on the gallery's own connected account, so the
gallery is the merchant of record and your integration never touches card data.
This is the Shopify-style pattern: you orchestrate, the platform is the system of
record and the payment rail.
The flow
- Create a sale — an invoice always belongs to a sale (
POST /gallery/sales). - Issue an invoice for that sale with
create_checkout_link: true(POST /gallery/invoices). The response includes a hostedcheckout_url. - Send the buyer to
checkout_urlto pay. - Receive webhooks —
payment.recordedwhen a payment settles andinvoice.paidonce the invoice is fully paid (see Webhooks).
Issue and collect in one call
curl -X POST https://app.example.com/api/v1/gallery/invoices \
-H "Authorization: Bearer $GALLERY_API_KEY" \
-H "X-Gallery-Slug: $GALLERY_SLUG" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"sale_id": "…",
"currency": "USD",
"total_cents": 250000,
"create_checkout_link": true
}'The response is the invoice plus a checkout_url:
{
"id": "…",
"invoice_number": "INV-0007",
"total_cents": 250000,
"amount_paid_cents": 0,
"currency": "USD",
"checkout_url": "https://checkout.stripe.com/c/pay/cs_…"
}Redirect the buyer to checkout_url. When they pay, the platform records the
payment against the invoice and fires your webhooks — no polling required.
When checkout_url is null
The link is minted only when the gallery can actually collect. checkout_url is
null if:
- the gallery hasn't connected Stripe (or charges aren't enabled yet), or
- the invoice has no outstanding balance (e.g. you created a header and will
add line items in later calls, so the total is still
0).
In that second case, build up the invoice with
POST /gallery/invoices/{id}/line-items, then mint a link on demand with
POST /gallery/invoices/{id}/payment-sessions — the same hosted checkout, minted
whenever you're ready.
Payment plans (installments)
Collect an invoice in stages — a deposit plus scheduled installments. Plan the installments in one call; the plan's total may not exceed the invoice total, and each installment is collected on its own hosted checkout.
# Plan a deposit + two installments
curl -X POST https://app.example.com/api/v1/gallery/invoices/$INVOICE_ID/payment-schedule \
-H "Authorization: Bearer $GALLERY_API_KEY" \
-H "X-Gallery-Slug: $GALLERY_SLUG" \
-H "Content-Type: application/json" \
-d '{
"installments": [
{ "label": "Deposit", "amount_cents": 100000, "due_date": "2026-08-01" },
{ "label": "Installment 1", "amount_cents": 75000, "due_date": "2026-09-01" },
{ "label": "Installment 2", "amount_cents": 75000, "due_date": "2026-10-01" }
]
}'Each installment comes back with an id, a sequence (0 = deposit), and a
status (scheduled → paid when it settles). To collect one, mint a checkout
link for it:
curl -X POST \
https://app.example.com/api/v1/gallery/invoices/$INVOICE_ID/payment-schedule/$SCHEDULE_ID/collect \
-H "Authorization: Bearer $GALLERY_API_KEY" \
-H "X-Gallery-Slug: $GALLERY_SLUG" \
-H "Idempotency-Key: $(uuidgen)"Send the buyer to the returned url. When they pay, the platform records the
payment against the invoice, marks that installment paid, and fires your
payment.recorded webhook — the same settlement path as a one-shot checkout.
GET …/payment-schedule lists the plan; DELETE …/payment-schedule/{scheduleId}
cancels an unpaid installment. Re-planning is refused once any installment has
been paid.
Scopes
Issuing invoices needs the sales:write scope; minting payment links and reading
payment state need payments:write / payments:read. Grant a key only the
scopes your integration uses (see Authentication).