Skip to content

Payment links

No app. No site. Still a real sale.

A payment link is the only thing you can ship with nothing behind it. Flint's is not a link to a charge: it is a sellable object with a type, a form, capacity and stock, and every buyer who pays leaves a real order.

201 Created
{
  "data": {
    "payment_link_id": "plink_1kmn0aExample",
    "status": "active",
    "payment_link_type": "standard",
    "url": "https://pay.withflintpay.com/plink_1kmn0aExample"
  }
}

One call, and the product is live. There is nothing else to deploy.

Share this and you are selling

https://pay.withflintpay.com/plink_1kmn0aExample
pay.withflintpay.com
Where that URL lands. Hosted, branded, and served by nothing you had to build.

01The four

One endpoint. Four products.

A link type is not a display option. Each one changes what the page collects, what the buyer gets, and what lands on the order.

A product with a price. Quantity and price can both be made adjustable, within bounds you set.

curl
curl -X POST https://api.withflintpay.com/v1/payment-links \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: payment-link-spring-tee-001" \
  -d '{
    "name": "Limited spring tee",
    "line_items": [
      { "name": "Spring tee", "quantity": 1,
        "unit_price_money": { "amount": 3500, "currency": "USD" } }
    ]
  }'
pay.withflintpay.com
The real link page, in each of its four shapes. One endpoint produced all of them.
  • payment_link_type
  • event_config
  • plan_id

02The buyer

What they get to change.

Pay-what-you-want is easy to offer and easy to regret. Every buyer-facing control here has a floor and a ceiling you set.

Adjustable, within bounds
{
  "name": "Pay what you can",
  "line_items": [
    {
      "name": "Workshop seat",
      "quantity": 1,
      "allow_quantity_adjustment": true,
      "min_quantity": 1,
      "max_quantity": 6,
      "allow_unit_price_adjustment": true,
      "min_unit_price_money": { "amount": 1000, "currency": "USD" },
      "max_unit_price_money": { "amount": 20000, "currency": "USD" },
      "suggested_unit_price_money_options": [
        { "amount": 2500, "currency": "USD" },
        { "amount": 5000, "currency": "USD" }
      ]
    }
  ]
}

Quantity and unit price are separately adjustable, each with a minimum and a maximum, plus suggested amounts the buyer can just click.

Ask for what you need
curl -X POST \
  https://api.withflintpay.com/v1/payment-links/plink_1kmn0aExample/custom-fields \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{
    "label": "Shirt size",
    "custom_field_type": "dropdown",
    "options": ["S", "M", "L", "XL"],
    "required": true
  }'

Text, dropdown, checkbox and textarea, each optionally required. Answers arrive on the order with the payment.

what the buyer fills in
custom_field_type: dropdown
Four field types, four real controls. The form on a link is the kit's own inputs, not a lookalike.
  • allow_unit_price_adjustment
  • suggested_unit_price_money_options
  • custom_field_type

03Availability

Three ways to stop selling.

A link keeps selling until you say otherwise, so the ways to say otherwise matter.

Off, without breaking the URL
curl -X POST \
  https://api.withflintpay.com/v1/payment-links/plink_1kmn0aExample/deactivate \
  -H "Authorization: Bearer YOUR_API_KEY"

Reactivating turns the same link back on, so a code already printed on something keeps working.

Cap it, and say why when it ends
{
  "name": "First 100 customers",
  "max_completions": 100,
  "inactive_message": "The spring drop has ended. Follow us for the next one."
}

max_completions counts checkouts in flight as well as finished ones, so a rush of simultaneous buyers cannot overshoot a limited drop.

Expiration is per visit, not a campaign endIt bounds how long one buyer's checkout stays open after they start it, which keeps a quoted configuration from lingering. It does not schedule the link to switch off. To end a campaign at a fixed time, deactivate it.
a retired link
Buyers who open an inactive link read your sentence, not a generic error. Set one on anything you plan to retire.
  • max_completions
  • completed_count
  • inactive_message

04No integration

Print it and walk away.

The buyer-side call carries no API key at all.

The buyer-side call
# no API key on this one. it is the buyer-side call behind
# a code printed on a poster, a table tent, or a business card.
curl -X POST \
  https://api.withflintpay.com/v1/payment-links/plink_1kmn0aExample/resolve \
  -H "Content-Type: application/json" \
  -d '{}'

A poster, a table tent, or the back of a business card is a complete storefront.

A link is a URL

Nothing has to run to serve it. There is no callback to host, no key to rotate into a client, and no build to ship when the price changes.

  • POST /v1/payment-links/{payment_link_id}/resolve

It is still a real sale

