Build your own customer account

/v1/me is the buyer's own view of their commerce data. Order history, delivery tracking, invoices, subscriptions, saved cards, addresses, and Returns, all authorized by a customer session rather than your merchant key.

The point of the namespace is what it refuses to do. There is no customer_id argument anywhere in it. You cannot ask for another buyer's orders, so you cannot accidentally ship a front end that does.

The whole loop#

  1. Authenticate the buyer yourself#

    Your login, your session, your user table. Flint is not involved.

  2. Mint a customer session on your backend#

    cURL
    curl -X POST https://api.withflintpay.com/v1/customer-sessions \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{ "customer_id": "cus_1kmn0aExample" }'
    

    Keep secret and refresh_token in your server-side session. Never send them to the browser.

  3. Read the buyer's data with the session secret#

    cURL
    curl https://api.withflintpay.com/v1/me/orders \
      -H "Authorization: Bearer flint_cses_example"
    

    No customer filter. There is nothing to filter by, because the credential already decided.

  4. Refresh on expiry, revoke on sign-out#

    CUSTOMER_SESSION_EXPIRED means refresh and retry. Sign-out means revoke.

What the buyer can do#

JobEndpoints
ProfileGET /v1/me, PATCH /v1/me
Change emailPOST /v1/me/email-change-requests, then /confirm
Email preferencesGET and PATCH /v1/me/email-preferences
Orders/v1/me/orders, /v1/me/orders/{order_id}, /activities, /send-receipt
Money/v1/me/payments, /v1/me/refunds
Delivery/v1/me/fulfillments, /v1/me/shipments, /v1/me/packages, /v1/me/fulfillment-events
Invoices/v1/me/invoices, /{invoice_id}, /pdf, /checkout-session
Subscriptions/v1/me/subscriptions, plus /pause, /resume, /cancel, /reactivate, /payment-method, /payment-retries, /delivery, /skip-cycle, /billing-interval, /quantity, /line-items/{subscription_line_item_id}, /renew; /v1/me/subscription-previews
Gift cards/v1/me/gift-cards to list and save, /{gift_card_id}, /transactions, and delete
Saved cards/v1/me/payment-methods, /{payment_method_id}, plus /set-default and delete
Addresses/v1/me/addresses, plus /set-default
Returns/v1/me/return-previews, /v1/me/returns, /cancel, POST /v1/me/return-resolutions/{return_resolution_id}/checkout-session
Account closure/v1/me/deletion-requests

The Returns, invoicing, and subscription billing guides describe the same buyer jobs. Use the /v1/me route and a customer session instead of sending customer_id. Buyer invoice responses use the BuyerInvoice schema and omit merchant-only fields such as collection policy, payment credentials, metadata, and write-off details. Buyer subscription reads include delivery_hold and upcoming_delivery_hold, so your account can warn a buyer before a renewal is held, and omit the merchant-only inventory_wait.

Subscription changes through a customer session follow your customer_account.buyer_capabilities, exactly as they do in Flint's account. Read the effective settings with your API key to decide what to show: a cancellation that ends right away needs cancellation_timing: "buyer_chooses", and a pause needs pause.enabled and, with max_cycles set, a length. Your page asks for a reason from cancellation_reasons and sends it as cancellation_reason_code. Anything the store doesn't allow returns CANCEL_IMMEDIATELY_NOT_ALLOWED, PAUSE_NOT_ALLOWED, PAUSE_DURATION_REQUIRED, PAUSE_DURATION_TOO_LONG, or CANCELLATION_REASON_NOT_OFFERED. Changes made with a customer session count as the buyer's: a cancellation set for the end of the period sends subscription.cancellation_scheduled with cancellation_details.requested_by set to buyer, and undoing it sends subscription.reactivated with initiated_by set to buyer.

Saved gift cards#

Use buyer gift card endpoints for a collection saved with a current code or the original private recipient link. Require a full customer session, parse pasted links locally into their grant ID and fragment token, and keep credentials out of URLs, storage, and telemetry. A saved card exposes its available balance and anonymous balance history. Saving changes neither ownership nor spending authority, and code replacement requires fresh possession proof.

Save a card with POST /v1/me/gift-cards and one of two bodies: {"credential_type": "code", "code": "..."} for a code the buyer typed, or {"credential_type": "recipient_access", "grant_id": "...", "recipient_access_token": "..."} for a recipient link. Send an Idempotency-Key, and reuse it if the response is lost.

Recover a past-due subscription#

