Customer accounts

Every buyer who pays you needs somewhere to go afterward: to find an order, track a parcel, change a card, pause a subscription, or start a return. Flint ships that surface, and you decide how much of it is yours.

You do not have to choose once and live with it. Branding, domain, and who serves the pages are independent, and each one moves without touching the others.

Four things you can decide#

DecisionSettingEffort
Use Flint's account as-isnothingnone
Put your brand on itbrandinga few colors
Serve it from your domaincustomer_account.presentation.custom_domainDNS records shown by Flint
Build the account yourselfcustomer_account.modeyour own front end

The first three are the same account with more configuration. Only the last one is a build.

Start from the default#

New merchants already have a working customer account at account.withflintpay.com. Flint authenticates the buyer, and every account link in a receipt, shipping notice, dunning email, or return update points at it. The Stripe customer portal shows invoices, so one-time Checkout payments without one don't appear there; Stripe customer portal order history explains how to show them.

You can also customize the portal's branding and manage buyer access.

How buyers get in#

Buyers sign in with a six-digit code Flint emails them. A link in one of Flint's emails opens the order, return, or subscription the email is about without a sign-in. If you send that email yourself, it can carry the same link: see Link the buyer to their order.

After paying in hosted checkout, View your order opens the order the same way. If the buyer confirmed their email with a code during that checkout, to save a card or use a saved one, the button signs them in to your customer account instead, so they see the order alongside everything else they have with you. That sign-in reaches your customer account only, never another business's, and ends after an hour. The button signs the buyer in only within an hour of the confirmed code; after that, and for a buyer who never confirmed one, it opens just the order.

Saved gift cards#

Buyers can open Gift cards from their store account to save a card using its code or the original private link from its email. The list shows available balances and status; each card shows its balances and anonymous history, with refresh and remove actions. Gift cards use the store's theme and custom domain.

Saving a card does not transfer ownership or make it available for checkout spending from the account. Several buyers may save the same card. If its code changes, the buyer needs the new code or private link to save it again.

Add your brand#

branding styles hosted checkout and the customer account together.

cURL
curl -X PATCH https://api.withflintpay.com/v1/settings \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "branding": {
      "primary_color": "#1B4D3E",
      "background_color": "#F4EFE5",
      "text_color": "#1A1714",
      "font_family": "system_serif",
      "corner_radius": 4
    }
  }'

font_family takes instrument_sans, system_sans, system_serif, or monospace. corner_radius is pixels, 0 to 32.

Then name the account:

cURL
curl -X PATCH https://api.withflintpay.com/v1/settings \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_account": {
      "mode": "flint_hosted",
      "presentation": {
        "account_name": "Cedar & Stone"
      }
    }
  }'

account_name defaults to your business name. Flint-hosted accounts show the Powered by Flint credit.

Serve it from your own domain#

A branded account still sits on account.withflintpay.com until you give it a hostname of yours.

Note: Custom domains are an add-on

An account hostname needs the custom domain add-on, $15 a month for one checkout hostname and one account hostname, bought on the dashboard's Flint billing page. Without it, setting custom_domain fails with CUSTOM_DOMAIN_SUBSCRIPTION_REQUIRED. Custom domains covers both hostnames in full: status reasons, checking again, removal, and DNS problems.

Use one exact subdomain, such as account.cedarandstone.com. Apex domains such as cedarandstone.com and wildcard hostnames such as *.cedarandstone.com are not supported. Write internationalized hostnames in ASCII/Punycode form. If your DNS provider can proxy the record through another CDN, publish it as DNS-only.

Flint registers the hostname as a payment method domain for you, so you don't register it first. The status reports the registration in payment_method_domain_id.

  1. Set the domain#

    cURL
    curl -X PATCH https://api.withflintpay.com/v1/settings \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "customer_account": {
          "mode": "flint_hosted",
          "presentation": { "custom_domain": "account.cedarandstone.com" }
        }
      }'
    
  2. Read the DNS records Flint needs#

    customer_account_domain_status appears on the settings object once provisioning starts. It is the only place these records come from; do not guess them.

    cURL
    curl https://api.withflintpay.com/v1/settings \
      -H "Authorization: Bearer YOUR_API_KEY"
    
    Response
    {
      "customer_account_domain_status": {
        "hostname": "account.cedarandstone.com",
        "domain_status": "provisioning",
        "status_reason": "ownership_record_missing",
        "dns_records": [
          { "dns_record_type": "cname", "name": "account.cedarandstone.com", "value": "account.withflintpay.com" },
          { "dns_record_type": "txt", "name": "_flint-verify.account.cedarandstone.com", "value": "flint-verify=2c8e4a6f0b3d7e1a9c5f2b8d4e0a6c3f9b1d7e5a2c8f4b0d6e3a9c1f7b5d2e8a" }
        ],
        "last_checked_at": "2026-08-10T17:00:00Z"
      }
    }
    

    The CNAME points the hostname at Flint. The TXT record at _flint-verify. plus your hostname proves your Flint account controls it, so no other account can claim the hostname. Copy both values from dns_records exactly as Flint returns them.

  3. Publish them and wait#

    Add every record returned by Flint at your DNS provider. domain_status moves to active once the records resolve and the certificate issues. Until then, status_reason says what Flint is waiting for. After you fix a record, POST /v1/settings/custom-domains/customer_account/validate checks again right away, once a minute at most; the custom_domain.status_changed webhook event tells you when the status changes. Ask Flint Help if the records are correct and the status does not recover.