The absence of an integration does not downgrade what happens. The same order, the same webhooks, the same refunds as anything else you sell.

  • order.paid

05Afterwards

One URL, one order each.

Many buyers share a link. None of them share a record.

What a completed link leaves
{
  "data": {
    "order_id": "ord_1kmn0aExample",
    "origin": "payment_link",
    "status": "closed",
    "payment_status": "paid",
    "line_items": [
      { "name": "Spring tee", "quantity": 2,
        "subtotal_money": { "amount": 7000, "currency": "USD" } }
    ],
    "pricing_amounts": {
      "total_money": { "amount": 7000, "currency": "USD" }
    }
  }
}

origin says where it came from. Your metadata rides along, and the buyer's custom field answers arrive with it.

Reconciling a campaign
curl "https://api.withflintpay.com/v1/orders?origin=payment_link" \
  -H "Authorization: Bearer YOUR_API_KEY"

The filter is origin, not source.

Because it is an ordinary order

Refunds target its line items. Receipts render from it. Analytics counts it beside every other sale. None of that is special-cased for links, which is the reason it works at all.

Events

  • order.paid
  • subscription.activated
  • origin
  • metadata
  • GET /v1/orders

06Limits

When to reach for something else.

A link is one URL for many buyers. When you need one page for one buyer, with an order you already built and a price nobody else should see, that is a checkout session. When you need terms, a due date and a document, that is an invoice. Which of the three to use is its own guide, and that guide owns the decision.

Capacity is enforced at payment, not at page open. An event link acquires its hold immediately before confirmation, so an older tab that has been sitting open can be turned away if someone else takes the last seats first. That is the right trade against overselling, and worth knowing before you promise a seat.

Flint is not a payment processor. Stripe processes every card, and Flint provisions and operates the processing account. You cannot attach a Stripe account you already have, and Flint inherits Stripe underwriting: if Stripe declined your business, Flint cannot approve it.

Against a link that only collects money

The link knows what it is selling, so donations, tickets and signups are types rather than workarounds.

payment_link_type

Buyers can move the price and the quantity, and cannot move either past your bounds.

min_unit_price_money

A limited drop stops at the limit, counting checkouts already in flight.

max_completions

A retired link explains itself instead of erroring.

inactive_message

Every buyer leaves a real order, filterable by where it came from.

origin=payment_link

Reviewed 2026-07-25 against the published API.

07Start

Sell something in one call.

Node

npm install @flintpay/node

CLI

brew install flintpay/tap/flint

Create a link, open its URL, and pay with 4242 4242 4242 4242. Then set max_completions to 1, pay it again, and watch the second buyer get told the drop is over. That is the whole product, and it takes about five minutes.

FAQ

Questions worth asking first.

What can a payment link actually sell?

Four different things, selected by payment_link_type. A standard link sells products with a price. A donation link presents an amount picker with suggested amounts and a floor. An event link sells ticket tiers against a shared capacity, with a date, a venue, and emailed tickets. Passing plan_id instead of line items turns the link into a hosted subscription signup.

Do I need a website to use one?

No, and that is the point. A link is a URL. The buyer-side resolve call carries no API key, so a code printed anywhere works with nothing running behind it. No app, no site, no integration, and what you get back is still a real order.

What happens after someone pays?

Every completed checkout creates its own order carrying origin payment_link, the link's metadata copied onto it, and the buyer's custom field answers. Refunds, receipts, webhooks and reporting work exactly as they do for any other order, because it is one. Filter them with GET /v1/orders and the origin parameter.

Can buyers change the price or quantity?

Only within bounds you set. allow_quantity_adjustment with min_quantity and max_quantity lets them pick how many. allow_unit_price_adjustment with min_unit_price_money and max_unit_price_money lets them pick what to pay, and suggested_unit_price_money_options gives them defaults to click. That covers pay-what-you-want, name-your-price and tiered giving without letting anyone pay a cent.

How do I stop a link, or cap it?

Three levers. Deactivate turns it off without breaking the URL, and buyers see your inactive_message instead of a generic error. max_completions caps how many checkouts can complete, counting in-flight ones too, so a burst of simultaneous buyers cannot blow past a limited drop. And expiration bounds how long one buyer's checkout stays open, which is per visit and not a campaign end date: to end a campaign, deactivate.

Are tickets real tickets?

Yes. An event link enforces max_total_quantity across all tiers, and capacity is acquired when payment starts rather than when a checkout opens, so an older idle tab cannot hold a seat. Buyers land on a ticket confirmation page, and with send_ticket_emails the tickets are emailed too. ticket_prefix brands the numbers, so GAL yields tickets like GAL042.