For a subscription whose status is past_due, change the card through POST /v1/me/subscriptions/{subscription_id}/payment-method if needed. Changing the card only updates the subscription's payment method.

Start a payment retry with POST /v1/me/subscriptions/{subscription_id}/payment-retries. Send an Idempotency-Key and no body, or {}. Reuse that key if the request's response is lost. Poll GET /v1/me/subscriptions/{subscription_id}/payment-retries/{subscription_payment_retry_id} until its status is succeeded or failed; a failed retry includes a buyer-safe failure.code and failure.message.

cURL
curl -X POST https://api.withflintpay.com/v1/me/subscriptions/sub_01JAAAAAAAAAAAAAAAAAAAAAAA/payment-retries \
  -H "Authorization: Bearer $CUSTOMER_SESSION_SECRET" \
  -H "Idempotency-Key: recover-subscription-1"

curl https://api.withflintpay.com/v1/me/subscriptions/sub_01JAAAAAAAAAAAAAAAAAAAAAAA/payment-retries/spr_01JAAAAAAAAAAAAAAAAAAAAAAA \
  -H "Authorization: Bearer $CUSTOMER_SESSION_SECRET"

Only one retry can be in progress at a time. Buyers can start a retry while fewer than 3 retries have been created for the current billing period, counting retries the store starts too. These limits return SUBSCRIPTION_PAYMENT_RETRY_IN_PROGRESS or SUBSCRIPTION_PAYMENT_RETRY_LIMIT_REACHED. A subscription that isn't past_due, or whose renewal is in a delivery hold or waiting on stock, returns 409 SUBSCRIPTION_PAYMENT_RETRY_NOT_ALLOWED. During a delivery hold, ask the buyer to fix delivery instead; the renewal is charged once the hold clears. Reusing a key returns its original retry even after the limit is reached or the subscription's state changes, because Flint checks the key first.

Offer the retry when the subscription's retry_payment action, in buyer_actions, is available: the subscription is past due, has no delivery hold, isn't waiting on stock, and both limits allow another retry. Otherwise its unavailable_reason is not_in_state. It is never required.

Every retry, whether the buyer or you started it, sends subscription_payment_retry.created, then subscription_payment_retry.succeeded or subscription_payment_retry.failed. Each carries the retry as data.object, so you can update your page or email the buyer without polling. See the webhook events catalog.

In Flint's hosted account, sessions opened from an email link with purpose subscription_card_update can change the card, start a retry, and read the retry on that subscription, and a subscription_view session can read a retry but cannot start one. Your own account never gets these sessions: see Emailed links are routing hints.

Show what the buyer can do next#

Orders, subscriptions, invoices, and Returns read through /v1/me carry buyer_actions. Each lists every action its resource has, in the same order every time: whether the buyer can take it now, whether the store needs them to, and by when. Flint decides with the rules its own routes enforce, including your buyer_capabilities, so your account doesn't reimplement them.

Resourcekind, in order
Orderstart_return, resend_receipt
Subscriptioncancel, pause, resume, reactivate, update_payment_method, retry_payment, update_delivery, skip, update_billing_interval, update_quantity, swap_items, renew
Invoicepay
Returnwithdraw, ship_items, pay_balance

An active subscription at a store that turned pausing off, whose card expires before the next charge, reads:

Response
"buyer_actions": [
  { "kind": "cancel", "is_available": true, "is_required": false },
  { "kind": "pause", "is_available": false, "is_required": false, "unavailable_reason": "store_policy" },
  { "kind": "resume", "is_available": false, "is_required": false, "unavailable_reason": "not_in_state" },
  { "kind": "reactivate", "is_available": false, "is_required": false, "unavailable_reason": "not_in_state" },
  { "kind": "update_payment_method", "is_available": true, "is_required": true, "due_at": "2026-11-01T00:00:00Z" },
  { "kind": "retry_payment", "is_available": false, "is_required": false, "unavailable_reason": "not_in_state" }
]
  • Show the actions with is_required as what needs the buyer's attention, with due_at as the deadline: a card to replace before the next charge or retry, an invoice that's due, items to send back.
  • Use the first available action as your page's primary button.
  • unavailable_reason says why an action is out of reach: not_in_state, store_policy, limit_reached, window_closed, nothing_to_return, or collection_unavailable. limit_reached means the buyer has used up a store limit, such as skip after the most consecutive skips the store allows. Both kind and unavailable_reason can gain values, so show a general message for one you don't recognize.
  • A list of orders leaves start_return out, since only a read of one order checks return eligibility. Read GET /v1/me/orders/{order_id} before offering a return.
  • Merchant reads of the same resources carry an empty buyer_actions.