Note: Account links wait for activation

Flint-generated account links use Flint's standard domain until the custom hostname is active. A direct visit to the custom hostname can fail while its DNS or certificate is incomplete. If an active hostname later becomes unhealthy, newly resolved account links return to Flint's standard domain.

Buyers sign in again on their first visit to the custom hostname. Browser sessions do not move between account.withflintpay.com and a customer-owned domain.

The account hostname serves only the Flint-hosted customer account. Hosted checkout, payment links, and invoice payment pages use a separate checkout hostname, checkout.custom_domain, covered in Custom domains. Send null as custom_domain to remove the account hostname; old links on it redirect to Flint's account address for 30 days.

Or build the account yourself#

Set mode to merchant_hosted when you want your own pages. Flint then stops serving an account and sends every account link into your application instead.

cURL
curl -X PATCH https://api.withflintpay.com/v1/settings \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_account": {
      "mode": "merchant_hosted",
      "merchant_account_url": "https://cedarandstone.com/account",
      "route_templates": {
        "order": "/orders/{resource_id}",
        "subscription": "/subscriptions/{resource_id}",
        "return": "/returns/{resource_id}",
        "invoice": "/invoices/{resource_id}",
        "email_preferences": "/email-preferences"
      }
    }
  }'

merchant_account_url is required. It must be an absolute https:// URL on a hostname you own: not an IP address, not a withflintpay.com hostname, and with no credentials or fragment. Anything else returns INVALID_CUSTOMER_ACCOUNT_URL. In test mode, http://localhost and http://127.0.0.1 with any port are also accepted, so you can point a sandbox at a portal running on your machine.

Saving the URL registers its hostname as a payment method domain for you, so you don't register it first. The URL must be on a payment method domain that Flint reports active, or on a subdomain of one. A new registration can still be waiting on its wallet check; saving then returns MERCHANT_ACCOUNT_DOMAIN_NOT_VERIFIED. Read the registration with GET /v1/payment-method-domains, run its check again once the hostname serves your site, and save the URL again with a new Idempotency-Key if you use one. Loopback URLs in test mode skip the registration and this check.

route_templates are optional. order, subscription, return, and invoice are relative paths containing {resource_id} exactly once. email_preferences is a relative path with no placeholder, such as /email-preferences. Anything else returns INVALID_CUSTOMER_ACCOUNT_ROUTE_TEMPLATE. Omit a template and that resource's links land on merchant_account_url. Build your own customer account covers the query parameters each link carries.

Links are resolved when the buyer clicks, not when the email is sent. A receipt from last month follows today's configuration, so changing your route templates fixes old mail too. Each link arrives with flint_ query parameters naming the resource and the action the email asked for; Point Flint's email at your pages lists them.

Building the account is the rest of the work, and it has its own guide: Build your own customer account.

The two modes accept different fields#

flint_hosted and merchant_hosted are not a superset and a subset. Each rejects the other's fields, so a half-applied switch fails loudly instead of silently doing nothing.