Flint's account also opens sessions from email links that cover only the linked order, subscription, or Return. An action such a session can't take reads sign_in_required and keeps is_required and due_at. Sessions you create with POST /v1/customer-sessions never do.

How saved address defaults work#

Use /v1/me/addresses for buyer-owned address changes. The first saved address becomes both defaults. Setting or editing a saved default also makes that postal address the customer's effective billing or shipping address, including for future subscription tax calculations.

When a linked customer starts a Flint-hosted checkout, Flint prefills any missing billing or shipping address from those effective defaults. An address supplied in prefilled_customer_info remains authoritative for that checkout.

Merchant integrations can still write billing_address and shipping_address through /v1/customers/{customer_id}. Writing one of those compatibility fields removes the corresponding default designation from the address book. That field remains effective until the buyer or merchant selects another saved default.

PATCH /v1/me accepts name and phone. Send address changes to /v1/me/addresses so the address book and effective customer values change together.

Email preferences#

GET /v1/me/email-preferences says which of your optional emails the buyer's address receives: shipping_updates and checkout_reminders. PATCH with { "checkout_reminders": false } turns one off and leaves the other as it is. The setting follows the email address, the same one Flint's unsubscribe links change, so it also covers guest checkouts with that address. Receipts and other transactional email always send. For a buyer who arrives from an unsubscribe link without signing in, see Unsubscribe without sign-in.

A customer without an email has no email preferences: both routes answer 404 with CUSTOMER_EMAIL_REQUIRED.

customer_id is rejected, not ignored#

Sending a customer_id in the query string, or nested anywhere in the request body, fails with ME_CUSTOMER_ID_FORBIDDEN.

Rejecting is deliberate. Silently ignoring a customer_id would let a caller believe they had scoped a request when they had not, and that belief is how impersonation bugs ship.

A resource that exists but belongs to a different buyer returns 404, not 403. The namespace will not confirm that someone else's order ID is real.

expand is not available on /v1/me. Subscription reads include subscription_plan without it, so an account can show each plan's current name. It is null for a subscription created without a plan.

Note:

The SDKs cover customer sessions and every /v1/me route. Use the customer auth mode, which carries the session token instead of your merchant key.

A narrower view, on purpose#

/v1/me is not the merchant surface with a filter bolted on. Reads are trimmed to what a buyer should see, and writes to what a buyer should be able to do.

Read through a customer session, a Return carries a shorter supported_actions and a completion_blockers list filtered to the four a buyer can personally clear. The rest of that list is warehouse and merchant work, and showing it to a buyer reads as "your return is stuck" when nothing is wrong. See buyer-initiated returns.

If your account UI needs something the buyer view does not carry, fetch it on your backend with your merchant key and decide for yourself whether the buyer should see it. Do not reach for a wider credential in the buyer's request path.

Money still moves through Flint#

Paying an invoice and paying the difference on a Return resolution both go through a checkout session:

cURL
curl -X POST https://api.withflintpay.com/v1/me/invoices/inv_1kmn0aExample/checkout-session \
  -H "Authorization: Bearer flint_cses_example" \
  -H "Content-Type: application/json" \
  -d '{"return_url": "https://cedarandstone.com/account/invoices/inv_1kmn0aExample"}'

By default the session is hosted: redirect the buyer to checkout_session.url. To take the payment on your own page instead, send "surface": "embedded" and use the returned checkout credential and checkout_session.payment_collection as Collect an invoice or a return balance describes. Either way a customer session never carries payment authority, so an account you build cannot charge a card directly, in the same way Flint's hosted account cannot.

An embedded launch can also take page_origin, the origin of the page that renders the checkout, so the session can show a gift card challenge. Your backend makes this call with the customer session, so set page_origin there from an origin you control. Never copy it from a request the browser sends. Omit it on a later embedded launch for the same invoice or return balance to keep the earlier session's origin; the invoice and return launch rules cover when a new session inherits it.

return_url is optional and brings the buyer back to your account after paying. /v1/me/return-resolutions/{return_resolution_id}/checkout-session takes it the same way for a Return's balance. For an account you build, it must be an HTTPS address on the host of the merchant_account_url you set below, or Flint refuses it with INVALID_RETURN_URL. In test mode, http://localhost and http://127.0.0.1 with any port also work. Once a payment starts on the session, the session keeps the return_url it has.

Show delivery history#

GET /v1/me/fulfillment-events lists what happened to the buyer's deliveries, newest first, so your order page can show a tracking timeline rather than only the current state. Filter it with order_id, fulfillment_id, shipment_id, or package_id, and page with page_size and page_token.

JSON
{
  "data": [
    {
      "fulfillment_event_id": "fev_1kmn0aExample",
      "order_id": "ord_1kmn0aExample",
      "fulfillment_id": "ful_1kmn0aExample",
      "shipment_id": "shp_1kmn0aExample",
      "event_type": "status_changed",
      "previous_status": "in_transit",
      "current_status": "delivered",
      "location_description": "Austin, TX",
      "occurred_at": "2026-10-03T18:42:00Z",
      "created_at": "2026-10-03T18:42:05Z"
    }
  ]
}

Each event carries fulfillment_event_id, order_id, fulfillment_id, event_type, occurred_at, and created_at, plus shipment_id, package_id, previous_status, current_status, and location_description when they apply. The buyer view leaves out what only you should see: your 3PL's identifiers and statuses, custom details, quantity effects, reasons, and messages. The list covers the same orders as GET /v1/me/fulfillments.

Let the buyer add a card#

Adding a card is the one flow where people assume they are blocked and are not. Flint is not callable from your browser code, but this flow never needs to be.

POST /v1/me/payment-methods returns the saved method plus everything Stripe.js needs:

JSON
{
  "data": {
    "payment_method": {
      "payment_method_id": "pm_1kmn0aExample",
      "status": "pending"
    },
    "client_setup": {
      "stripe": {
        "account_id": "acct_1kmn0aExample",
        "publishable_key": "pk_test_1kmn0aExample",
        "setup_intent": {
          "client_secret": "seti_1kmn0aExample_secret_...",
          "stripe_js_call": "confirm_setup"
        }
      }
    }
  }
}

Your backend makes that call under the customer session, then hands account_id, publishable_key, and the setup intent's client_secret to your own page. The page loads Stripe.js, mounts Elements, and performs the named stripe_js_call. Every browser request goes to Stripe, none to Flint, so the missing browser credential is irrelevant here.

Two things to build around:

  • The method starts pending. It becomes active when the processor confirms, which arrives as a webhook rather than a synchronous response. Do not render the card as usable, or set it as default, on the strength of the create call alone. Listen for the payment-method webhook and reconcile, or poll GET /v1/me/payment-methods/{payment_method_id} until status is active.
  • The publishable key is Flint's platform key, not one you own or rotate. Use the one in the response rather than hardcoding a key, because it tracks the active payment mode.

Removing a card and changing the default are plain calls: DELETE /v1/me/payment-methods/{payment_method_id} and POST .../set-default.

Point Flint's email at your pages#

Building the UI is half the job. The other half is making sure Flint's transactional email sends buyers to it instead of to account.withflintpay.com:

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"
      }
    }
  }'

Links resolve when the buyer clicks, so this also repoints mail you sent before you shipped the account. See customer accounts.

In merchant_hosted mode, every account link in Flint's email goes to your pages: receipts, delivery updates, subscription and dunning email, Returns, invoice email, and unsubscribe links.

Saving merchant_account_url registers its domain as a payment method domain for you, the same way a custom domain is registered. Until that registration's check passes, saving returns MERCHANT_ACCOUNT_DOMAIN_NOT_VERIFIED; Customer accounts explains how to check it again.

Route templates#

Each template is a path relative to merchant_account_url. Leave one out and that resource's links land on merchant_account_url itself, with the same query parameters.

TemplateRuleExample
order, subscription, return, invoiceContains {resource_id} exactly once/invoices/{resource_id}
email_preferencesContains no placeholder/email-preferences

A template that breaks its rule returns INVALID_CUSTOMER_ACCOUNT_ROUTE_TEMPLATE. The invoice page receives the invoice ID. It's where the "Pay invoice" button in Flint's invoice email lands, so let the buyer pay from it through POST /v1/me/invoices/{invoice_id}/checkout-session.

Flint adds flint_* query parameters to every link it sends to your pages:

Text
https://cedarandstone.com/account/orders/ord_1kmn0aExample?flint_action=view&flint_merchant_id=mer_1kmn0aExample&flint_mode=live&flint_resource_id=ord_1kmn0aExample&flint_resource_type=order&flint_source=order_receipts
ParameterValue
flint_merchant_idYour merchant ID.
flint_resource_typeorder, subscription, return, invoice, email_preferences, or account for a link to the account itself.
flint_resource_idThe ID of the order, subscription, return, or invoice. Omitted for email_preferences and account.
flint_actionview for an order or invoice, manage for a subscription, return, or email preferences, and home for the account.
flint_buyer_actionThe control the email asked the buyer to use: skip, update_delivery, or pause. Only on subscription links that carry one. It matches a kind in the subscription's buyer_actions.
flint_modelive, or sandbox for a link from a sandbox.
flint_environment_idThe sandbox the link came from. Present only when flint_mode is sandbox.
flint_sourceThe email family that sent the link, such as order_receipts or invoices. Omitted for unsubscribe links and for email no family covers, such as refund receipts.

New resource types, actions, and sources can be added. Send the buyer to your account home for a value you don't recognize.

The buyer usually arrives signed out, and the parameters are what take them to the right page after they sign in. This is the step most often missed: without it, the buyer lands on a generic home page instead of their order.

  1. Before you redirect to your login, save the full path and query string of the page they asked for, as a relative path you check is on your own site.
  2. After login, send the buyer back to that saved path, with the flint_* parameters still on it.
  3. Use flint_mode and flint_environment_id to pick the live or sandbox API key for the buyer's customer session. Then open the resource named by flint_resource_type and flint_resource_id through /v1/me, such as GET /v1/me/orders/{order_id}.
  4. A 404 means the resource belongs to a different buyer, or doesn't exist. Show the account home, not an error that says which.

The parameters are a routing hint, not proof of anything. The customer session you create for the signed-in buyer decides what they can see. Before you show the control flint_buyer_action names, read the subscription with the buyer's customer session and check that the matching buyer_actions entry has is_available: true. Flint still checks each request.

In Flint's hosted account, an email link can open a session limited to the linked order, subscription, or Return, so a buyer can act on it without signing in. Those emailed access grants, and the email link session purposes such as subscription_card_update and subscription_view, exist only in Flint's hosted account.

In merchant_hosted mode, a link carries no grant and opens no session. You sign the buyer in with your own login, create a full customer session with POST /v1/customer-sessions, and serve the page through /v1/me.

Email change uses codes only#

When the buyer changes their email through POST /v1/me/email-change-requests, Flint emails a code to each address. In merchant_hosted mode the email has no confirmation link, only the code. Ask for the codes on the page where the buyer started the change, and confirm them with /confirm.

Unsubscribe without sign-in#

Flint's shipping update and checkout reminder emails carry an unsubscribe link. In merchant_hosted mode it opens your email_preferences page, and the buyer must be able to use it without signing in.

The emailed link goes to Flint first, which sends the buyer on to your page with a token in the fragment. Your page never gets the token in its path or query string:

Text
https://cedarandstone.com/account/email-preferences?flint_action=manage&flint_merchant_id=mer_1kmn0aExample&flint_mode=live&flint_resource_type=email_preferences#flint_email_preference_token=...

Browsers don't send the fragment to your server, so read it in the page's own script, remove it from the address bar, and post it to your backend:

JavaScript
const params = new URLSearchParams(window.location.hash.slice(1));
const token = params.get("flint_email_preference_token");
history.replaceState(null, "", window.location.pathname + window.location.search);
await fetch("/account/email-preferences/lookup", {
  method: "POST",
  headers: { "Content-Type": "application/json" },
  body: JSON.stringify({ token }),
});

Don't put your email preferences page behind your login: a sign-in redirect can drop the fragment.

Your backend redeems the token with your secret key. The token is a credential, so it goes in the request body, and it stays out of logs and URLs:

cURL
curl -X POST https://api.withflintpay.com/v1/email-preference-links/unsubscribe \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"token": "..."}'
Response
{
  "data": {
    "email": "buyer@example.com",
    "email_preference": "shipping_updates",
    "enabled": false,
    "customer_id": "cus_1kmn0aExample"
  }
}
  • email is the address the email went to, and email_preference is the preference the link controls: shipping_updates or checkout_reminders, the same names as /v1/me/email-preferences. More can be added, so show a general label for one you don't recognize.
  • enabled is the preference's current state for that address. customer_id is null when the address belongs to no customer, such as a guest's.
  • Show the buyer which email they're unsubscribing from, and turn it off when they confirm. Use lookup to render the page and unsubscribe for the button.
  • Use the API key of the environment the link came from, which flint_mode and flint_environment_id name. A token that is malformed, unknown, from another merchant, or from the other mode returns 404 EMAIL_PREFERENCE_LINK_INVALID, and the error never says which.
  • Customer sessions and publishable keys can't call these routes.