You sentWith modeResult
presentationmerchant_hostedCUSTOMER_ACCOUNT_MODE_CONFLICT
merchant_account_url or route_templatesflint_hostedCUSTOMER_ACCOUNT_MODE_CONFLICT
no merchant_account_urlmerchant_hostedCUSTOMER_ACCOUNT_URL_REQUIRED
a merchant_account_url that breaks the URL rules abovemerchant_hostedINVALID_CUSTOMER_ACCOUNT_URL
a merchant_account_url whose payment method domain isn't active yetmerchant_hostedMERCHANT_ACCOUNT_DOMAIN_NOT_VERIFIED
a custom_domain that isn't one exact subdomainflint_hostedINVALID_CUSTOM_DOMAIN
a custom_domain without the custom domain add-onflint_hostedCUSTOM_DOMAIN_SUBSCRIPTION_REQUIRED
a custom_domain in use by another Flint account, your checkout, or another of your environmentsflint_hostedCUSTOM_DOMAIN_UNAVAILABLE
a custom_domain whose payment method domain's validation_status isn't activeflint_hostedCUSTOM_DOMAIN_NOT_VERIFIED
a merchant_account_url or custom_domain before payment onboarding is finishedeitherMERCHANT_ACCOUNT_NOT_READY
a merchant_account_url or custom_domain while another update to its payment method domain is runningeitherPAYMENT_METHOD_DOMAIN_OPERATION_IN_PROGRESS
a merchant_account_url or custom_domain when Flint can't update its payment method domaineitherPAYMENT_METHOD_DOMAIN_REGISTRATION_FAILED

The CUSTOM_DOMAIN_* and INVALID_CUSTOM_DOMAIN errors apply only to a Flint-hosted account, because custom_domain is a flint_hosted field. Custom domains explains each one.

MERCHANT_ACCOUNT_NOT_READY, PAYMENT_METHOD_DOMAIN_OPERATION_IN_PROGRESS, and PAYMENT_METHOD_DOMAIN_REGISTRATION_FAILED come from registering the hostname as a payment method domain, and their param names the field that failed. Finish payment onboarding for MERCHANT_ACCOUNT_NOT_READY. For the other two, wait a moment. Then send the same request again, with the same Idempotency-Key if you use one.

A retry with the same key returns MERCHANT_ACCOUNT_DOMAIN_NOT_VERIFIED or CUSTOM_DOMAIN_NOT_VERIFIED again, so once the payment method domain is active, save the setting again with a new key.

customer_account is a merchant setting. Locations and devices inherit it and can't override it, because a buyer's account is not a per-register concept.

Warning: The hosted account cannot be iframed

Flint's hosted account sends frame-ancestors deny, so it cannot be embedded in an iframe and there is no setting to allow it. To put the account inside your own page, build it against /v1/me.

What buyers can do to their subscriptions#

customer_account.buyer_capabilities decides what a buyer may change on their own subscriptions, in Flint's account and through the customer sessions your own account uses. It applies in both modes, and changing mode keeps it.

cURL
curl -X PATCH https://api.withflintpay.com/v1/settings \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "customer_account": {
      "buyer_capabilities": {
        "cancellation_timing": "buyer_chooses",
        "pause": { "enabled": true, "max_cycles": 3 },
        "cancellation_reasons": ["too_expensive", "unused", "switched_service", "other"],
        "retention_offer": { "kind": "pause_instead", "pause_cycles": 2 },
        "can_update_delivery": true,
        "skip": { "enabled": true, "max_consecutive_skips": 2 }
      }
    }
  }'
FieldDefaultWhat it controls
cancellation_timingend_of_periodend_of_period ends a buyer's cancellation when the billing period ends. buyer_chooses also lets the buyer end it right away.
pause.enabledtrueWhether buyers may pause.
pause.max_cyclesno limitThe longest pause a buyer may choose, 1 to 12 billing periods. With a limit set, every buyer pause names its length.
cancellation_reasons[]The reasons Flint's account asks a buyer to choose from, in order. Empty asks nothing.
retention_offernonepause_instead offers a pause of pause_cycles billing periods before the buyer cancels. It needs pausing on, and pause_cycles of at most max_cycles.
can_update_deliverytrueWhether buyers may change the shipping address and delivery method of a subscription that ships.
skip.enabledtrueWhether buyers may skip their next billing cycle.
skip.max_consecutive_skipsno limitHow many cycles in a row a buyer may skip, 1 to 12. Every skip counts, including yours, and the next charged cycle resets the count.

Buyers can always cancel: no setting removes the cancel button or makes a buyer answer a question or decline an offer first. Your API key isn't bound by any of these fields, so your support team can still pause for six months or cancel on the spot.

buyer_capabilities is written as a whole. Each update replaces the stored value, and every field it leaves out goes back to its default. Read it from GET /v1/settings/effective, which fills in each default.

A subscription read through a customer session applies these settings for you: its buyer_actions mark pause as store_policy when pausing is off, update_delivery when can_update_delivery is false, and skip when skipping is off. A buyer who has reached max_consecutive_skips sees skip as limit_reached.

An account you build uses these routes with a customer session:

RouteWhat it does
POST /v1/me/subscriptions/{subscription_id}/deliveryReplaces the subscription's delivery: type, delivery_method_id, destination, and optional recipient. Flint checks the method against the new address before saving; if it doesn't serve it, send a method that does.
POST /v1/me/subscriptions/{subscription_id}/skip-cycleSkips the next cycle.
POST /v1/me/subscriptions/{subscription_id}/billing-intervalChanges how often it renews, to one of the plan's billing_interval_options.
POST /v1/me/subscriptions/{subscription_id}/quantityChanges quantity to one of the plan's quantity_options.
PATCH /v1/me/subscriptions/{subscription_id}/line-items/{subscription_line_item_id}Swaps a line's variant_id to one the plan line allows in swap_variant_ids, or back to its own variant.
POST /v1/me/subscriptions/{subscription_id}/renewOrders the next renewal now. Send an Idempotency-Key.
POST /v1/me/subscription-previewsWith mode: "delivery_options", a subscription_id, and a destination, lists every method the plan offers plus the subscription's current method (current: true), each with availability and selectable for that address. A method that can't serve it carries unavailable_reason; one that can carries its shipping price in amount_money. Nothing is saved. Show it before the buyer confirms a new address, and offer only methods where selectable is true.

Send the subscription's version as expected_version on each write, so a change made in another tab isn't overwritten. Interval, quantity, and swap changes need no store setting: the plan's options decide what the buyer can choose, and buyer_actions reports them as store_policy when the plan offers no choice.

The buyer can choose among the plan's offered methods. A buyer who keeps a method you've since removed from the plan can't switch back to it after changing. A change applies from the next unpaid renewal; a renewal that is already paid still ships as it was. See Buyer self-service.

A buyer's changes send the same webhooks as yours and name the buyer as who made them. When a buyer sets a cancellation for the end of the billing period, or undoes one, you hear about it before the subscription ends:

Cards saved with Flint#

When Flint wallet support is enabled, a buyer who signs in with an emailed code can see Saved with Flint cards alongside the store's saved cards. This also works on a custom domain with a Flint-hosted account. Merchant-hosted accounts, merchant-created customer sessions, API keys, email links, checkout sessions, and access grants cannot open the wallet.

Call GET /v1/me/flint-wallet/payment-methods with the buyer's customer session. Each card has an id that works only at this store and a nullable store_payment_method_id. The list returns all usable cards in data, with has_more: false. Live and sandbox cards are separate.

After the buyer agrees to save a store copy for subscription and other off-session payments, call POST /v1/me/flint-wallet/payment-methods/{id}/store-setups with an Idempotency-Key and an empty body or {}. For example, use Idempotency-Key: store-card-consent-1 for the selected card and reuse it if the response is lost. The response gives you store_setup_id, status, the reserved payment_method_id, and the same client_setup shape as saving a store card. Confirm it with Stripe.js confirmSetup, which may ask the buyer to authenticate, then poll GET /v1/me/payment-methods/{payment_method_id} until the card is active. Use that store payment method with POST /v1/me/subscriptions/{subscription_id}/payment-method. Confirm the SetupIntent with its preselected card. If you confirm with a different payment method, Flint marks the reserved copy as failed and preserves the replacement card. Get fresh consent and use a new key to try again.

The store copy is independent of the platform card and carries consent for this store. Removing the platform card or revoking the store's consent blocks the copy. Retrying any setup key for a revoked copy is refused. Get new consent and use a new key to create another copy. Some cards still require authentication on a later off-session charge; the existing subscription card-update link brings the buyer back. /v1/me/payment-methods responses include saved_with: "flint" or "store" when wallet support is enabled. Accept future values. When support is disabled, wallet routes return 404 and saved_with is omitted.

Let the buyer change their own email#

A customer's email is their identity, and PATCH /v1/customers/{customer_id} cannot change it. The buyer changes it themselves by confirming both addresses: Flint mails a code to the current address and a code to the new one, and nothing moves until both are confirmed.

An account with no email on file only has to confirm the new address.

Account deletion#

A buyer can ask you to delete their account from either kind of account surface. The request goes to you, not straight through: someone approves or rejects it, and three webhook events report the outcome.

Completion anonymizes the customer rather than erasing history. Orders, payments, and refunds survive without the buyer's identity attached, which is what retention_policy: "retain_required_commerce_records" means. Email preferences survive separately so Flint can continue to honor the buyer's choices. Flint stores a keyed recipient marker rather than the email address with those preferences. The buyer can still manage those preferences after verifying the address, but this access does not restore deleted customer or commerce data. If you keep your own copy of buyer data, customer.deletion_completed is your cue to delete it.

Approval, the subscriptions and saved cards that block it, and requests you open on a buyer's behalf are covered in Customer deletion requests.

Next steps#

Was this helpful?