Mail apps that offer their own unsubscribe button use the email's one-click List-Unsubscribe-Post header, as RFC 8058 defines. Flint handles those requests directly, so there's nothing for you to build.

A buyer who checked out as a guest before they had an account has orders with their email and no customer. Once they sign in to your account, you can attach those purchases to their customer, but only after Flint confirms they control the email. Never accept a bare email, an order ID, or a customer session as proof.

  1. Request a code#

    cURL
    curl -X POST https://api.withflintpay.com/v1/customer-verifications \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Idempotency-Key: link-guest-cus_1kmn0aExample-1" \
      -H "Content-Type: application/json" \
      -d '{
        "customer_id": "cus_1kmn0aExample",
        "email": "buyer@example.com",
        "purpose": "link_guest_purchases",
        "channel": "email"
      }'
    

    Flint emails a six-digit code from your business name to email when it is the customer's email. The 201 response reads the same whether or not a code was sent:

    Response
    {
      "data": {
        "customer_verification_id": "cver_1kmn0aExample",
        "customer_id": "cus_1kmn0aExample",
        "email": "buyer@example.com",
        "purpose": "link_guest_purchases",
        "channel": "email",
        "status": "pending",
        "expires_at": "2026-10-05T15:19:05Z",
        "created_at": "2026-10-05T15:04:05Z"
      }
    }
    

    purpose must be link_guest_purchases, and channel, when sent, must be email.

  2. Confirm the code the buyer types#

    cURL
    curl -X POST https://api.withflintpay.com/v1/customer-verifications/cver_1kmn0aExample/confirm \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{"code": "482913"}'
    

    The response is the verification with status: "confirmed". Before that it reads pending. status doesn't record expiry or linking. Confirming a code that ran out returns CUSTOMER_VERIFICATION_EXPIRED. Confirming a verification that is already confirmed returns CUSTOMER_VERIFICATION_CODE_INVALID until its code runs out, then CUSTOMER_VERIFICATION_EXPIRED. Linking the same verification twice returns CUSTOMER_VERIFICATION_USED (see below). A code works for 15 minutes and 5 tries. A customer can request 3 codes in 15 minutes and 10 in 24 hours.

  3. cURL
    curl -X POST https://api.withflintpay.com/v1/customers/cus_1kmn0aExample/link-guest-purchases \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Idempotency-Key: link-guest-cus_1kmn0aExample-1-link" \
      -H "Content-Type: application/json" \
      -d '{"customer_verification_id": "cver_1kmn0aExample"}'
    
    Response
    {
      "data": {
        "customer_id": "cus_1kmn0aExample",
        "email": "buyer@example.com",
        "linked_order_count": 3,
        "linked_at": "2026-10-05T15:06:12Z"
      }
    }
    

    Link within 15 minutes of confirming. Flint attaches every order in the same environment whose buyer email is the verified email and that has no customer, with its payments, invoices, Returns, and receipts. A purchase that already belongs to another customer stays where it is. They then appear in /v1/me for that buyer.

A verification links once. Repeat the request with the same Idempotency-Key to get the first result after a lost response; a new key returns CUSTOMER_VERIFICATION_USED. Each linked order sends order.updated. customer.updated is not sent.

Use Flint's account with your own checkout#

You can build your own checkout and keep Flint's hosted account. Leave customer_account.mode as flint_hosted, so Flint's email keeps linking to Flint's account, and connect the two through the customer:

  • When you create a customer, set external_reference_id to your own user ID. To find that customer later, list with GET /v1/customers?external_reference_id=....
  • Set customer_id on the order for a signed-in buyer before checkout, so the purchase appears in their account. For a guest, Flint links the order to the customer with the buyer's email once the payment settles, as Build your own checkout describes.
  • To send a signed-in buyer from your site to their account without a second login, create a customer session for them and redirect to its account_url. It is a one-time link that signs the buyer in to Flint's account, returned only in Flint-hosted mode, and it lasts 15 minutes by default. See Customer sessions.

Develop locally#

In test mode, merchant_account_url can be http://localhost or http://127.0.0.1 with any port, so links in sandbox email open your local account. A loopback URL skips domain registration. Live mode still needs HTTPS on a hostname you own.

The return_url for invoice and Return checkout accepts the same loopback addresses in test mode.

Next steps#

Was this helpful?