Skip to content
Changelog

Changes to the Flint API, SDKs, and docs

Newest first. Flint versions its API by date, and many entries are labeled as breaking changes. New endpoints, fields, and events are listed once a week. The upgrade guide covers what can change under the version you pin, testing a newer version, and rolling back. Follow changes with the RSS feed.
  • API

Workload identity management#

Create and list workload identities at /v1/workload-identities, and retrieve, update, or archive one at /v1/workload-identities/{workload_identity_id}. An identity defines a trusted external OIDC issuer, audience, subject, claim conditions, and scopes. Each identity belongs to one live environment or test sandbox.

Reading requires accounts.workload_identities.read; writes require accounts.workload_identities.write. API keys and CLI OAuth callers can grant only scopes they hold. Dashboard changes that create, reactivate, or widen a live identity require fresh identity verification.

Updates that replace claim_conditions or scopes require expected_version; scalar-only updates can send it to require the version reviewed. Deleting returns the archived resource and frees a place in the merchant's limit of 100 non-archived identities.

See Workload identities for fields, matching rules, and errors.

  • API

Exchange workload identity tokens for API access#

POST /v1/oauth/token now supports urn:ietf:params:oauth:grant-type:token-exchange. After creating a workload identity, send subject_token, subject_token_type, and workload_identity_id; optional scope narrows the access token to a subset of the identity's scopes. Client credentials and unrecognized fields are rejected.

The response contains an opaque fwat_ access token that lasts up to one hour, bounded by the issuer token's remaining lifetime. No refresh token is issued. The identity fixes the merchant, mode, and sandbox. Deactivation, archive, and scope narrowing take effect on the next API request, including for previously issued tokens.

GET /v1/developer/auth-context returns auth_type: workload_identity, with the identity ID, name, and access token expiry. OAuth discovery advertises the new grant when available. See Workload identity token exchange for fields, errors, and limits.

  • API

New in the API: week of October 19, 2026#

517 additions across Inventory, Workload identities, and 52 more areas. These are additions, so nothing you already use needs to change.

Related: Workload identity management, Exchange workload identity tokens for API access, Verification requests use the customer's email and purpose-specific fields, Catalog category filters and modifier rules, Child lookups and resource state use 404 and 409, Configuration prerequisites return ACTION_NOT_ALLOWED, Credit note corrections and available credit, Inventory holds last up to 24 hours, Control Location allocation with a boolean, Weight and dimensions share decimal shapes, OAuth discovery and public signing keys, One response schema per checkout and delivery operation, Fulfillment writes and command results use consistent response shapes, Check buyer return eligibility and control reason availability, Returns use generic validation and action refusal codes, Risk previews check list references and rule limits, Flint mints every request ID, Match subscription offers to external systems and filter by update time, Tax errors distinguish request input from stored configuration, Filter orders with unassigned fulfillment quantity, Product variants support unit weight and dimensions, Delivery rates use variant unit weight, Analytics uses explicit periods and flat metrics, Buyer resource and Return request schemas, Catalog retirement is safe to repeat, Edit active bundles and add options to live products, Filter catalog lists by creation and update time, The checkout credential is one Authorization bearer token, Checkout creation uses an explicit type and shared buyer fields, Default delivery profiles use is_default, Read files and attach images by file ID, Read inventory allocation policy revisions, Inventory movements name their source and recording credential, Inventory transfers replace lines as one collection, List merchant billing entries, Merchant billing uses one API vocabulary, Merchant settings and explicit patch rules, Partner OAuth follows the OAuth standards, Webhooks report closed-order edits and shipment and package changes, Line-item writes use the order version, Partner apps can receive inventory events, Payment and order state refusals use ACTION_NOT_ALLOWED, Payment intent responses share one collection schema, Payment intent create and confirm results, Choose a default payment method on the customer, Payment reviews use explicit names and typed actors, Promotion stacking and rule shapes, Refunds can issue store credit on new gift cards, Report CSVs use API money signs, names, and relationships, Read request logs across callers, Fraud warnings and purchase refund allocations have distinct ID prefixes, Return receipts and inspections have their own versions, Read Return reasons and activity history, Subscription resumes, scheduled starts, and cancellation timing are explicit, Subscription plan resets and checkout payment risk, Subscription state refusals use ACTION_NOT_ALLOWED, Order tickets move to ticketed_event, Webhook deliveries are listed at /v1/webhook-deliveries, Resource version rollout preparation, State refusals include required reasons, and live-mode refusals return FORBIDDEN, Buyer payment responses omit risk, Customer sessions return a resource and support sign-out, Embed dispute responses and use Flint dispute statuses, Throttles and duplicate error codes use shared names, Gift card command results and consistent field names, Hosted partner installs show a consent screen, Inclusive invoice line totals, Request validation uses INVALID_REQUEST, Inventory counts recover from stock that moves, Inventory provenance accepts opaque identifiers and publishes validation rules, Invoice delivery attempts return failure and suppression reasons, Merchant requirement codes and account session targeting, Orders say why they closed, and closing asks before keeping collected money, Order line items report fulfillment quantities, Preparation for partner app updates, Webhook mutations require merchant credentials, Refund create names its type, a recorded refund is always 201, and refund statuses split in two, Returns use release timing and stable adjustment IDs, A failed decline refund reopens the review, and expiry shows as resolving, Risk list checks no longer fail on accepted email addresses, Sandbox resets link archived and replacement sandboxes, Shared invoice credit limits for Returns and credit notes, Enforce published text limits before normalization, Create and import subscriptions from offers, Subscription events cover every change, activation follows the first paid period, and billing-schedule initiated_by is removed, Response warnings use typed fields, Stale versions on catalog, subscription, plan, and risk writes return VERSION_CONFLICT, Stale versions on customer, settings, invoice, and other writes return VERSION_CONFLICT with the current value, Resend pending webhook deliveries and create missing ones, Webhook endpoint status and secret rotation, Build invoice pages with the buyer invoice document, Buyer responses omit merchant inventory, tax configuration, and courier operations, Catalog changes send webhook events, and deleted promotion codes read archived, Read checkout branding, subscription choices, and order tax price mode, Credit note tax and remaining value, Fulfillment event types are complete, and checkout surfaces stay consistent, Hosted invoice links use the invoice ID, Invoice webhooks cover every status change, and paid invoices can reopen, Order sort errors and tax and invoice values match responses, Organization membership changes require existing access, Filter payments that need action and send standalone receipts, Required response fields include nullable values, Filter disputes that need a response and choose their order, Gift card restrictions, a default funding disposition, and a customer filter, One code for records in the way and one for a taken key, Packages dispatch and complete shipments, and the buyer gets one email per moment, Retry PAYMENT_METHOD_NOT_READY with the same Idempotency-Key

Inventory
24 additions
Workload identities
6 additions
Risk controls
7 additions
Files
6 additions
Merchant billing
3 additions
Webhooks
20 additions
Gift cards
14 additions
Payments
11 additions
Customer account
46 additions
Returns
19 additions
Disputes
10 additions
Customers
6 additions
Partner apps
1 addition
Orders
33 additions
Checkout sessions
27 additions
  • Query parameter
    typeon GET /v1/checkout-sessions

    Filter by checkout session type.

  • Request field
    buyer_fieldson POST /v1/checkout-sessions

    What checkout asks the buyer for.

  • Request field
    customer_idon POST /v1/checkout-sessions

    Existing customer to link to the order Flint creates.

  • Request field
    expires_in_secondson POST /v1/checkout-sessions

    Seconds the session stays open, from 60 to 86,400.

  • Request field
    payment_return_urlon POST /v1/checkout-sessions

    Embedded sessions only.

  • Request field
    prefillon POST /v1/checkout-sessions

    Contact details and addresses to fill in for the buyer, who can change them.

  • Request field
    typeon POST /v1/checkout-sessions

    What the checkout collects payment for. order: an order you created; send order_id. quick_pay: one named amount; send quick_pay_item, and Flint creates the order. subscription_plan: a signup for an active plan; send subscription_plan_id, and Flint creates the order and, once the buyer pays, the subscription.

  • Field

    Always present with an API key or dashboard credential.

Analytics
26 additions
  • Query parameter
    period_end_aton GET /v1/analytics/overview

    Exclusive period end as an RFC 3339 instant.

  • Query parameter
    period_start_aton GET /v1/analytics/overview

    Inclusive period start as an RFC 3339 instant.

  • Query parameter
    granularityon GET /v1/analytics/payment-volume-timeseries

    Bucket size: hour, day (default), week starting Monday, or month.

  • Query parameter
    period_end_aton GET /v1/analytics/payment-volume-timeseries

    Exclusive period end as an RFC 3339 instant.

  • Query parameter
    period_start_aton GET /v1/analytics/payment-volume-timeseries

    Inclusive period start as an RFC 3339 instant.

  • Query parameter
    currencyon GET /v1/analytics/subscriptions

    ISO 4217 currency for every amount and payments_count.

  • Query parameter
    period_end_aton GET /v1/analytics/subscriptions

    Exclusive period end as an RFC 3339 instant.

  • Query parameter
    period_start_aton GET /v1/analytics/subscriptions

    Inclusive period start as an RFC 3339 instant.

API request logs
25 additions
  • Query parameter
    caller_idon GET /v1/developer/request-logs

    Filter by the exact public ID of the caller.

  • Query parameter
    caller_typeon GET /v1/developer/request-logs

    Filter by one or more caller resource types.

  • Query parameter
    client_request_idon GET /v1/developer/request-logs

    Filter by the exact, case-sensitive X-Request-Id you sent.

  • Query parameter
    client_trace_idon GET /v1/developer/request-logs

    Filter by a W3C traceparent trace ID, 32 lowercase hexadecimal characters, excluding all zeros.

  • Field

    The public resource that made this request.

  • Field

    The caller's X-Request-Id, matched exactly and case-sensitively.

  • Field

    The trace ID from a valid W3C traceparent, for correlation with the caller's tracing.

  • Field

    Normalized expand paths Flint applied.

Products
17 additions
  • Query parameter
    created_afteron GET /v1/products/{product_id}/variants

    Inclusive lower bound on created_at.

  • Query parameter
    created_beforeon GET /v1/products/{product_id}/variants

    Exclusive upper bound on created_at.

  • Query parameter
    updated_afteron GET /v1/products/{product_id}/variants

    Inclusive lower bound on updated_at.

  • Query parameter
    updated_beforeon GET /v1/products/{product_id}/variants

    Exclusive upper bound on updated_at.

  • Request field
    existing_variant_valueon PATCH /v1/products/{product_id}

    Value of this option given to every active variant that has no active value for it after this request, such as when you add an option to a product that already has active variants.

  • Request field
    unit_dimensionson PATCH /v1/products/{product_id}/variants/{variant_id}

    Replaces the complete object.

  • Request field
    unit_weighton PATCH /v1/products/{product_id}/variants/{variant_id}

    Replaces the complete object.

  • Request field
    file_idon POST /v1/bundles

    The ID of a ready file uploaded with POST /v1/files.

Payment links
16 additions
Promotions
16 additions
  • Query parameter
    availabilityon GET /v1/promotion-codes

    Filter by whether a code can be redeemed now or why it cannot. available matches available: true; every other value matches unavailable_reason.

  • Query parameter
    created_afteron GET /v1/promotion-codes

    Inclusive lower bound on created_at.

  • Query parameter
    created_beforeon GET /v1/promotion-codes

    Exclusive upper bound on created_at.

  • Query parameter
    sort_byon GET /v1/promotion-codes

    Field to sort by.

  • Query parameter
    sort_directionon GET /v1/promotion-codes

    Sort direction.

  • Query parameter
    updated_afteron GET /v1/promotion-codes

    Inclusive lower bound on updated_at.

  • Query parameter
    updated_beforeon GET /v1/promotion-codes

    Exclusive upper bound on updated_at.

  • Query parameter
    availabilityon GET /v1/promotions

    Filter by whether a promotion can apply now or why it cannot. available matches available: true; every other value matches unavailable_reason.

Subscriptions
15 additions
Modifiers
12 additions
  • Query parameter
    created_afteron GET /v1/modifier-groups

    Inclusive lower bound on created_at.

  • Query parameter
    created_beforeon GET /v1/modifier-groups

    Exclusive upper bound on created_at.

  • Query parameter
    sort_byon GET /v1/modifier-groups

    Field to sort by.

  • Query parameter
    sort_directionon GET /v1/modifier-groups

    Sort direction.

  • Query parameter
    updated_afteron GET /v1/modifier-groups

    Inclusive lower bound on updated_at.

  • Query parameter
    updated_beforeon GET /v1/modifier-groups

    Exclusive upper bound on updated_at.

  • Query parameter
    created_afteron GET /v1/modifier-sets

    Inclusive lower bound on created_at.

  • Query parameter
    created_beforeon GET /v1/modifier-sets

    Exclusive upper bound on created_at.

Partner OAuth
9 additions
  • Query parameter
    code_challengeon GET /v1/oauth/authorize

    Base64url-encoded SHA-256 hash of your code_verifier, without padding (RFC 7636).

  • Query parameter
    code_challenge_methodon GET /v1/oauth/authorize

    PKCE challenge method.

  • Query parameter
    sandbox_idon GET /v1/oauth/authorize

    Optional active merchant sandbox ID, accepted only with mode=test.

  • Query parameter
    scopeon GET /v1/oauth/authorize

    Space-separated permission IDs from your app's permission manifest.

  • Query parameter
    code_challengeon GET /v1/oauth/authorize/preview

    Base64url-encoded SHA-256 hash of your code_verifier, without padding (RFC 7636).

  • Query parameter
    code_challenge_methodon GET /v1/oauth/authorize/preview

    PKCE challenge method.

  • Query parameter
    sandbox_idon GET /v1/oauth/authorize/preview

    Optional active merchant sandbox ID, accepted only with mode=test.

  • Query parameter
    scopeon GET /v1/oauth/authorize/preview

    Space-separated permission IDs from your app's permission manifest.

Subscription offers
8 additions
  • Query parameter
    created_afteron GET /v1/subscription-offers

    Inclusive lower bound on created_at.

  • Query parameter
    created_beforeon GET /v1/subscription-offers

    Exclusive upper bound on created_at.

  • Query parameter
    external_reference_idon GET /v1/subscription-offers

    Exact-match filter on the caller-owned external reference ID.

  • Query parameter
    updated_afteron GET /v1/subscription-offers

    Inclusive lower bound on updated_at.

  • Query parameter
    updated_beforeon GET /v1/subscription-offers

    Exclusive upper bound on updated_at.

  • Request field
    external_reference_idon PATCH /v1/subscription-offers/{subscription_offer_id}

    Caller-owned identifier for this resource in an external system.

  • Request field
    external_reference_idon POST /v1/subscription-offers

    Caller-owned identifier for this resource in an external system.

  • Field

    Caller-owned identifier for this resource in an external system.

Bundles
7 additions
  • Query parameter
    created_afteron GET /v1/bundles

    Inclusive lower bound on created_at.

  • Query parameter
    created_beforeon GET /v1/bundles

    Exclusive upper bound on created_at.

  • Query parameter
    updated_afteron GET /v1/bundles

    Inclusive lower bound on updated_at.

  • Query parameter
    updated_beforeon GET /v1/bundles

    Exclusive upper bound on updated_at.

  • Event

    A bundle was archived.

  • Event

    A bundle was created.

  • Event

    A bundle's own fields, images, categories, modifier set or components changed.

Categories
7 additions
  • Query parameter
    created_afteron GET /v1/categories

    Inclusive lower bound on created_at.

  • Query parameter
    created_beforeon GET /v1/categories

    Exclusive upper bound on created_at.

  • Query parameter
    handleon GET /v1/categories

    Exact handle match.

  • Query parameter
    sort_byon GET /v1/categories

    Field to sort by.

  • Query parameter
    sort_directionon GET /v1/categories

    Sort direction.

  • Query parameter
    updated_afteron GET /v1/categories

    Inclusive lower bound on updated_at.

  • Query parameter
    updated_beforeon GET /v1/categories

    Exclusive upper bound on updated_at.

OAuth
7 additions
Refunds
7 additions
Sandboxes
7 additions
Merchants
6 additions
Return resolutions
6 additions
Fulfillment records
5 additions
Invoices
5 additions
Locations
4 additions
Return dispositions
4 additions
Return inspections
4 additions
Return reasons
4 additions
  • Query parameter
    handleon GET /v1/return-reasons

    Exact, case-sensitive match on the reason's handle, for Flint and merchant reasons.

  • Request field
    statuson PATCH /v1/return-reasons/{return_reason_id}

    Set active to offer the reason or inactive to stop offering it.

  • Value
  • Value
Return receipts
4 additions
Credit notes
3 additions
Customer sessions
3 additions
CLI OAuth
2 additions
  • Request field
    modeon POST /v1/oauth/device/authorize

    string

  • Request field
    modeon POST /v1/oauth/device/reauthorize

    string

Delivery configuration
2 additions
  • Query parameter
    is_defaulton GET /v1/delivery-profiles

    Filter by whether the profile is the current physical catalog default.

  • Request field
    is_defaulton PATCH /v1/delivery-profiles/{delivery_profile_id}

    Send true to make this active profile the physical catalog default.

Delivery previews
2 additions
Delivery quotes
2 additions
Money movement
2 additions
Payment methods
2 additions
  • Field

    A value that is the same for every saved copy of one card number at this merchant and environment, so you can spot duplicates.

  • Field

    Whether this is the customer's default payment method, the one Customer.default_payment_method_id names.

Return policies
2 additions
Settings
2 additions
Subscription plans
2 additions
  • Query parameter
    updated_afteron GET /v1/subscription-plans

    Inclusive lower bound on updated_at.

  • Query parameter
    updated_beforeon GET /v1/subscription-plans

    Exclusive upper bound on updated_at.

Delivery selections
1 addition
Onboarding
1 addition
Organizations
1 addition
Webhook events
33 additions
  • Field
    DeliveryProfile.is_default

    True only when this profile is the current physical catalog default.

  • Field
    FraudWarning.is_actionable

    boolean

  • Field
    FraudWarning.updated_at

    RFC3339 timestamp.

  • Field
    InventoryReservation.expires_at

    When held quantity is released if it isn't committed first. null once no line holds quantity.

  • Field
    InventoryReservation.supported_actions

    Actions that can change this reservation now.

  • Field
    InventoryTransfer.external_reference_id

    Caller-owned identifier for this resource in an external system.

  • Field
    InventoryTransferLine.condition

    string

  • Field
    MerchantBillingBalance.currency

    ISO 4217 currency code.

Errors
3 additions
  • API
  • Customers
  • Checkout

Verification requests use the customer's email and purpose-specific fields#

POST /v1/customer-verifications no longer takes email. Flint sends the code to the customer's current email and returns that address in email. A customer without an email returns 409 ACTION_NOT_ALLOWED, with param: customer_id and reason configuration_required. No verification or email delivery is created for that request.

POST /v1/checkout-sessions/{checkout_session_id}/customer-verifications now returns 200 instead of 201. Its request schema is a closed oneOf on purpose: gift_card_purchase and save_payment_method require email and accept only channel email; use_saved_payment_methods requires email and accepts email, sms, or auto; confirm_saved_payment_method requires channel email or sms and accepts no email input. Existing channel defaults, rate limits, and verification behavior are unchanged.

Customer verifications prove ownership for linking guest purchases through a merchant's backend. Checkout verifications bind a buyer inside one checkout and use only that session's checkout credential.

These changes apply to every supported API version.

  • Breaking change
  • API
  • Catalog
  • Promotions

Catalog category filters and modifier rules#

GET /v1/categories accepts an exact handle filter. Handles are lowercase and matching is case-sensitive. Category references include the stable category_id; legacy snapshots without an ID are omitted.

Breaking: list modifier groups use min_selected and max_selected; text groups use required. A set override for the other group type returns 400 INVALID_REQUEST with details[0].code: "NOT_ALLOWED". For a list group, the set's min_selected overrides the group's value and defaults to zero when neither is set. For a text group, the set's required overrides text.required.

Breaking: modifier overrides and payment link options no longer include hidden. Send available: false to make a choice unavailable. Previously stored hidden choices remain unavailable. Promotion application methods no longer accept or return applies_to. Sending either removed request field returns 400 INVALID_REQUEST with details[0].code: "UNKNOWN_FIELD".

Breaking: variants and bundle components no longer return current_delivery_profile_revision_id. Read the current revision from the delivery profile.

Catalog request schemas publish string length limits and category handle patterns. Product variant and bundle SKU and barcode fields accept up to 100 characters. Longer values return 400 INVALID_REQUEST with details[0].code: "INVALID_VALUE" before saving.

  • Breaking change
  • API

Child lookups and resource state use 404 and 409#

Missing child IDs now return 404 RESOURCE_NOT_FOUND with param, resource_type, and resource_id. This covers bundle components, product options and values, payment link line items and custom fields, subscription plan line items, order line-item modifiers, order line items, and order charges. A missing default variant, saved payment method, or deleted parent organization also returns 404.

An inactive default variant or saved payment method returns 409 ACTION_NOT_ALLOWED with the resource reference, current status, and reason. A refund for an existing line item or charge with nothing refundable returns 409 AMOUNT_EXCEEDS_REFUNDABLE. A missing fulfillment obligation returns 404; an exhausted obligation returns 409 FULFILLMENT_QUANTITY_EXCEEDS_AVAILABLE.

Gift card code lookups return the same 400 INVALID_REQUEST with detail INVALID_VALUE on code for unknown, other-merchant, and inactive codes. An allocation that is no longer available returns 409 GIFT_CARD_ALLOCATION_CHANGED; refresh the order and review its allocation before paying again.

Order composition that cannot start a subscription returns 400 INVALID_REQUEST with detail NOT_ALLOWED. Malformed payment sources and self-parent organization references use INVALID_VALUE.

Replace branches on the retired validation codes with RESOURCE_NOT_FOUND and its resource facts, or the relevant conflict code. For request-only failures, branch on INVALID_REQUEST and read details[].param and details[].code. Retained failures use the same public rewrites.

  • Breaking change
  • API

Configuration prerequisites return ACTION_NOT_ALLOWED#

Missing configuration on a saved resource now returns 409 ACTION_NOT_ALLOWED with reason configuration_required. Update the resource named by resource_type and resource_id when available, then retry with a new Idempotency-Key. These refusals omit current_status.

This replaces PRODUCT_OPTIONS_REQUIRED, ORDER_CURRENCY_REQUIRED, INVOICE_TIMEZONE_REQUIRED, CHECKOUT_RECOVERY_EMAIL_ADDRESS_REQUIRED, and FULFILLMENT_PROFILE_REQUIRED. The stored-state branches of LINE_ITEM_PRICE_REQUIRED, INVENTORY_ROUTING_SOURCE_REQUIRED, and INVOICE_PAYMENT_OPTIONS_REQUIRED use the same refusal.

DELIVERY_RECIPIENT_REQUIRED also becomes 409 ACTION_NOT_ALLOWED on the delivery_selection. Its update_checkout_session next action lists required_fields. Add the missing recipient details, create a replacement selection, then retry payment. Orders without a currency include the add_line_items next action.

Missing prices, inventory routing sources, or invoice payment options that the request can supply return 400 INVALID_REQUEST with detail REQUIRED. Branch on INVALID_REQUEST, then read details[].param and details[].code to correct the request. Branch on ACTION_NOT_ALLOWED and its reason to handle saved configuration.

  • Breaking change
  • API
  • Credit-notes

Credit note corrections and available credit#

Breaking: credit note lines now return correction in the same shape accepted by draft writes. Replace top-level line quantity reads with correction.quantity for quantity corrections. Amount corrections return correction.amount_money, equal to the line's total_money. To update a draft from a read, send each line's credit_note_line_item_id, invoice_line_item_id, and correction, with expected_version set to the credit note's current version.

available_money replaces unallocated_money and reports credit that can still be allocated or refunded. It is zero on draft and void notes. The new allocated_money reports credit applied to the invoice, net of reversed allocations. On issued notes:

total_money = allocated_money + pending_refund_money + refunded_money + available_money

memo and external_reference_id are always present on merchant credit notes and are null when unset. Buyer credit notes also always include memo, with null when unset. Buyer lines use the same correction shape.

POST /v1/credit-notes/{credit_note_id}/issue now returns data.credit_note and data.invoice, plus data.refund when a refund was requested. Read the issued credit note and its version from data.credit_note instead of directly from data. Issuing without a refund still leaves allocation to a separate call.

  • Breaking change
  • API
  • Inventory

Inventory holds last up to 24 hours#

Breaking: a reservation's hold deadline is now the top-level expires_at, replacing owner.expires_at. It is null once no line holds quantity, such as after a full commit, release, or expiry. Committed and consumed quantity never expire.

When you create a reservation, send expires_at up to 24 hours from now, or omit it for the 15-minute default. A deadline that isn't after now, or is more than 24 hours out, returns 400 INVALID_REQUEST with param expires_at. The deadline used to be limited to 15 minutes, and it moved from owner to the top level for both merchant and order owners.

PATCH /v1/inventory-reservations/{inventory_reservation_id} with a new expires_at extends or shortens a merchant hold before it lapses. It accepts an optional expected_version, increments the reservation's version, and returns the reservation. A lapsed hold can't be extended: the PATCH returns 409 ACTION_NOT_ALLOWED, as it does for a closed reservation or one with nothing held. A reservation Flint holds for an order returns 403 INVENTORY_RESERVATION_OWNER_MISMATCH.

Breaking: the list filters owner_expires_after and owner_expires_before are now expires_after and expires_before. They match only reservations that currently hold quantity, so a reservation that is fully committed no longer appears in an expires_before query for deadlines that have passed.

Breaking: the commit, consume, and release routes for reservations and the apply and cancel routes for counts are removed. Send POST /v1/inventory-reservations/{inventory_reservation_id}/transitions with action set to commit, consume, or release, and POST /v1/inventory-counts/{inventory_count_id}/transitions with action set to apply or cancel. A request that carries another action's fields returns 400 INVALID_REQUEST at that field. Resending reservation targets that are already applied still succeeds in any status.

Reservations and counts now report supported_actions, the actions that can change them right now. An action that isn't available returns 409 ACTION_NOT_ALLOWED with supported_actions in details[0], and applying an incomplete count returns 409 INVENTORY_COUNT_INCOMPLETE with the count's current_status.

Breaking: the OpenAPI document replaces the operations commitInventoryReservation, consumeInventoryReservation, releaseInventoryReservation, applyInventoryCount, and cancelInventoryCount with transitionInventoryReservation and transitionInventoryCount, and adds updateInventoryReservation. The schemas ReservationLine, CountProvenance, InventoryReservationProvenance, and InventoryTransferProvenanceInput are now InventoryReservationLine, InventoryCountObservationProvenance, and one shared InventoryProvenanceInput. Regenerate clients from the updated document.

The Inventory reference covers deadlines, expiry filters, and transitions, and the inventory guide walks through a hold from creation to consumption.

  • Breaking change
  • API
  • Inventory

Control Location allocation with a boolean#

Breaking: inventory_allocation_enabled replaces inventory_allocation_status on Locations and their create and update requests. Send true to enable inventory allocation and false to disable it. The default stays false, including for Locations without an allocation configuration. Setting the field, including false, still requires commerce.inventory_locations.write in addition to the route's scope.

Filter GET /v1/locations with inventory_allocation_enabled=true or inventory_allocation_enabled=false. This filter accepts one boolean value. The false filter includes Locations without an allocation configuration. Replayed Location responses also return the boolean field.

INVENTORY_LOCATION_ALLOCATION_DISABLED replaces INVENTORY_LOCATION_ALLOCATION_INACTIVE. To accept new inventory claims, a Location must be active and have inventory_allocation_enabled: true.

Inactive Locations can still record physical stock changes through adjustments, counts, transfers, receipts, and safety stock updates. Those commands reject archived Locations. Routing rejects both inactive and archived Locations. Updating an address still requires coordinate in the same request; send null to clear it.

  • Breaking change
  • API

Weight and dimensions share decimal shapes#

Breaking: Packages, delivery profile configuration, and line-item fulfillment inputs now use the same Weight and Dimensions shapes. The separate ShippingWeight, ShippingDimensions, and line-input measurement schemas have been removed.

Weight values must be greater than zero and at most 1,000,000. Each dimension must be greater than zero and at most 100,000. Both accept up to three decimal places. Values with more precision are rejected, not rounded. Supported units are unchanged.

Send every required member when replacing a weight or dimensions object on PATCH. Stored measurements keep the value and unit you send. Regenerate clients from the updated OpenAPI document.

  • API

OAuth discovery and public signing keys#

Flint now serves the same discovery document at /.well-known/openid-configuration and /.well-known/oauth-authorization-server on the API host. It advertises the available OAuth endpoints and grants, CLI scopes, and the jwks_uri for Flint's public signing keys.

Retrieve the key set at /.well-known/jwks.json. All three documents allow public browser access and can be cached for 15 minutes. JWKS supports revalidation with ETag and If-None-Match.

See OAuth discovery and signing keys for request examples and verification guidance.

  • Breaking change
  • API
  • Checkout
  • Delivery

One response schema per checkout and delivery operation#

Checkout session and delivery operations now publish one response schema for API keys, dashboard credentials, and checkout credentials. Delivery responses no longer include audience. The separate Buyer* delivery schemas and credential-selected response unions have been removed. Update generated type references when regenerating your SDK.

Fields a credential never receives carry x-flint-omitted-for-auth. These fields are optional in the shared schema, including merchant-only fields on merchant-only routes that reuse it. Request fields and request branches a credential cannot send carry x-flint-rejected-for-auth; sending them still returns the operation's existing error code. Both annotations list the affected authentication schemes. The API reference shows them beside each field and request branch, for example "Not returned with a checkout credential" or "Not accepted with a checkout credential".

Merchant delivery quotes and checkout fulfillment summaries now include buyer_reasons, and their choice groups include address_advisories when available. Checkout credentials keep delivery_method_ids on pickup previews and receive the same field in shared delivery input requirements on quotes, selections, and fulfillment summaries. The checkout session already returns these IDs in CheckoutSession.delivery_method_ids. Other checkout-credential delivery fields remain the same, apart from the removal of audience.

  • Breaking change
  • API
  • Fulfillment
  • Risk
  • Orders
  • Subscriptions
  • Invoices

Fulfillment writes and command results use consistent response shapes#

Fulfillment writes return their subject#

Package create, update, transition, and void now return Package directly. Fulfillment transitions return Fulfillment. Fulfillment event creation returns FulfillmentEvent: 201 creates an event, and 200 returns the same provider event without creating another. The replayed and unchanged body flags are removed. Idempotency-Key replays continue to use the response header and meta.idempotency_replayed.

Before, a package command returned:

JSON
{"data":{"package":{"package_id":"pkg_123"},"events":[],"fulfillment_notifications":[],"unchanged":false}}

After:

JSON
{"data":{"package_id":"pkg_123"}}

These examples abbreviate resource fields. Shipment void still returns a result because it also voids the shipment's packages. Its members are exactly shipment and packages:

JSON
{"data":{"shipment":{"shipment_id":"shp_123"},"packages":[]}}

Read events through GET /v1/fulfillment-events, filtered by package_id, shipment_id, or fulfillment_id. Read notifications through GET /v1/fulfillment-notifications, filtered by fulfillment_event_id or fulfillment_id.

Risk list additions use values and outcomes#

POST /v1/risk-lists/{risk_list_id}/items accepts only values, an array of 1 to 500 strings. Each string must contain 1 to 512 characters. A request using value now returns 400 INVALID_REQUEST with an UNKNOWN_FIELD detail naming value.

Before:

JSON
{"value":"blocked@example.com"}

After:

JSON
{"values":["blocked@example.com"]}

The response changes from items[].status to additions[].outcome. Each addition includes its full risk_list_item. Entries remain in request order; a repeated value reports existing after its first entry.

Before:

JSON
{"data":{"items":[{"status":"created","risk_list_item":{"risk_list_item_id":"rli_123"}}]}}

After:

JSON
{"data":{"additions":[{"outcome":"created","risk_list_item":{"risk_list_item_id":"rli_123"}}]}}

The status remains 201 when any entry is created, or 200 when every entry already exists.

Command results name their resources#

Order pay, order payment attempt cancellation, and merchant and buyer subscription renewal results rename payment_attempt to order_payment_attempt. Renewal results keep the subscription as their primary resource.

Before:

JSON
{"data":{"order":{"order_id":"ord_123"},"payment_attempt":{"order_payment_attempt_id":"opa_123"}}}

After:

JSON
{"data":{"order":{"order_id":"ord_123"},"order_payment_attempt":{"order_payment_attempt_id":"opa_123"}}}

Invoice collection and Return processing remove the result-level idempotency_key. Read the Idempotency-Key response header instead. Resource-level idempotency keys are unchanged.

Before:

JSON
{"data":{"invoice":{"invoice_id":"inv_123"},"invoice_payment_attempt":null,"idempotency_key":"collect-123"}}

After:

JSON
{"data":{"invoice":{"invoice_id":"inv_123"},"invoice_payment_attempt":null}}

Invoice commands return Invoice#

Invoice issue, send-reminder, and regenerate-public-link return Invoice directly. The public payment URL is data.public_url. Delivery attempts remain available through GET /v1/invoices/{invoice_id}/delivery-attempts.

Before:

JSON
{"data":{"invoice":{"invoice_id":"inv_123"},"public_url":"https://checkout.example.com/i/token","delivery_attempt":null}}

After:

JSON
{"data":{"invoice_id":"inv_123","public_url":"https://checkout.example.com/i/token"}}

The existing 24-hour replay limit for recovering public_url is unchanged.

  • API
  • Returns

Check buyer return eligibility and control reason availability#

Eligibility previews now return can_buyer_request instead of self_service_enabled. Use it to decide whether a buyer can request the line through a customer session. It accounts for policy eligibility, remaining quantity, and the store's available reasons. A policy exclusion includes ineligibility_reason and a specific reason_message. Final-sale exclusions take precedence over the policy's return window.

Return reasons support active, inactive, and archived. Set a Flint or merchant reason to inactive with PATCH /v1/return-reasons/{return_reason_id}, then set it back to active to offer it again. Flint reasons accept only status changes and cannot be archived. Existing Returns keep their recorded reason name and handle.

Find a reason with the exact, case-sensitive handle filter on GET /v1/return-reasons. Lists include active and inactive reasons by default; customer sessions see only active reasons. Flint handles and existing merchant handles, including archived reasons, are reserved. Creating a conflicting handle returns 409 ALREADY_EXISTS.

A reason's category_handles are catalog category handles from GET /v1/categories. Customer sessions may select a scoped reason only for matching products. Merchant and integration callers may select any active reason. Every new Return line still requires a reason.

Eligibility selection lines now use ReturnEligibilityLineItemInput: order_line_item_id and requested_quantity are required, return_reason_id is optional, and buyer_note is no longer accepted. Policy automatic_resolution_type accepts only refund and no_monetary_action. A line's requested_resolution_type excludes correction, and Return.initiated_by excludes system; initiation remains determined by the caller's credential.

Resolution previews no longer return the always-empty warnings array. Resolution and disposition failure_code fields publish extensible vocabularies with descriptions. Dispositions use inventory_effect_failed and inventory_item_unmapped. A failed resolution offers retry only when its failure code is retryable, and POST /v1/return-resolutions/{return_resolution_id}/retry on any other code returns 409 ACTION_NOT_ALLOWED. For refund_line_value_unavailable, cancel the resolution and resolve fewer units or choose an outcome without a refund.

When canceling approved line quantity, omit handback_quantity to reduce the expected handback only as far as the canceled quantity requires. An explicit value cannot leave return_required_quantity above the remaining approved quantity.

  • Breaking change
  • API
  • Returns
  • Errors

Returns use generic validation and action refusal codes#

Breaking: branch on ACTION_NOT_ALLOWED and details[0].reason for Returns state refusals. This 409 response replaces RETURN_ACTION_NOT_ALLOWED. Details identify the resource whose state refuses the action, including a line item when its verification or acceptance status is the cause. Resources that publish supported_actions include them in the refusal.

Returns refusals use already_applied for completed actions, archived for archived reasons and policies, terminal_status for final Returns, status_not_reached when a required lifecycle step is pending, and unsupported_state for other state refusals. A resolution that already created a refund uses refund_created and offers get_refund in next_actions. refund_unavailable identifies the order whose payment cannot be refunded and omits current_status. Every reason has remediation.retry: none. For an unknown reason, fall back to current_status and choose from supported_actions when present.

Breaking: 400 INVALID_REQUEST replaces RETURN_INVALID and RETURN_FIELD_NOT_ALLOWED_FOR_BUYER. Branch on the detail code: REQUIRED for a missing required field, INVALID_VALUE for an invalid value, UNKNOWN_FIELD for a field the request does not accept, and NOT_ALLOWED for a field unavailable to the buyer. Sending a path field in the body returns UNKNOWN_FIELD; omitting a policy revision's expected_version returns REQUIRED.

Idempotency conflicts return IDEMPOTENCY_KEY_REUSED consistently across Returns transports. Missing internal dependencies return 500 INTERNAL_ERROR. Retained idempotency errors replay with the new public codes. Update error handling and regenerate SDKs from the published OpenAPI schema.

  • Breaking change
  • API
  • Risk

Risk previews check list references and rule limits#

Risk previews now check missing, archived, and incompatible lists, return RULE_RESERVED for proposed policy changes to system rules, and warn with risk_rule_limit_reached when a new enabled rule would exceed the limit.

Preview issue codes use lower_snake_case and paths use public field names, such as predicate.all[0].list_alias or action. Call POST /v1/risk-previews for typed issues. For rule writes, branch on INVALID_REQUEST and read details[].param and details[].code.

Archived list references now return 409 ACTION_NOT_ALLOWED with the list ID and reason archived. Missing aliases return 404 RESOURCE_NOT_FOUND with the alias field path and no resource_id. The enabled-rule error is now RISK_RULE_LIMIT_REACHED, replacing RULE_LIMIT_EXCEEDED, and includes details[0].limit.

  • Breaking change
  • API
  • Developers
  • Webhooks

Flint mints every request ID#

Flint now mints every request ID: X-Request-Id, request_id, and error.request_id are req_ followed by 26 characters, unique per request. Send your own X-Request-Id (1 to 128 visible ASCII characters, sent once) and Flint stores it on the request log as client_request_id, which the request-log list filters by. A valid W3C traceparent stores its trace ID as client_trace_id.

error.request_log_url is now the log's detail path, and API keys and CLI tokens with developer.request_logs.read or developer.request_logs.self.read receive it when their request will be logged. Flint writes the log just after responding, so this path can briefly return 404.

Breaking: X-Request-Id is now always Flint's ID; your value is stored as client_request_id. Flint no longer echoes a caller's X-Request-Id, and an invalid or repeated one returns 400 INVALID_REQUEST with param: "header.X-Request-Id" and reason INVALID_VALUE. The request_id list filter accepts only Flint request IDs. Webhook events drop correlation_id and its filter; use api_request_log_id. Public requests no longer continue the caller's trace, so meta.trace_id is Flint's own.

  • API
  • Subscriptions

Match subscription offers to external systems and filter by update time#

Subscription offers now accept external_reference_id on create and update. Use it to link an offer to a selling plan in another system. The value is optional, opaque, and limited to 255 characters. On PATCH, omit it to keep the current value or send null to clear it. An empty string is invalid.

GET /v1/subscription-offers supports an exact, case-sensitive external_reference_id filter. Its query searches name, subscription_offer_id, and external_reference_id. It also supports created_after, created_before, updated_after, and updated_before.

GET /v1/subscription-plans now supports updated_after and updated_before. All these timestamp ranges include the lower bound and exclude the upper bound.

Offers and plans now publish the same SubscriptionIntervalOption schema for billing_interval_options. The shared schema explicitly rejects unknown fields and publishes billing_interval_count as an int32 integer from 1 to 365. Offer update descriptions now name the public VERSION_CONFLICT error for a stale expected_version.

  • Breaking change
  • API

Tax errors distinguish request input from stored configuration#

Missing tax input in a request now returns 400 INVALID_REQUEST with detail code REQUIRED and the request path in param.

Missing tax input on a stored order, line item, or charge now returns 409 ACTION_NOT_ALLOWED with reason configuration_required. Read details[].resource_type and resource_id, update that resource as the message describes, then retry. param no longer names tax fields absent from the request. This applies to required taxability, external calculations and components, and the order's tax location or full address.

Replace branches on ORDER_LINE_ITEM_TAX_INPUT_REQUIRED, ORDER_CHARGE_TAX_INPUT_REQUIRED, TAX_CALCULATION_REQUIRED, EXTERNAL_TAX_COMPONENTS_REQUIRED, ORDER_TAX_LOCATION_REQUIRED, and ORDER_TAX_FULL_ADDRESS_REQUIRED with the status, public code, and detail fields above. Retained failures use the same distinction when replayed.

  • API
  • Orders
  • Fulfillment

Filter orders with unassigned fulfillment quantity#

Use GET /v1/orders?has_unassigned_fulfillment_quantity=true to find orders with required fulfillment quantity that no fulfillment owns. The filter uses the sum of each line's fulfillment_quantities.unassigned_quantity, including bundle components in component units.

Set the filter to false to find orders with no unassigned quantity, including orders that need no fulfillment. A failed fulfillment returns its quantity to this queue only when it releases that quantity. Combine the filter with other order filters and keep it unchanged when following page tokens.

This filter is available to merchants on /v1/orders and is not accepted on /v1/me/orders.

  • API

Product variants support unit weight and dimensions#

Product variants now accept optional unit_weight and unit_dimensions on create, including variants created inline with a product. They use the shared Weight and Dimensions shapes and preserve the value and unit you send.

On variant PATCH, send a complete object to replace a measurement, omit it to leave it unchanged, or send null to clear it. Reads omit measurements that are unset.

  • Breaking change
  • API

Delivery rates use variant unit weight#

Breaking: Delivery quotes now use the variant's unit_weight at the time the order line is created. Bundle lines use each component variant's unit weight and quantity. Later variant edits leave existing order-line measurements unchanged. Subscription delivery previews and renewals use the current variant measurements.

Delivery profile weight and dimensions remain accepted for now, but no longer supply quote weight. Custom lines continue to use their fulfillment.weight and fulfillment.dimensions inputs.

Delivery rate callbacks omit candidates[].pricing.total_weight_grams when any obligation in the group has unknown weight. Handle an absent value separately from zero grams. Methods that require weight remain unavailable for groups with unknown weight.

  • Breaking change
  • API
  • Analytics

Analytics uses explicit periods and flat metrics#

Breaking: all three /v1/analytics/* routes now require period_start_at and accept an optional period_end_at. The period includes its start and excludes its end. An omitted or future end resolves to the request time, truncated to the second. Periods can span at most 400 days, or 32 days with hourly buckets.

  • range and include_previous_period are removed. Request a second period to compare values. Responses no longer contain previous_*, change_percent, has_multiple_currencies, or bucket label fields.
  • Payment volume timeseries accepts granularity=hour|day|week|month, defaulting to day. Weeks start Monday. Buckets follow local boundaries in timezone and are clipped to the requested period. Empty buckets are included. A repeated local hour has two buckets; skipped hours and days have none.
  • timezone is accepted only by the timeseries route. Sending it to overview or subscriptions returns 400 UNSUPPORTED_QUERY_PARAM.
  • Overview returns gross_volume_money, refunded_money, payments_count, and new_customers_count. Metric wrappers, net_volume, average_payment, and new_subscriptions are removed. Derive net volume and average payment from the returned amounts and payment count.
  • Subscription window metrics are new_subscriptions_count, canceled_subscriptions_count, and collected_money. Snapshot metrics are mrr_money, arr_money, and status_counts. New subscriptions include only subscriptions created in the period whose current status is trialing, active, paused, or past_due.
  • Subscriptions now accepts currency, defaulting to your account's default currency. All routes return currency and currencies. Amounts and payment counts use the selected currency; customer and subscription counts span currencies. Subscription snapshots reflect current state, independently of the requested period.
  • API
  • Customer-accounts
  • Returns

Buyer resource and Return request schemas#

The /v1/me schemas now describe the resources buyers receive, including subscriptions, shipments, packages, payment methods, Returns, and nested checkout and renewal results. Generated SDK types omit merchant-only and expansion-only fields. Examples and error lists match the buyer operations.

Buyer Return creation accepts order_id and line_items. Resolution previews accept catalog variants or bundles as replacement items, without merchant pricing overrides or adjustments. The buyer resolution preview no longer accepts expected_version. Read based_on_return_version in the result for the evaluated snapshot.

The origin, billing_schedule_owner, needs_attention, and hold_reason filters remain available. Buyer operations continue to reject expand.

Return image objects no longer include an undocumented internal field.

Return bundle components now use the published delivery_configuration_status, delivery_configuration_reason, delivery_profile_id, and current_delivery_profile_revision_id names instead of undocumented fulfillment_* names.

Return modifiers now use the published modifier_group_name instead of the undocumented group_name.

  • Breaking change
  • API
  • Catalog
  • Promotions

Catalog retirement is safe to repeat#

DELETE archives products, variants, bundles, modifier groups, modifier sets, promotions, and promotion codes. The response matches a following GET. Repeating DELETE returns 200 with the archived resource without changing its version or updated_at, checking references, or sending another webhook. When the route accepts expected_version, a stale version still returns 409 VERSION_CONFLICT before the replay.

Writes on an archived resource return 409 ACTION_NOT_ALLOWED. details[0] identifies the resource with resource_type and resource_id, current_status: "archived", and reason: "archived". Creating a variant under an archived product identifies the product; creating a code under an archived promotion identifies the promotion. Updating an archived variant or code identifies that child. remediation.retry is none.

Breaking: these archived writes replace 404, PRODUCT_ARCHIVED, or PROMOTION_ARCHIVED with ACTION_NOT_ALLOWED. Repeated DELETE requests return 200 instead of 404.

Breaking: bundles now default to status: "active", like other catalog resources. Include components when creating an active bundle. To create a draft without components, send status: "inactive". Omitting both returns 400 INVALID_REQUEST with details[0].code: "REQUIRED" and param: "components".

Promotion and promotion code creation now accept status: "active" or "inactive", including codes created in a promotion's codes array. Omission defaults to active. An inactive promotion or code has available: false and cannot apply to a purchase. Catalog create schemas now publish their active status defaults.

  • Breaking change
  • API
  • Catalog

Edit active bundles and add options to live products#

You can now replace components on an active bundle using its latest expected_version. Existing orders, checkout sessions, invoices, subscriptions, and plans keep their component snapshots. New orders use the new components, and payment links resolve them on their next read.

When adding or activating a product option, send existing_variant_value on its entry in options[] to give existing active variants a value. It must match one of the option's active values, ignoring case and surrounding whitespace. Flint updates only active variants that lack an active value for that option. The option and variant changes succeed together or fail together. Each changed variant advances its version and sends product_variant.updated, alongside the product's product.updated event.

If active variants need a value and you omit existing_variant_value, the request returns 400 INVALID_REQUEST with detail code REQUIRED and a parameter such as options[0].existing_variant_value. This prevents an option edit from leaving active variants incomplete and causing a later price or name update to fail.

Removing or inactivating a value selected by active variants returns 409 RESOURCE_IN_USE, with up to 10 blocking variant references and a total count. Move those variants to another value or inactivate them before retrying. You can still retire a whole option while variants keep its historical selections.

Open orders no longer block variant or bundle deactivation. Open checkout sessions, invoice payment attempts, active payment links, plans, subscriptions, and active bundles that contain a variant still protect their references. Delete and currency-change guards retain their existing behavior.

ACTIVE_BUNDLE_COMPONENTS_IMMUTABLE, ACTIVE_VARIANTS_REQUIRE_OPTION_BACKFILL, and OPTION_IN_USE_BY_ACTIVE_VARIANTS are removed. The Dashboard supports editing active bundle items and choosing the value existing variants receive when you add an option.

  • Breaking change
  • API
  • Catalog

Filter catalog lists by creation and update time#

GET /v1/bundles, GET /v1/products/{product_id}/variants, GET /v1/categories, GET /v1/modifier-groups, GET /v1/modifier-sets, and GET /v1/promotion-codes now accept created_after, created_before, updated_after, and updated_before. Lower bounds are inclusive; upper bounds are exclusive. Pass RFC 3339 timestamps. Products and promotions already support these ranges.

Categories add sort_by=handle|created_at|updated_at and sort_direction=asc|desc, keeping handle ascending as the default. Modifier groups, modifier sets, and promotion codes add sort_by=created_at|updated_at and sort_direction=asc|desc, defaulting to created_at descending. Ties use the resource ID in the same direction.

Breaking: modifier group and modifier set lists now default to created_at descending instead of ID descending. Start a fresh pagination sequence after upgrading. Promotion codes keep their existing default order.

To recover catalog changes, list with sort_by=updated_at&sort_direction=asc&updated_after=<last seen minus a few minutes>. Include archived in the status filter for resources that support it, and re-list a changed product's variants. Use an overlap and apply results by resource ID: updated_at does not promise the order in which changes committed. Catalog writes preserve later stored timestamps, including when a variant change touches its product or a promotion or code is archived.

  • Breaking change
  • API
  • Checkout

The checkout credential is one Authorization bearer token#

Breaking: the REST contract replaces X-Checkout-Session-ID and X-Checkout-Session-Secret with one bearer token. Send the checkout_auth_token from checkout_access:

HTTP
GET /v1/checkout-sessions/cs_1kmn0aExample
Authorization: Bearer ckat_1kmn0aExample

The token names its session, so you send no session ID beside it. The OpenAPI spec publishes it as the CheckoutSessionBearer security scheme on the 30 operations that accept a checkout credential, and removes the two header schemes and their parameters from every operation. Update your relay to build one Authorization header. The prefix tells a checkout credential from an API key, but sending it together with X-API-Key returns 400 INVALID_REQUEST with detail code NOT_ALLOWED and conflicting_fields naming Authorization and X-API-Key.

Breaking: a checkout credential used on another session's /v1/checkout-sessions/{checkout_session_id} path now returns 404 RESOURCE_NOT_FOUND. It returned 403.

Breaking: a credential past its own lifetime returns 401 INVALID_CREDENTIAL with details[0].reason expired. It returned unknown, as does a credential Flint doesn't recognize.

The cklt_ launch token in a hosted checkout's url isn't an API credential. Sent as a bearer token, it returns 403 FORBIDDEN with details[0].reason checkout_launch_token_restricted.

Ended sessions#

The checkout credential used to stop working the moment its session closed or expired, which left a buyer's page with a 401 instead of the outcome. Now it keeps reading its own session:

  • GET /v1/checkout-sessions/{checkout_session_id} returns 200 with status closed or expired, terminal_reason, and merchant_support, so your page can say what happened and how to reach the merchant. The read leaves out payment_collection and setup_collection, because an ended session can't take a payment.
  • Every other call with the credential returns 409 CHECKOUT_SESSION_NOT_OPEN, with details[0].current_status set to closed or expired. These calls returned 401.

To let the buyer try again, create a session with the same order_id while the order is still payable. You can omit replace_checkout_session_id or send the old closed or expired session's ID. If the order has no open session, Flint creates the new session normally. If another session is now open, naming the old one returns 409 CHECKOUT_SESSION_CURRENT_CHANGED with the current session in details[0].current_checkout_session_id and details[0].blocking_resources. The named session must belong to the same merchant, environment, and order. Replacing the current open session still invalidates it and creates the new one in one step.

closed_by_buyer is removed#

A buyer can't close a session, so terminal_reason: "closed_by_buyer" and the matching checkout_session.closed detail never appeared on a stored session. Both are gone from the spec and the webhook catalog. A closed session has terminal_reason: "closed_by_merchant".

See Build your own checkout and When the session ends.

  • Breaking change
  • API
  • Checkout

Checkout creation uses an explicit type and shared buyer fields#

POST /v1/checkout-sessions now requires type. Send order with order_id, quick_pay with quick_pay_item, or subscription_plan with subscription_plan_id. Fields belonging to another type return INVALID_REQUEST with a NOT_ALLOWED detail. Subscription plan sessions require email; explicitly setting buyer_fields.require_email to false is rejected.

customer_id moves to the top level of checkout requests and responses. Create accepts it with quick_pay and subscription_plan. With order, set the customer on the order first; sending customer_id on checkout creation returns INVALID_REQUEST with a NOT_ALLOWED detail. Creation no longer returns CHECKOUT_CUSTOMER_CONFLICT or CHECKOUT_CUSTOMER_NOT_SET_ON_ORDER.

buyer_fields replaces customer_collection for checkout sessions and payment links. Checkout sessions expose merchant-supplied contact and address values in top-level prefill, replacing customer_collection.prefilled_customer_info. customer_prefill still comes from the customer record, and buyer_contact still records what the buyer saved.

Hosted checkout uses redirects.success_url, redirects.cancel_url, and redirects.expired_url. Embedded checkout uses top-level payment_return_url for redirect payments such as Affirm. Redirect URLs accept at most 2,048 characters. Successful hosted redirects append checkout_session_id and order_id, replacing the previous csId and orderId parameters. Confirm payment through the order.paid webhook or by reading the session.

Top-level expires_in_seconds replaces expiration.expires_in_seconds on checkout sessions. Payment links use checkout_session_expires_in_seconds for each session they start. The old expiration objects are removed; expiration-page destinations use redirects.expired_url.

Checkout session responses and webhooks expose type in place of origin: order, quick_pay, subscription_plan, invoice, or payment_link. Lists accept a multi-value type filter; the origin filter is removed. Closing a checkout session now accepts an omitted request body.

  • Breaking change
  • API
  • Delivery

Default delivery profiles use is_default#

Breaking: Settings and UpdateSettingsRequest no longer include catalog. Delivery profiles now return the required, read-only boolean is_default. Find the default with GET /v1/delivery-profiles?is_default=true; use is_default=false to list other profiles.

To change the default, PATCH the selected delivery profile with {"is_default":true,"expected_version":7}, using that profile's current version. You can combine this with other profile edits. Moving the default advances both profiles' versions and emits delivery_profile.updated for each. Selecting the current default alone succeeds without advancing its version or emitting an event. New physical product variants and bundle components that omit delivery_profile_id use the default.

is_default:false returns 400 INVALID_REQUEST. Archiving the default profile returns 409 RESOURCE_IN_USE with your merchant as the blocker; make another active profile the default first.

POST /v1/delivery-profiles/{delivery_profile_id}/assign-to-unconfigured now accepts only the profile's expected_version fence. expected_catalog_default_version is removed. A default change advances the previous default's profile version, so an assignment with its old version returns VERSION_CONFLICT.

The settings routes no longer declare delivery errors or SETTINGS_CONCURRENT_MODIFICATION. Update callers and regenerate SDKs from the published OpenAPI schema.

  • Breaking change
  • API
  • Catalog

Read files and attach images by file ID#

Use GET /v1/files/{file_id} to read a file's status: awaiting_upload, processing, ready, failed, or expired. Failed files include a failure_code. Reads never return an upload URL. To retry an upload, create a new file.

expires_at now describes when a file expires in its current status. While awaiting_upload, it equals the upload deadline. For a ready, unattached file, it is the last instant you can attach it: 24 hours after processing or 7 days after its last detachment. It is omitted while attached and in every other status. content_type and size_bytes describe the declared upload until the file is ready, then the stored image.

Attach a completed file with images: [{ "file_id": "file_…" }], or use file_id in a payment link's image or a merchant's logo or icon. Each image input accepts exactly one of file_id and source_url. Canonical Flint URLs still work in source_url when copying an image to another resource.

On updates, resending the exact external source_url of an image still attached to the same field reuses that image without downloading it again. To replace bytes behind a stable URL, send a different URL, such as one with a version query parameter, or upload a file. An unchanged gallery keeps its image revision. Owner updates still follow their existing version and event behavior. Flint never returns source_url.

Breaking: Per-image external_reference_id is now source_reference. It accepts 1 to 255 characters, preserves surrounding whitespace, and remains unique within a gallery. Resource-level external_reference_id fields keep their names.

Owned galleries and image slots now return ImageAttachment: url, alt, width, height, optional file_id, and optional source_reference. Snapshot, public, and buyer images use Image, which contains only url, alt, width, and height.

Send an Idempotency-Key when creating a file to recover a lost response. Without one, create another file; the unused upload expires after 15 minutes.

  • Breaking change
  • API
  • Inventory

Read inventory allocation policy revisions#

Inventory allocation policies now expose current_inventory_allocation_policy_revision_id. Creating a policy publishes its first revision, and replacing its configuration publishes a new immutable one. Updating its name, metadata, external reference, or status keeps the current revision. A policy's version is still the counter you send as expected_version.

Read a policy's revision history with GET /v1/inventory-allocation-policies/{inventory_allocation_policy_id}/revisions, or retrieve one revision with GET /v1/inventory-allocation-policies/{inventory_allocation_policy_id}/revisions/{inventory_allocation_policy_revision_id}. Both routes require commerce.inventory.read. The list defaults to newest first and supports page_size, page_token, sort_direction, created_after, and created_before. created_after includes its boundary and created_before excludes it.

Breaking: Inventory routing sources use type: policy_revision and inventory_allocation_policy_revision_id in place of policy_version and inventory_allocation_policy_version_id. This applies to reservations, orders, payment links, subscription plans, invoice quick-pay requests, and delivery previews. Send the new names in requests; policy_version is rejected. The invpv_ ID prefix stays the same.

Routing sources in responses name both the policy and the revision, so you can read the configuration that routed the stock. A policy_revision source returns the inventory_allocation_policy_id its revision belongs to wherever it appears, including replayed responses, and a reservation routed by policy returns the inventory_allocation_policy_revision_id it resolved to. Send only the ID that belongs to the source's type in a request.

Reservation webhook snapshots and replayed responses use the revision field names too, including a replayed policy create or update first sent before this change, which now includes current_inventory_allocation_policy_revision_id. Errors name inventory_allocation_policy_revision_id, and resource references use inventory_allocation_policy_revision.

  • Breaking change
  • API
  • Inventory

Inventory movements name their source and recording credential#

Every inventory movement now uses source_reference to name the resource whose write recorded the change. Adjustments point to the adjustment, receipts to the receipt, reservation changes to the reservation, and safety stock changes to the inventory level. Fulfillment, count, and transfer movements point to the fulfillment, count, and transfer. Read the resource with its GET route, or list a resource's movements with source_reference_type and source_reference_id.

Breaking: source_reference is required in the movement schema. Its resource_type is one of inventory_adjustment, inventory_count, inventory_receipt, inventory_reservation, fulfillment, inventory_transfer, and inventory_level. The source_reference_type list filter accepts those same types; inventory_reservation_line and product_variant are removed. Reservation movements name the reservation, not one of its lines. Match a line by the movement's inventory_item_id and location_id.

Breaking: created_by is a required, nullable ResourceReference instead of a string. It identifies the API key, partner app install, or user that recorded the movement, using resource_type values api_key, partner_app_install, or user. An API key is recorded as the key, not as the user who created it. created_by is null when Flint records a movement on its own, including expired holds, order and payment automation, and fulfillment handoffs.

Breaking: reservation_consumed replaces fulfillment_consumed for both merchant and fulfillment consumption. The movement's source_reference tells them apart: an inventory_reservation for a merchant's consume, and a fulfillment for a handoff. system_correction is removed from the movement type vocabulary.

Breaking: movement reason is now optional and appears only on adjustments, where it uses the adjustment reason vocabulary: received, returned, sold, theft, loss, condition_changed, counted, and correction. The reason list filter accepts only those values and returns adjustment movements. Other movements describe the event in type.

Breaking: every line sent to POST /v1/inventory-receipts requires both inventory_reservation_id and inventory_reservation_line_id. A receipt puts back units that the reservation line consumed, bounded by its consumed quantity minus units already received. A missing ID returns 400 INVALID_REQUEST with detail code REQUIRED at that field. Record new stock with an adjustment using reason: received, or a customer return sold outside Flint with reason: returned. Return dispositions keep their existing receipt rules.

The Inventory reference lists which call records each physical event, and Reading the ledger covers movement sources and created_by. available_quantity is documented as never below 0.

  • Breaking change
  • API
  • Inventory

Inventory transfers replace lines as one collection#

Breaking: PATCH /v1/inventory-transfers/{inventory_transfer_id} now accepts lines instead of line_changes. Send the complete collection and expected_version. Keep inventory_transfer_line_id on existing lines, omit it to add a line, and omit a line from the array to remove it. Every line requires inventory_item_id and requested_quantity; condition defaults to sellable. An existing line must repeat its stored item and condition, so send condition for any line that isn't sellable. A line's item and condition can't change.

Flint validates the full collection before saving it. Lines with departed or canceled quantity must remain. A departed line must keep its requested quantity, and any line must request at least its canceled quantity. Refusals return 409 INVENTORY_TRANSFER_LINE_CONFLICT; an omitted line with history is named in the error details. Duplicate line IDs or item and condition pairs return 400 INVALID_REQUEST.

Transfer lines now use condition instead of physical_condition on create, PATCH, and reads. Transfers use external_reference_id instead of external_reference on create, PATCH, reads, and the list filter. The filter matches exactly and is case-sensitive. On PATCH, omit the reference to keep it or send null to clear it. Empty strings are rejected.

  • Breaking change
  • API
  • Merchant-billing
  • Reports

List merchant billing entries#

GET /v1/merchant-billing-entries returns merchant billing activity with signed effects on your outstanding balance and available credit. It requires merchant_billing.read and supports API key and Dashboard authentication.

Filter by type, currency, occurrence or recording time, payment_intent_id, dispute_id, merchant_billing_invoice_id, or balance_transaction_id. The list returns newest occurred_at first, then descending causal type rank and entry ID within the same second. Use page_size and page_token to read additional pages.

occurred_at is when the event happened; created_at is when Flint recorded the entry. Events can arrive late. To pick up newly recorded entries after merchant_billing_balance.updated, filter on created_after and deduplicate by merchant_billing_entry_id. Generate a merchant_billing_itemized_v1 report to export an interval. It fails with data_unavailable when billing projections or membership history are inconsistent, collection outcome or balance transaction evidence is missing, or an entry is invalid. See Reporting for retry behavior.

The report and list return the same entries. These report changes apply immediately to every supported API version:

  • created_at follows occurred_at in the CSV header.
  • merchant_billing_invoice_id replaces merchant_subscription_invoice_id.
  • Entry types invoice and custom_domain_collection replace subscription_invoice and software_collection. All 11 types are available in the list's type filter.
  • mbe_ IDs use the underlying source identity, so public type renames preserve entry IDs. Existing report entry IDs change once.
  • CSV rows use ascending occurrence time, causal type rank, and entry ID. Reversing the list returns the same order.
  • Breaking change
  • API
  • Merchant-billing

Merchant billing uses one API vocabulary#

These changes apply immediately to every supported API version.

  • Use /v1/merchant-billing-invoices and /v1/merchant-billing-invoices/{merchant_billing_invoice_id} instead of /v1/merchant-subscription-invoices and its retrieval route. The operations are listMerchantBillingInvoices and getMerchantBillingInvoice.
  • Invoice responses use merchant_billing_invoice_id. The schemas are MerchantBillingInvoice, MerchantBillingInvoiceLineItem, MerchantBillingInvoiceResponse, and MerchantBillingInvoiceListResponse. Invoice IDs still start with msinv_, and line item IDs still start with msinvl_.
  • Invoice line_items replaces lines. Each item has merchant_billing_invoice_line_item_id, type, name, quantity, unit_price_money, total_money, and created_at. These replace merchant_subscription_invoice_line_id, line_type, description, unit_amount_money, and amount_money. Amounts remain unsigned: subscription totals add to subtotal_money, and credit totals add to credit_money.
  • Subscribe to merchant_billing_invoice.issued and merchant_billing_invoice.updated instead of merchant_subscription_invoice.issued and merchant_subscription_invoice.updated. Their resource type is merchant_billing_invoice.
  • Balance transaction type values are now merchant_billing_collection and merchant_billing_collection_reversal, replacing merchant_billing_payment and merchant_billing_payment_reversal. Use the new values in filters. Transaction IDs are unchanged.
  • Balance transaction related_resource.resource_type and the related_resource_type filter use merchant_billing_invoice instead of merchant_subscription_invoice.
  • Merchant billing read failures return INTERNAL_ERROR instead of MERCHANT_FINANCE_READ_FAILED. Missing invoices continue to return RESOURCE_NOT_FOUND, with param: merchant_billing_invoice_id and resource type merchant_billing_invoice.
  • Merchant billing balances include currency. /v1/merchant-billing-balances returns all balances on one page, omits next_page_token, and rejects page_size and page_token.

To find unpaid invoices, use GET /v1/merchant-billing-invoices?status=open,collecting,delinquent. The status filter accepts comma-separated or repeated values from draft, open, collecting, paid, delinquent, void, and written_off.

  • Breaking change
  • API
  • Settings

Merchant settings and explicit patch rules#

Settings changes apply immediately to every supported API version:

  • Each environment keeps its own merchant settings. Settings responses no longer include settings_scope, organization_id, location_id, or device_id. GET /v1/settings/effective rejects the removed location_id and device_id query parameters.
  • GET /v1/settings returns stored values. GET /v1/settings/effective fills in Flint's defaults. Send null for a settings section to remove every value you set in it; effective reads then use the defaults.
  • PATCH field descriptions now state whether a nested object applies only the fields you send, replaces the whole object, replaces the full map, or merges metadata by key. Present arrays replace the stored array.
  • Every tax_identity patch requires expected_version, including a legal_name edit or tax_identity: null. Read the current Settings.version and send it as expected_version.
  • Settings no longer declare INHERIT_ONLY_VIOLATION, SUBSET_ONLY_VIOLATION, MIN_BOUND_VIOLATION, or MAX_BOUND_VIOLATION errors.
  • Breaking change
  • API

Partner OAuth follows the OAuth standards#

Partner apps and flint-cli share the OAuth token and revocation endpoints. Discovery metadata is available at https://api.withflintpay.com/.well-known/oauth-authorization-server.

These breaking changes apply to partner OAuth on every supported API version:

  • Authorization requires code_challenge and code_challenge_method=S256. Keep the matching code_verifier with your state and send it when exchanging the code. A wrong verifier consumes the code.
  • Authorization and preview accept space-separated scope permission IDs in place of permission_ids. Omit scope to request your app's defaults. Required permissions are always granted. Unknown parameters, including the removed permission_ids, are ignored and never forwarded to consent. Recognized parameters may appear only once.
  • POST /v1/oauth/token accepts only application/x-www-form-urlencoded bodies. JSON bodies and URL query strings return invalid_request. Authenticate with HTTP Basic or client_id and client_secret form fields, using one method per request.
  • Partner token responses return token_type: Bearer, replacing bearer. Their scope lists sorted granted permission IDs, replacing API scope names. Test-mode responses include sandbox_id; live-mode responses omit it. An optional refresh scope must match the full granted permission set.
  • Redirect URIs must not contain a fragment, including an empty #.
  • The token and revocation operations are named exchangeOAuthToken and revokeOAuthToken, under the oauth tag. They publish OAuthTokenRequest, OAuthTokenResponse, and OAuthRevokeRequest in place of the separate partner and CLI schemas. Regenerate clients that use the old names.

If a partner refresh response is lost, retry the same token within 60 seconds while its replacement is unused. Flint returns a new pair and invalidates the lost replacement. The retry window starts at the original rotation and does not extend. Reusing an old token after that window, after its replacement was rotated, or after it was superseded by a retry revokes its token family and invalidates that environment grant's access tokens. The other mode keeps working, and the install and grant stay active. Authorize that mode again to recover. Reusing a consumed authorization code also revokes the tokens that code issued.

Partner apps can revoke tokens with POST /v1/oauth/revoke. Revoking a refresh token ends its family and invalidates the grant's access tokens. Revoking an access token invalidates the grant's outstanding access tokens while its refresh token keeps working. Unknown, expired, and other clients' tokens return 200. Revocation does not uninstall the app.

CLI OAuth errors now include a fixed error_description. Existing flint-cli form requests, token lifetimes, and refresh-reuse behavior continue to work.

  • Breaking change
  • API
  • Webhooks
  • Fulfillment

Webhooks report closed-order edits and shipment and package changes#

Breaking: order.updated now reports material edits to orders that stay closed, including fulfillment creation, fulfillment progress, and paid subscription destination corrections. order.closed fires only when an order moves from open to closed, with no additional generic order.updated for that transition. Payment/refund-only changes continue to use their dedicated money events.

Breaking: shipment.updated and package.updated now cover resource revisions beyond status changes. Shipment metadata and external-reference edits send an update. Adding a package to an existing shipment sends package.created and shipment.updated with the final package_count. Derived shipment timestamps and other persisted shipment revisions also send updates when status stays the same.

Package updates cover tracking, carrier, service, label, weight, dimensions, external references, metadata, and contents. Creating, editing, or deleting a package item advances the parent package's version and updated_at and sends one package.updated. Its data.object is the package snapshot, without items. Re-list GET /v1/packages/{package_id}/items, paging to completion, to refresh contents. An item edit alone does not update the shipment or order. Initial shipment, package, and item creation together sends creation snapshots without extra updates for the initial contents.

Subscribers to these events or to all events receive more deliveries. Your own API writes produce events too. Distinct committed revisions produce distinct events, including changes within one second or a change back to an earlier value. Deduplicate deliveries by webhook_event_id, expect out-of-order delivery, and compare shipment and package version before replacing a saved snapshot. Retries preserve the original snapshot; fetch the resource for current state.

fulfillment_event.created still reports timeline facts and their typed previous_status and current_status on transitions. Tracking changes keep their existing timeline event. Metadata and contents edits do not create status facts or buyer emails; use actual buyer moments when sending your own mail.

Event names and resource payload shapes stay the same. Past missing updates are not backfilled; read current resources to refresh a mirror. See Webhook event payloads and Staying in sync.

  • Breaking change
  • API
  • Orders
  • Checkout

Line-item writes use the order version#

PATCH /v1/orders/{order_id}/line-items/{order_line_item_id} now compares expected_version with the order's version, matching line-item adds and removals. The fence is optional for scalar edits and required when modifiers, gift_card_recipient, or subscription is present. A stale fence returns 409 ORDER_CHANGED_REFRESH_REQUIRED with param: expected_version and the order's current version in details[0].current_version.

Line-item PATCH returns OrderResponse for every credential, including checkout credentials. OrderLineItem.version, CheckoutSessionLineItemModifierUpdate, and CheckoutSessionLineItemModifierUpdateResponse are removed. Read the order's version from data.version before updating a line.

CHECKOUT_SESSION_REVISION_CONFLICT and ORDER_LINE_ITEM_VERSION_CONFLICT are retired. The checkout conflict's modifier choices, selected modifiers, and pricing facts are removed from ErrorDetail. Retained errors with either retired code replay as VERSION_CONFLICT. Refresh the order before rebuilding the change with a new idempotency key.

A retry using the same idempotency key across this change can return a key-reuse conflict. Previously recorded checkout modifier successes can replay their earlier response shape during the 24-hour retention window.

  • API
  • Webhooks
  • Inventory

Partner apps can receive inventory events#

Partner apps can subscribe to all 17 inventory_* events on webhook endpoints with the installed_merchants event source. The installed merchant's grant must include commerce.inventory.read.

These events cover inventory levels, counts, receipts, reservations, and transfers. Reservation events cover merchant-owned reservations; stock changes from checkout and order reservations are reported through inventory_level.updated.

See Inventory events and Partner app installs for event details and endpoint setup.

  • Breaking change
  • API
  • Payments
  • Orders
  • Errors

Payment and order state refusals use ACTION_NOT_ALLOWED#

Breaking: branch on ACTION_NOT_ALLOWED and details[0].reason. Payment intent updates, confirmations, captures, cancellations, receipt requests, refunds, and order mutations now use this shared 409 error for state refusals. details[0] identifies the refusing resource with resource_type and resource_id and reports its current_status. For an unknown reason, fall back to current_status.

The following public codes are retired:

  • PAYMENT_INTENT_CANNOT_BE_UPDATED
  • PAYMENT_ALREADY_SUCCEEDED, PAYMENT_CANCELED, PAYMENT_EXPIRED
  • INVALID_STATUS_FOR_CAPTURE
  • PAYMENT_INTENT_NOT_CANCELABLE, CANNOT_CANCEL_SUCCEEDED_PAYMENT, PAYMENT_ALREADY_CANCELED, CANNOT_CANCEL_EXPIRED_PAYMENT
  • AMOUNT_NOT_UPDATABLE, CUSTOMER_NOT_UPDATABLE, RECEIPT_EMAIL_NOT_UPDATABLE, PAYMENT_SOURCE_NOT_UPDATABLE
  • PAYMENT_INTENT_NOT_REFUNDABLE
  • ORDER_CLOSED, ORDER_NOT_PAYABLE

Reasons distinguish the available next step:

ReasonMeaning
already_appliedThe requested outcome is already reflected in the resource. Read it to confirm.
terminal_statusThis action is unavailable in the final status. Create a new resource if needed, or follow remediation.next_actions.
status_not_reachedComplete the required earlier step or wait for the resource to reach the required status.
in_progressWait for the operation already underway to finish, then read the resource before choosing an action.
field_lockedOmit the field named by param, or create a new resource with the value you need.
payment_collectedThe order is still open but payment has been collected, so its commercial fields cannot change.

Receipt requests and refunds before success use status_not_reached, in_progress, or terminal_status according to the payment intent's status. Merchant-managed receipts continue to use reason merchant_managed_receipts without a lifecycle status. An invoice payment attempt cancellation identifies the refusing payment_intent, even though the route names the invoice attempt.

remediation.retry is none; remediation.next_steps describes the remedy for the reason. A processing cancellation refusal includes a get_payment_intent next action. Canceling a succeeded payment includes create_refund.

Retained idempotency errors replay with the new public codes. Update error handling and regenerate SDKs from the published OpenAPI schema.

  • Breaking change
  • API
  • Payments
  • Webhooks

Payment intent responses share one collection schema#

POST /v1/payment-intents and GET /v1/payment-intents/{payment_intent_id} now publish PaymentIntentWithCollection, composed from PaymentIntent plus payment_collection. It replaces PaymentIntentResult on GET and the unnamed create schema. Update generated type references when regenerating your SDK.

The JSON responses are unchanged. Create always includes payment_collection. GET includes it while status is requires_payment_method or requires_confirmation, subject to these conditions: a payment intent owned by a checkout session requires that session's checkout credential, and an order-linked payment intent omits it when using a checkout credential.

The composed schema inherits PaymentIntent's field definitions and presence rules. Payment intent webhook schemas reference PaymentIntent and no longer declare payment_collection.

  • Breaking change
  • API
  • Payments
  • Orders

Payment intent create and confirm results#

Breaking: POST /v1/orders/{order_id}/payment-intents now returns CreatePaymentIntentResponse, matching standalone creates. Read data.payment_intent_id instead of data.payment_intent.payment_intent_id. The required data.payment_collection remains beside the payment intent's fields and directs order payments to POST /v1/orders/{order_id}/pay. CreateOrderPaymentIntentResponse and CreateOrderPaymentIntentResult are removed.

current_payment_action appears only in PaymentIntentWithAction, returned by POST /v1/payment-intents/{payment_intent_id}/confirm as ConfirmPaymentIntentResponse. It is always present: a typed action when buyer authentication is needed and Flint can provide it, or null otherwise. It is removed from PaymentIntent, PaymentIntentWithCollection, and BuyerPaymentIntent. Reads, lists, other writes, and webhooks omit it. If a confirm response is lost, read the intent; if its status is requires_action, call confirm with an empty body to recover the action. If confirm returns requires_action with a null action, call confirm again with an empty body.

GET /v1/payment-intents/{payment_intent_id} omits payment_collection for order-owned intents. Use Order.payment_collection and its selectable_payment_intents instead.

Standalone capture and cancel accept no body, whitespace, or {}. These changes apply to every supported API version. Regenerate SDKs against the updated OpenAPI specification.

  • API
  • Payments
  • Customers

Choose a default payment method on the customer#

Set default_payment_method_id on PATCH /v1/customers/{customer_id} or PATCH /v1/me to choose an active off_session payment method belonging to that customer. Send null to clear it. The customer response always includes the field, with null when there is no default.

Changing the default, including deleting the default card, advances the customer's version once and sends customer.updated. An unchanged default is a no-op. You can use expected_version on either PATCH route to refuse an update when the customer has changed.

Payment method responses include is_default. Merchant responses and payment method webhook snapshots also include fingerprint once card details are known. Equal fingerprints identify copies of one card number within your merchant and environment. Wallet cards can have a different fingerprint from the same card entered directly. Buyer credentials never receive this field.

The operation IDs savePaymentMethod, removePaymentMethod, saveMePaymentMethod, and removeMePaymentMethod are now createPaymentMethod, deletePaymentMethod, createMePaymentMethod, and deleteMePaymentMethod. Their HTTP paths are unchanged.

  • Breaking change
  • API
  • Risk

Payment reviews use explicit names and typed actors#

Breaking: Payment reviews now use /v1/payment-reviews and payment_review_id. The ID prefix remains rev_. Operations are listPaymentReviews, getPaymentReview, approvePaymentReview, and declinePaymentReview; schemas use PaymentReview names, and resource references use payment_review. Webhook events are payment_review.opened, payment_review.closed, and payment_review.reopened.

Breaking: Payment risk, refunds, and refund creation results use payment_review_id in place of review_id. Buyer refunds omit this merchant-only reference. Payment review closed_by and resolution_started_by are nullable resource references to the acting credential, or null when Flint acted.

Breaking: A decline closes with closed_reason: declined, including when Flint refunded the payment. Read refund_id and refunded_money to see whether it refunded. Conflicting review actions return 409 ACTION_NOT_ALLOWED with resource_type: payment_review: a closed review reports current_status: closed and reason: terminal_status; an action already underway reports current_status: resolving and reason: in_progress.

Breaking: The decline request field add_to_block_list becomes add_to_block_lists. Fraud warnings and their list filter use is_actionable in place of actionable. Fraud warning payment summaries use the FraudWarningPaymentSummary schema.

Risk scores keep their 0-99 range, now published in the reference schema. Higher scores mean higher fraud risk. An absent score means no score is available, not a score of zero. Risk levels remain normal, elevated, highest, and not_assessed, with descriptions of the processor's screening result.

  • Breaking change
  • API
  • Promotions

Promotion stacking and rule shapes#

These changes apply immediately to every supported API version.

  • stacking_mode is removed from promotions, create and update requests, and discount preview candidates. To prevent a promotion from combining with other promotion discounts, set all three combines_with booleans to false.
  • exclusivity.selection is removed. Each group chooses its highest-priority promotion, then the larger standalone discount, then the earlier created_at, then the lower promotion_id. Priority defaults to 0, is always present in responses, and cannot be null in PATCH. Send 0 to reset it. Group names must contain 1 to 100 characters after trimming and match exactly, including case.
  • GET /v1/promotions removes the available and unavailable_reason filters. Use the multi-value availability filter, for example ?status=active&availability=available,no_active_codes. Archived promotions still require status=archived.
  • GET /v1/promotion-codes replaces the available filter with multi-value availability: available, inactive, expired, exhausted, or promotion_unavailable. Removed query parameters return INVALID_REQUEST with detail code UNKNOWN_FIELD. Response fields available and unavailable_reason are unchanged.
  • Rule groups use only objects: {"all": [...]} or {"any": [...]}, each with at least one rule or nested group. Bare arrays, empty groups, and a rules key are rejected with INVALID_REQUEST and detail code INVALID_VALUE. Omit a rule group for no rules, or send null when PATCH allows clearing it. Responses list rules before nested groups. Under any, a nested group containing only one rule reads back as that rule. Under all, nested groups keep their boundaries to preserve matched_items.subtotal scope.
  • Creating a promotion requires application_method.type: percent_off, amount_off, or buy_x_get_y. The flat application method accepts only fields for that effect. PATCH remains sparse: omitting type keeps the stored effect; changing type replaces it.
  • matched_items.subtotal in an all group now counts only the line items matched by that group's line_item.* rules, including its nested groups. A cart with $30 of hoodies and $40 of hats no longer satisfies a $40 hoodie minimum because the hat amount was included.

Manual discounts stay on the order and do not count toward the promotion cap. Frozen subscription-term promotions remain exempt from exclusivity, combination rules, and the cap.

  • Breaking change
  • API
  • Refunds
  • Gift-cards

Refunds can issue store credit on new gift cards#

POST /v1/refunds accepts destinations, with optional payment_intent and gift_card_redemption keys. Each is original or new_gift_card and defaults to original. Use it with refund_type: "full", "targets", or "amount" without tender_allocations. A key for a tender absent from the refund has no effect.

Setting payment_intent to new_gift_card refunds that value as store credit. The payment must have succeeded, belong to an order, use USD, and have been paid by card, Apple Pay, Google Pay, or Link. Affirm, ACH debit, other payment options, non-USD payments, and payments without an order return 400 INVALID_REQUEST with a NOT_ALLOWED detail. Store credit needs a payment that belongs to an order so Flint can issue the gift card to that order's customer. The error's param is destinations.payment_intent or tender_allocations[i].destination. Open chargebacks retain the existing dispute refusal. Refunds to the original payment method still work for standalone payments.

The refund consumes the original payment's refundable value and reverses the order value and tax. Flint makes no processor refund: the merchant keeps the cash, the original processing fee is not returned, and the gift card value becomes a liability. A later dispute on the payment freezes the gift card value. The gift card load reports source.funding_source_type: "store_credit", payment_intent_id, order_id, and refund_id.

For gift card redemption value, new_gift_card issues new cards instead of crediting the original card. This includes refunds whose original gift card is closed or cannot accept the full value. The order requirement for payment intent store credit does not change gift card redemption destinations.

destinations is not allowed with tender_allocations or gift_card_load_id; the error detail names both conflicting_fields. With explicit allocations, set destination on each allocation. Gift card purchase refunds return cash.

New gift card codes are returned in data.gift_card_codes on create and same-key retries for 24 hours, while each code remains current. Ordinary refund reads and webhooks omit the codes. Direct refund creation sends no gift card email; the caller delivers the codes.

Breaking: destination: "replacement" is renamed "new_gift_card" on refund tender allocation inputs and responses. Sending replacement returns INVALID_REQUEST with an INVALID_VALUE detail. Payment intent allocations now accept destination too.

Breaking: refund_method is removed from Refund and BuyerRefund responses, including expansions and webhook payloads. Read tender_allocations[].destination and destination_cards for the result. A store credit refund can succeed with an empty payment_refunds list.

  • Breaking change
  • API
  • Reports
  • Balances
  • Payouts

Report CSVs use API money signs, names, and relationships#

Breaking: CSV columns change in place for balance_transactions_itemized_v1, payouts_itemized_v1, payments_itemized_v1, tax_transactions_itemized_v1, and gift_card_liability_v1. Their report type names stay the same. Update importers that depend on column names, positions, or fee signs before generating new reports.

  • Balance transaction fee charges are positive, and returned fees are negative, matching GET /v1/balance-transactions. net_money.amount stays unchanged and equals amount_money.amount - fee_money.amount. The report adds description and order_id. payout_id identifies the paid payout containing the transaction. related_resource.resource_type and related_resource.resource_id replace payment_intent_id, refund_id, dispute_id, and return_id, and identify the source payout for a payout debit. Related resources contain only valid Flint IDs.
  • Payout reports contain the full balance transaction columns followed by arrival_date. They remove payout_entry_id. For each payout, net_money.amount sums to the payout's amount_money.amount. A payout matches the interval when the start of its arrival_date in the report's timezone falls within the interval.
  • Payment reports rename invoice_fee_money to invoice_collection_fee_money and subscription_fee_money to subscription_collection_fee_money. These fees and automatic_tax_fee_money each include an amount and currency column.
  • Tax transaction reports use line1, line2, state, tax_category, price_mode, and jurisdiction.*. Provider columns are removed. Every money value has an amount and currency pair.
  • Gift card liability reports replace bare amount columns and the shared currency column with opening_money, loads_money, imports_money, redemptions_money, refunds_money, cash_outs_money, purchase_reversals_money, purchase_refund_recoveries_money, adjustments_money, and closing_money, each with an amount and currency column.
  • Text cells that start with =, +, -, or @, after any leading whitespace, get a leading ' so spreadsheets don't run them as formulas. This applies to description on balance transaction and payout reports, jurisdiction.* text on the jurisdiction tax reports, and the text columns on tax transaction reports. Strip the ' if your importer reads these values as data.
  • Breaking change
  • API
  • Developers

Read request logs across callers#

developer.request_logs.read reads request summaries and detail for every caller in the credential's environment. developer.request_logs.self.read now reads both summaries and detail for the calling credential. The separate developer.request_logs.self.detail.read scope is retired; stored grants retain access to their own requests. The operations are now listAPIRequestLogs and getAPIRequestLog.

Request logs include a caller resource reference when Flint can identify the caller. Filter lists with caller_id or one or more caller_type values using developer.request_logs.read. Filtering caller_id by an API key ID also includes its older requests within the 90-day summary retention window. Other caller IDs and caller_type match requests recorded after this deployment. Older rows can still show a caller, except for older CLI requests whose user was not recorded. An own-only reader opening another caller's detail receives 403 INSUFFICIENT_SCOPE naming developer.request_logs.read. Other merchants and environments remain inaccessible.

Detail includes ip_address and user_agent for API keys, users and partner app installations. Buyer-side checkout, customer-session and invoice callers never expose these device fields. Headers and captured bodies remain redacted, with detail retained for 14 days.

Breaking: Request summaries and detail replace response_shape_metadata with expand, the normalized expand paths Flint applied, and remove correlation_id. Use api_request_log_id to identify the log. reference_url is now an absolute Flint guide URL. Reproductions remove safe_to_reproduce, reproduction_complete, recommended_environment and warnings. Replaying a non-GET request changes data; replay with a sandbox key.

Resource timelines now return a discriminated union of api_request, webhook_event, webhook_delivery_attempt and image_mutation entries. API request entries follow the same own-only or merchant-wide visibility rule. The default includes all four groups; include=image_mutations selects image changes. Entries no longer repeat the timeline's test, resource_type or resource_id, and no longer include correlation_id. Webhook event entries contain event identity and optional request links; delivery-attempt entries contain attempt results and recovery guidance. Attempt failures use error_category and error_summary, with no duplicate error_code, last_error or diagnostic_category.

  • Breaking change
  • API

Fraud warnings and purchase refund allocations have distinct ID prefixes#

Fraud warning IDs now start with frw_ instead of fw_. Gift card purchase refund allocation IDs now start with gcpra_ instead of rta_. Update prefix checks in your integration, including checks for fraud_warning_id, dispute references, and webhook payloads. These changes apply to all supported API versions; the old prefixes are not accepted for these resources.

Refund allocation IDs keep rta_, Flint wallet card handles keep fwc_, and other existing public resource prefixes stay the same.

The OpenAPI specification and webhook examples now use valid Flint ID prefixes and complete uppercase ULIDs. Request ID examples use UUIDs. Repeated example IDs stay consistent across fields and URLs.

  • Breaking change
  • API
  • Returns

Return receipts and inspections have their own versions#

Breaking: receipt verification and inspection acceptance decisions now compare expected_version with the receipt or inspection returned in data. They previously compared it with the parent Return. Read the observation's new version and send that value when verifying a receipt line or deciding an inspection line. Each observation also has updated_at. The version advances and the timestamp records the write time when a line changes or the observation is superseded. A new observation starts at version 1.

A stale fence returns 409 VERSION_CONFLICT with param: expected_version. Its detail names return_receipt or return_inspection, the observation's ID, and its current_version. Changes to the parent Return alone no longer invalidate an observation fence. Return state restrictions still apply.

Breaking: resolution create and preview now take expected_return_version. Send the Return's version as expected_return_version on POST /v1/returns/{return_id}/resolutions, or as resolution.expected_return_version on POST /v1/return-previews. The fence remains optional. expected_version is no longer accepted on those inputs. A stale parent fence returns 409 VERSION_CONFLICT naming the field you sent, with the Return's ID and current_version.

To preserve a preview before creating a resolution, send the same expected_return_version on both requests. GET /v1/return-receipts and GET /v1/return-inspections now accept updated_after (inclusive) and updated_before (exclusive).

  • API
  • Returns
  • Webhooks

Read Return reasons and activity history#

Cancel actions on Returns, Return resolutions, and Return dispositions now record cancellation_reason, cancellation_reason_message, and canceled_by on the resource. The message and actor are visible only to merchants. A disposition's reason still describes why it was created.

Canceling a Return's last approved line quantity also records that call's reason, message, and actor on the Return.

Manual completion records the supplied completion_reason and completion_reason_message on the Return. Automatic completion leaves these fields absent. Reopening a Return clears its completion reason, message, time, and actor.

Use GET /v1/returns/{return_id}/activities with commerce.returns.read to read the merchant activity history, including reasons for reopen, retry, release, and partial line cancellation. Each activity includes its actor, referenced resource, and resulting return_version. Results are newest first; send sort_direction=asc for chronological order. Use page_size and page_token to paginate.

Activities are merchant-only and have no /v1/me route. Read the referenced resources for their current state. Existing Return, resolution, and disposition webhook events include the new resource fields; no new event types are added.

  • Breaking change
  • API
  • Subscriptions

Subscription resumes, scheduled starts, and cancellation timing are explicit#

Breaking: resume applies only to paused subscriptions and returns 202. Both POST /v1/subscriptions/{subscription_id}/resume and its /v1/me equivalent return 202 Accepted with Retry-After: 2. The subscription remains paused with resume_requested_at set until Flint applies the request. Flint then clears the timestamp and sends subscription.resumed as the status becomes active, or past_due while an overdue period is collected. If delivery becomes unavailable before Flint can resume it, the subscription stays paused, the timestamp clears, and Flint sends subscription.updated. Accepting each request also sends subscription.updated and advances the subscription's version.

Breaking: reactivate is the only way to undo a scheduled cancellation. Resume no longer clears a pending cancellation or returns a successful no-op on an active subscription. Resume refusals return 409 ACTION_NOT_ALLOWED with details[0].reason set to cancellation_scheduled, payment_required, or not_paused, plus current_status. For a scheduled cancellation, call reactivate. For past_due, update the payment method and create a payment retry. These reasons replace the former CANNOT_RESUME and payment-related resume error codes.

Breaking: scheduled starts report scheduled. This new closed-enum status appears on subscriptions, expanded subscription summaries, and subscription webhook snapshots. It means the subscription has starts_at and no delivery hold or inventory wait. At starts_at, Flint collects the first period; it becomes active once paid, or past_due if the payment fails. incomplete now describes a first payment being collected or held. Analytics add status_counts.scheduled_count, and incomplete_count excludes scheduled starts. Both list filters accept scheduled.

Breaking: default cancellation honors its scheduled time or returns a conflict. Without cancel_immediately: true, trialing subscriptions continue until trial_end_at, active subscriptions end at current_period_end_at, and past_due subscriptions end at next_retry_at instead of that retry, including when the first period was never paid. An active subscription with an expired renewal delivery hold still ends immediately. For scheduled, incomplete, paused, and past_due without a retry time, send cancel_immediately: true; otherwise Flint returns 409 ACTION_NOT_ALLOWED with reason period_end_unavailable. An existing scheduled cancellation remains unchanged on repeated requests.

Buyer cancellation follows the same timing. Under end_of_period, a trialing buyer can cancel at trial end and is never charged. Immediate cancellation during the trial requires buyer_chooses. When no scheduled cancellation time is available, a buyer can send cancel_immediately: true under either policy.

New: scheduled_cancel_at and resume_requested_at are always present and nullable. scheduled_cancel_at reports the trial end, current period end, or next retry at which a scheduled cancellation takes effect. It is null when no cancellation is scheduled. resume_requested_at is null unless a paused subscription has a pending resume request. Trial-end cancellations now send subscription.cancellation_scheduled.

Attention filtering excludes first payments until they are overdue. A scheduled start or an immediate signup's in-flight first payment does not match needs_attention unless its billing date is more than 24 hours overdue or another existing attention condition, such as a hold, applies.

  • API
  • Subscriptions
  • Payments

Subscription plan resets and checkout payment risk#

When updating a subscription plan, leaving out billing_interval_options, quantity_options, or subscription_delivery_method_ids keeps the current values. Each field rejects null with a 400 response.

  • Send billing_interval_options: [] to reset the choices to the plan's default billing interval and count.
  • Send quantity_options: [] to reset the choices to [1].
  • Send subscription_delivery_method_ids: [] to remove all delivery methods.

The field descriptions and error messages now explain these existing behaviors. The update request example omits these arrays when changing only the description and metadata.

The shared PaymentIntent schema now marks risk as optional and omitted with the checkout credential. Merchant credentials always receive the field, with null when no assessment is available. This documents the responses already returned by order payment creation and cancellation.

  • Breaking change
  • API
  • Subscriptions
  • Errors

Subscription state refusals use ACTION_NOT_ALLOWED#

Breaking: branch on ACTION_NOT_ALLOWED and details[0].reason. Merchant and buyer subscription routes now return this 409 error for state refusals. It replaces CANNOT_PAUSE, CANNOT_RESUME, CANNOT_RESUME_PAST_DUE_PAYMENT_REQUIRED, CANNOT_RESUME_INITIAL_PAYMENT_PENDING, SUBSCRIPTION_ALREADY_PAUSED, SUBSCRIPTION_RENEW_NOT_ALLOWED, SUBSCRIPTION_SCHEDULE_MUTATION_NOT_ALLOWED, SUBSCRIPTION_PAYMENT_RETRY_NOT_ALLOWED, and SUBSCRIPTION_CANCELED.

The single error detail identifies the subscription with resource_type: subscription, resource_id, and its stored current_status. State refusals omit param: subscription_id. Pausing an already paused subscription with a different duration returns reason field_locked and param: pause_duration_cycles; resume before choosing a new duration. When included, current_version is the subscription's version.

Use these reasons to choose the next step:

  • terminal_status: create a new subscription if the canceled subscription must be replaced.
  • cancellation_scheduled: reactivate the subscription before pausing or renewing.
  • trial_in_progress: wait for the trial to finish before renewing.
  • external_billing_schedule: use the system that owns the billing schedule.
  • delivery_hold: update the delivery preference before choosing another action.
  • payment_required: complete the outstanding payment before resuming.
  • initial_payment_pending: wait for the initial payment to complete before resuming.
  • inventory_wait: wait for inventory or update the subscription items before retrying payment.
  • unsupported_state: read the subscription and choose an action its state allows.

The old renewal reasons not_active and renewal_in_progress become terminal_status or unsupported_state, and in_progress, respectively. Every ACTION_NOT_ALLOWED reason uses remediation.retry: none. In particular, wait for an ongoing renewal and read its result; automatically retrying renew-now after it finishes could start another renewal. For an unknown reason, fall back to current_status.

Retained idempotency errors replay with the new public codes and reasons. Older errors that did not store enough facts use unsupported_state.

Breaking: checkout terms that no longer match the frozen subscription lines return ORDER_CHANGED_REFRESH_REQUIRED with no param. Read the current checkout before submitting new terms. A genuine concurrent terms update still returns CONCURRENT_MODIFICATION, without a field pointer, and can be retried with the same idempotency key.

  • Breaking change
  • API
  • Orders

Order tickets move to ticketed_event#

Order responses and webhook payloads replace purchased_event with ticketed_event. Update integrations to read the event's name, starts_at, timezone, and location from ticketed_event. Read ticket numbers from ticketed_event.ticket_numbers and ticket status from ticketed_event.ticket_status: pending, issued, or void. When ticket_status is void, ticket_numbers can be empty if a refund released the tickets before issue.

Every metadata key beginning with flint_ is reserved for Flint and hidden from public responses and webhooks. Other keys belong to the caller, and only caller keys count toward the 50-key limit on order metadata, including charge, fulfillment, and line-item metadata. Move integrations that read ticket metadata to ticketed_event. Buyer order responses now omit metadata entirely.

These are breaking changes to order responses and webhook payloads.

  • Breaking change
  • API
  • Webhooks

Webhook deliveries are listed at /v1/webhook-deliveries#

Breaking: the nested deliveries route is removed. GET /v1/webhook-events/{webhook_event_id}/deliveries no longer exists on any API version. Use GET /v1/webhook-deliveries?webhook_event_id=whev_... for the same result.

GET /v1/webhook-deliveries returns deliveries across all of your endpoints, newest first (created_at descending, then webhook_delivery_id). Filter it with:

  • webhook_event_id: deliveries of one event.
  • webhook_endpoint_id: deliveries to one endpoint.
  • status: pending, delivered, failed, or suppressed. Repeat the parameter to match more than one.
  • created_after (inclusive) and created_before (exclusive): bounds on created_at.

Page with page_size and page_token. A page token only works with the filters it was issued for. A webhook_event_id or webhook_endpoint_id that doesn't exist, or that your credential can't see, returns 404 RESOURCE_NOT_FOUND. Unlike GET /v1/webhook-events, the list includes deliveries of synthetic test events.

The operation ID listWebhookDeliveries moves to the new route, so regenerate SDKs and update calls that passed a webhook_event_id path parameter to pass it as a query parameter. To find deliveries that need a resend, filter by status=failed, then call POST /v1/webhook-deliveries/{webhook_delivery_id}/resend.

  • API

Resource version rollout preparation#

Additional resource versions are being prepared for a later release. Continue using the version and expected_version fields documented for each endpoint. The release that activates additional counters will describe their fields and any required collection fences.

  • Breaking change
  • API
  • Errors

State refusals include required reasons, and live-mode refusals return FORBIDDEN#

Breaking: branch on ACTION_NOT_ALLOWED and details[0].reason. This 409 response now requires a reason. remediation.retry is always none, and remediation.next_steps explains what to do for that reason. For an unknown reason, fall back to current_status and choose from supported_actions when present.

The shared reasons are already_applied, terminal_status, status_not_reached, in_progress, archived, field_locked, and unsupported_state. Reasons with a specific remedy are merchant_managed_receipts, derived_from_packages, on_hold, refund_unavailable, refund_created, payment_collected, cancellation_scheduled, trial_in_progress, external_billing_schedule, delivery_hold, payment_required, initial_payment_pending, and inventory_wait.

The registry also includes the existing credit_note_tax_reconciliation_required reason and its reconciliation steps.

Fulfillment and package transitions replace FULFILLMENT_ACTION_NOT_ALLOWED and PACKAGE_ACTION_NOT_ALLOWED with ACTION_NOT_ALLOWED, reason unsupported_state. FULFILLMENT_ON_HOLD becomes ACTION_NOT_ALLOWED with reason on_hold. The details identify the refusing resource and its current status. Unsupported transitions also include supported_actions, current_version, current_resource, and param: action.

Existing archived-product, archived-plan, and archived-risk refusals use reason archived. Closed and expired inventory reservation refusals use terminal_status, replacing the old expired reason. Payment receipt requests before success replace reason payment_not_succeeded with status_not_reached. Inventory lifecycle refusals use unsupported_state, and payout entries requested before payment use status_not_reached.

Breaking: live-mode authorization uses 403 FORBIDDEN with reason live_mode_required. This replaces API_VERSION_LIVE_MODE_REQUIRED, PROFILE_WRITE_LIVE_MODE_REQUIRED, and CUSTOM_DOMAIN_LIVE_MODE_REQUIRED. Merchant profile writes previously returned 409; checkout custom domain refusals previously returned 400. Sandbox, partner app, and organization management use the same reason instead of sandbox_management_requires_live_key, partner_app_management_requires_live_key, or organization_live_mode_required.

Send a live-mode API key or use the live dashboard environment. A merchant update with several live-only fields returns one detail per field, in request order; top-level param names the first. An unresolved credential mode also refuses these writes. Custom domain validation names domain_type; a settings update names checkout.custom_domain.

Retained idempotency errors replay with the new public codes and reasons. Update error handling and regenerate SDKs from the published OpenAPI schema. Payment, order, Returns, delivery, and subscription code migrations will follow separately.

  • API
  • Payments

Buyer payment responses omit risk#

Payment intent responses to customer sessions, checkout credentials, and invoice access tokens now omit risk instead of returning risk: null, matching the documented buyer response schema. Merchant responses continue to include risk.

  • API
  • Customers

Customer sessions return a resource and support sign-out#

Sign out with POST /v1/me/session/revoke, using the current customer session secret as Authorization: Bearer. The secret stays on your server. Sign-out works after the secret expires and after the session has been revoked. Repeating the call preserves the first revoked_at. A secret replaced by refresh is no longer accepted.

Create and refresh return CustomerSessionWithCredentials: the session resource plus secret and refresh_token. Both single-session revoke routes return CustomerSession with status: revoked and revoked_at, without credentials. The resource includes created_at, expires_at, and refresh_token_expires_at. Revoke-all keeps its customer_id and revoked_count result.

Customer sessions no longer return account_url or account_url_expires_at, and create rejects account_url_expires_in_seconds as an unknown field. For hosted sign-in, use POST /v1/customers/{customer_id}/access-links.

Refresh still requires Idempotency-Key. Use one fixed key per session, such as refresh-cses_...: Flint scopes it to the refresh token, so concurrent requests with the same token and key receive the same rotated credentials. On 409 IDEMPOTENCY_KEY_IN_PROGRESS, retry with the same key and token. Refreshing a used token with a different key revokes the session.

These changes apply to every supported API version.

  • Breaking change
  • API
  • Disputes
  • Webhooks

Embed dispute responses and use Flint dispute statuses#

POST /v1/disputes/{dispute_id}/response-sessions creates a browser session for responding to or viewing a dispute with Stripe Connect. It returns 201 with dispute_id and client_session. Initialize Connect with the returned publishable_key and account session client_secret, then pass components[0].payment to setPayment or the React payment prop. Keep the Flint API key on your server.

The route requires payments.disputes.write, which is now grantable to merchant API keys in live and test mode and included in full_access. An empty body or {} is accepted. A completed retry with the same Idempotency-Key creates a fresh single-use client secret. Sessions can open for closed disputes to show the submitted response. Bank returns return 409 ACTION_NOT_ALLOWED with reason bank_return_not_contestable; an unavailable linked payment returns the same code with reason dispute_payment_unavailable.

Breaking: inquiry statuses have new names. These changes apply to disputes, payment intent dispute statuses, payment intent activity and results, and their status filters:

Previous valueNew value
warning_needs_responseinquiry_needs_response
warning_under_reviewinquiry_under_review
warning_closedinquiry_closed

An unrecognized dispute state now returns other instead of under_review. Treat other as open, do not assume an outcome, and read it again later. You can still open a response session. Refund protection remains in effect.

Breaking: dispute reasons use a smaller vocabulary. product_unacceptable is now product_not_as_described, credit_not_processed is refund_not_processed, incorrect_account_details is bank_account_not_found, and bank_debit_not_authorized is debit_not_authorized. general, customer_initiated, check_returned, and bank_cannot_process become other. bank_account_closed and bank_account_restricted have been removed. Reason filters use the new values too. Unrecognized reasons return other.

Breaking: evidence fields. is_evidence_submission_past_due has been removed. Use is_evidence_deadline_passed for the deadline. has_evidence means evidence is saved, submitted or not; evidence_submission_count tells you whether it was submitted. can_respond_with_evidence means the dispute has an evidence response window and can remain true after the window closes or a decision is made. Use is_action_required to know whether a response is needed now. The response-unavailable reason provider_response_unavailable is now response_window_unavailable.

Breaking: one condition event per change. dispute.closed has been removed, and dispute.warning_closed is now dispute.inquiry_closed. The terminal events are dispute.won, dispute.lost, dispute.prevented, and dispute.inquiry_closed. Update webhook subscriptions to use them. dispute.needs_response also covers an inquiry escalating to a chargeback when the dispute needs a response now. dispute.updated is sent only for another public change when no condition event applies. A first observation sends dispute.created and, when applicable, one condition event, so a new bank return sends dispute.created and dispute.lost.

  • Breaking change
  • API
  • Errors
  • Checkout
  • Refunds

Throttles and duplicate error codes use shared names#

These error code changes apply immediately to every supported API version, including retained error responses replayed with an idempotency key. HTTP statuses stay the same.

The following throttles now return RATE_LIMIT_EXCEEDED with remediation.retry: same_request:

  • CUSTOMER_VERIFICATION_RATE_LIMITED
  • PAYMENT_METHOD_SAVE_CODE_LIMIT_REACHED for temporary phone and network throttles
  • EMAIL_CHANGE_RATE_LIMITED
  • RECEIPT_RATE_LIMITED
  • ORDER_RECEIPT_RESEND_RATE_LIMITED
  • INVOICE_REMINDER_RATE_LIMITED
  • CUSTOM_DOMAIN_VALIDATION_RATE_LIMITED
  • PAYMENT_LINK_RESOLUTION_RATE_LIMITED
  • TOO_MANY_PENDING_REPORTS
  • WEBHOOK_STREAM_CONNECTION_LIMIT
  • DAILY_LIMIT_EXCEEDED

Handle RATE_LIMIT_EXCEEDED instead of these names. When a limit belongs to one action, param names it, such as order_id for an order receipt resend or new_email for an email change. Honor Retry-After when present. Flint-Rate-Limited-Reason continues to identify the limiter scope, including demo-session for demo sandbox limits.

The permanent quota of 3 confirmation codes per saved card and channel returns 429 SAVE_PAYMENT_METHOD_CODE_LIMIT_REACHED with remediation.retry: none. Confirm through the other channel; waiting does not reset this quota. Receipt cooldowns and the rolling 24-hour demo-session cap now send the remaining wait in Retry-After.

CHARGE_REFUND_AMOUNT_EXCEEDS_REFUNDABLE and LINE_ITEM_REFUND_AMOUNT_EXCEEDS_REFUNDABLE now return AMOUNT_EXCEEDS_REFUNDABLE. Use param to identify the indexed charge or line item amount to reduce, and details[0].maximum_money for the remaining refundable amount and currency.

Creating another checkout session for an order that already has an open session now returns ALREADY_EXISTS with param: order_id. Read the existing session reference from details[0].current_resource instead of existing_checkout_session_id or blocking_resources. Reuse that session, or send its ID as replace_checkout_session_id to replace it. Flint omits current_resource when the caller can't read checkout sessions, such as a key with no checkout session access or a buyer credential, and on a replayed response whose credential can't read it.

DELIVERY_SELECTION_REPLACEMENT_LIMIT is now DELIVERY_SELECTION_REPLACEMENT_LIMIT_REACHED. For capacity errors, _LIMIT_REACHED means requests of that kind cannot succeed until capacity resets or a count drops. _LIMIT_EXCEEDED means a smaller request can fit within the remaining capacity. For the delivery selection history limit, finish the checkout or start another checkout session.

  • Breaking change
  • API
  • Gift-cards
  • Orders
  • Customer-accounts

Gift card command results and consistent field names#

Breaking: update gift card integrations to use the routes, result objects, and field names below. These changes apply to every supported API version. The removed routes return 404; old request fields and query parameters are rejected.

OldNew
POST /v1/gift-cards/{gift_card_id}/adjustments, createGiftCardAdjustmentPOST /v1/gift-cards/{gift_card_id}/adjust-balance, adjustGiftCardBalance
POST /v1/gift-cards/{gift_card_id}/cash-outs, createGiftCardCashOutPOST /v1/gift-cards/{gift_card_id}/cash-out, cashOutGiftCard
CreateGiftCardAdjustmentRequestAdjustGiftCardBalanceRequest
CreateGiftCardCashOutRequestCashOutGiftCardRequest
GiftCardWithCodeCreateGiftCardResult or RotateGiftCardCodeResult
GiftCardWithTransactionsAdjustGiftCardBalanceResult or CashOutGiftCardResult
GiftCardLoadWithCardCreateGiftCardLoadResult
GiftCardRedemptionWithCardGiftCardRedemptionResult
gift_card_transaction_idsgift_card_transactions, with full transaction objects
secret_recovery_expires_atcode_recovery_expires_at
payout_reference on cash-out requests, transactions, and the transaction filtercash_out_reference
Funding source.buyer_id, including refund provenancesource.customer_id
last_characters on gift cards, buyer gift cards, order selections and settlements, payment attempt redemptions, and purchased gift cardslast4
Notification delivery.provider_outcomes[].kinddelivery.events[].type
Notification delivery.has_more_provider_outcomesdelivery.has_more_events
GiftCardNotificationProviderOutcomeGiftCardNotificationDeliveryEvent

The result schemas replace their corresponding response wrappers as well. Issuance, loads, balance adjustments, cash-outs, and redemption create, capture, and cancel return data.gift_card and data.gift_card_transactions. Loads also return data.gift_card_load; redemption commands return data.gift_card_redemption. An adjustment that increases value returns its load. Manual redemption creation returns an empty transaction array until capture. Code rotation returns data.gift_card and recoverable code fields, with no transaction array. Issue and load return 201; adjustments, cash-outs, rotation, capture, and cancel return 200.

Transactions appear in posting order. A same-key retry returns the original transaction IDs and balance snapshots without posting again. On card commands, expected_version fences data.gift_card.version; capture and cancel fence data.gift_card_redemption.version. Code and code_recovery_expires_at remain available only during the 24-hour recovery window while that code is current.

idempotency_key is required and nullable on transactions, loads, and redemptions. It contains your key when you supplied one; it is null when Flint created the record or no key was sent. The list filters match only caller keys. Transaction reason is your adjustment reason on adjustments, customer_request on cash-outs, and null on every other transaction type, including funding_loss_accepted. Neither field uses an empty string as a missing value.

Notification retrieval returns up to 20 delivery attempts and 50 delivery events, with has_more_attempts and has_more_events. Attempts no longer include provider_status_code. Event types are accepted, delivered, bounced, and dropped; they replace processed, delivered, bounce, and dropped. accepted means Flint's email service accepted the message for delivery.

Gift card and order webhooks use the same new field names and null rules. sale_reference, reserved_money, and the funding-limit reason buyer_daily keep their names. Update SDKs after regenerating them against the new OpenAPI specification.

  • API
  • Invoices
  • Credit-notes

Inclusive invoice line totals#

Invoice line totals now keep inclusive tax inside the subtotal, matching the order total. A line with subtotal 1200 and included tax 200 has total_money 1200. Additive tax continues to be added to the discounted subtotal.

Credit notes also support invoices whose stored inclusive line total previously counted tax twice. Corrections use the original subtotal, discount, and included tax to limit credit to the amount billed. Issued invoice amounts stay unchanged. Existing credit notes with inconsistent tax still require reconciliation before further credit can be issued or spent.

  • Breaking change
  • API

Request validation uses INVALID_REQUEST#

Request validation failures now return 400 INVALID_REQUEST. Branch on INVALID_REQUEST, then read details[].param and details[].code to identify each input to correct. Every detail uses REQUIRED, INVALID_VALUE, UNKNOWN_FIELD, or NOT_ALLOWED. Whole-body failures name body; header failures name the header, such as Idempotency-Key or Flint-Version.

Fields that a checkout or customer credential cannot send now return 400 INVALID_REQUEST with NOT_ALLOWED, including customer_id on /v1/me routes. Conflicting credentials return INVALID_REQUEST with NOT_ALLOWED and conflicting_fields. A present but malformed expected_version uses INVALID_VALUE; an omitted required version uses REQUIRED.

Three domain codes keep their own handling: PAYMENT_OPTION_UNAVAILABLE, PROMOTION_DECLINED, and PAYMENT_PROCESSOR_REJECTED. Payment option amount limits now use PAYMENT_OPTION_UNAVAILABLE with reason amount_out_of_range, the payment option when known, and maximum_money when the amount exceeds the maximum. Payment update refusals use PAYMENT_PROCESSOR_REJECTED.

Inventory routing no longer returns 422. The reservation endpoint continues to reject caller-supplied assignments as UNKNOWN_FIELD. Failed scheduled invoice activities expose public error codes. Retried requests with retained failures use the same public error shapes.

Update code that branches on retired validation codes to use the input path and detail code. Correct each named input, then send the request again with a new Idempotency-Key. State-dependent validation conditions scheduled for separate changes keep their existing codes for now.

  • Breaking change
  • API
  • Errors

Inventory counts recover from stock that moves#

Breaking: every count observation requires expected_physical_version. PATCH /v1/inventory-counts/{inventory_count_id} now requires this on each entry in observations: the level's physical_version you read before counting that item. A level that doesn't exist yet reads as 0, and 0 is valid. A missing, null, negative, or fractional value returns 400 INVALID_REQUEST with param set to observations[i].expected_physical_version.

Before this change, a count captured each level's physical version once, when you created it, and nothing could refresh it. A sale, receipt, or correction after you created the count made it impossible to apply, even if you counted again with the correct expected_version. The only way out was to cancel the count and start another. Now each saved observation keeps its own version, and captured_physical_version on a count line means the version of the latest saved observation, not the version when the count was created.

To count again after stock moved, read the level, count the item, and save the complete observation set with a fresh expected_physical_version for that item. Send unchanged items back with their saved quantities and saved captured_physical_version. Reading a newer version without counting again doesn't make an old count current, so recount an item even when the number is the same. Holds and commitments don't move physical_version, so they never invalidate a count. The same count then applies without being canceled or recreated.

Breaking: stale physical counts return VERSION_CONFLICT. INVENTORY_COUNT_PHYSICAL_REVISION_CHANGED is removed. A PATCH whose expected_physical_version is stale, and an apply against a saved observation that has since gone stale, return 409 VERSION_CONFLICT. details has one entry per stale line with resource_type inventory_level, resource_id, inventory_count_line_id, inventory_item_id, location_id, captured_physical_version, and current_physical_version. A PATCH conflict sets param to observations[i].expected_physical_version and saves nothing. An apply conflict has no param, because the apply request has no field for it, and changes nothing. Before this change the apply error named param lines, which the request doesn't contain. A stale count expected_version keeps param expected_version and resource_type inventory_count.

A replayed response for an Idempotency-Key first used before this change returns VERSION_CONFLICT, not the removed code. Send a corrected count with a new key.

A count of items that keep selling can keep going stale. Count fewer items at a time, or pause physical movement for them while you count. See Inventory and the inventory guide.

The Dashboard now reads each counted item's version, marks an item whose stock moved after you counted it, and asks you to count it again instead of telling you to drop the count.

  • Breaking change
  • API
  • Inventory
  • Errors

Inventory provenance accepts opaque identifiers and publishes validation rules#

Inventory provenance accepts opaque actor and source identifiers, including picker@dc-3 and secret-garden-pos. Don't put secrets in these fields.

The OpenAPI schemas now publish the existing provenance rules:

  • external_actor_id and source_system.external_source_id accept at most 255 Unicode code points.
  • Source types pos, wms, erp, and other require external_source_id and occurred_at.
  • Source type manual does not allow external_source_id.
  • occurred_at can be at most 5 minutes in the future.

Public callers can send source types manual, pos, wms, erp, and other. Flint-authored events can return flint. Return receipt schemas publish their corresponding limits using received_at and source_system.source_system_type.

Breaking: inventory provenance validation returns 400 INVALID_REQUEST instead of INVENTORY_EVENT_PROVENANCE_INVALID. Check details[].code and param: missing fields use REQUIRED, invalid values use INVALID_VALUE, and an external source ID sent with type manual uses NOT_ALLOWED with both fields in conflicting_fields.

Merchant-authenticated checkout delivery quotes accept at most 1,000 inventory_assignments. More entries return 400 INVALID_REQUEST with details[0].code INVALID_VALUE and param inventory_assignments.

Breaking: INVENTORY_ROUTING_TOO_COMPLEX and inventory routing's 422 response are removed. Invalid inventory inputs return 400 INVALID_REQUEST with the affected field in param. Regenerate SDKs and request validators to pick up these published limits and conditions.

  • Breaking change
  • API
  • Invoices

Invoice delivery attempts return failure and suppression reasons#

Breaking: GET /v1/invoices/{invoice_id}/delivery-attempts no longer returns error_message. Use failure_reason when status is failed:

  • rejected: The email service refused the message, for example because of an invalid address.
  • delivery_unknown: Flint could not confirm the send. Check with support before resending.
  • canceled: The email was canceled before sending, for example because the invoice changed.

When status is suppressed, suppression_reason is merchant_managed_email or recipient_unsubscribed. Each reason field is omitted for other statuses. Legacy failed attempts without a stored reason return rejected.

Handle future values in both reason enums. The invoice.delivery_failed webhook continues to carry the invoice in data.object.

  • Breaking change
  • API
  • Onboarding

Merchant requirement codes and account session targeting#

Readiness requirement arrays now publish the Flint requirement codes in OpenAPI. The list can grow. Treat an unknown response code as additional_account_information.

Breaking: Send targeted_requirements when creating a merchant account session. It replaces targeted_requirement_ids in the request and in readiness action recommendations. Values such as business_website are unchanged. An unknown request code returns 400 INVALID_REQUEST with an INVALID_VALUE detail at the array item.

Breaking: Merchant account sessions no longer return requirements. Read Merchant.readiness.requirements with GET /v1/merchant for the current requirements.

Breaking: Merchant and User no longer return banners. Use Merchant.readiness to find blocked capabilities and the actions available to resolve them. Reading a merchant no longer fetches a payment processor account to compute banners.

The OpenAPI descriptions now explain the merchant account statuses and identify 409 VERSION_CONFLICT as the response to a stale expected_version on a merchant update.

  • API
  • Orders
  • Fulfillment

Order line items report fulfillment quantities#

Order line items now include a read-only fulfillment_quantities array. It has one entry for a single item, or one entry per bundle component that requires fulfillment, with bundle_component_id set on component entries. Each entry reports required_quantity and splits it into unassigned_quantity, assigned_quantity, fulfilled_quantity, canceled_quantity, and failed_quantity. Together the five add up to required_quantity.

Read unassigned_quantity to see how much a new fulfillment can still claim. It includes quantity returned by a fail with release_quantity: true; a fail with release_quantity: false counts in failed_quantity instead. Single items count in line units and bundle components in component units. The array is omitted for a line that needs no fulfillment: when every line on the order has fulfillment, a line without fulfillment_quantities needs none. If any line has no fulfillment, as with a line older than recorded fulfillment requirements, the order's obligations cannot be computed and no line has the array, so treat the order's obligations as unknown.

This change is additive. No existing field changed, and the field also appears on orders in webhook payloads.

  • API
  • Partners

Preparation for partner app updates#

This release prepares support for versioned partner app edits, overlapping client secrets during rotation, and identifying who revoked an install.

The public API is unchanged. The new behavior and any required integration changes will be announced when API support ships.

  • Breaking change
  • API
  • Security
  • Webhooks
  • Partners

Webhook mutations require merchant credentials#

Partner install tokens now return 401 INVALID_CREDENTIAL with reason wrong_credential_type on these routes, on every supported API version:

  • POST /v1/webhook-endpoints
  • PATCH /v1/webhook-endpoints/{webhook_endpoint_id}
  • DELETE /v1/webhook-endpoints/{webhook_endpoint_id}
  • POST /v1/webhook-endpoints/{webhook_endpoint_id}/rotate-secret
  • POST /v1/webhook-endpoints/{webhook_endpoint_id}/test-events
  • POST /v1/webhook-deliveries/{webhook_delivery_id}/resend

Use the endpoint owner's API key with webhooks.write or an authorized Dashboard session. To manage partner-source endpoints, use the app owner's API key. Install tokens retain their existing webhook read access, and installed-merchant deliveries continue to require an active grant with the event's resource-read scope.

Delivery attempt reads, resend responses, and test-event action responses omit response_headers, response_body_excerpt, and response_content_type unless the caller can read the event's resource. A receiver can echo the webhook payload in these fields, so they now require the same access as the event payload. Delivery status, attempt counts, and other diagnostic fields remain available.

  • Breaking change
  • API
  • Refunds

Refund create names its type, a recorded refund is always 201, and refund statuses split in two#

Breaking: POST /v1/refunds requires refund_type, which is full, amount, or targets. A body without it returns 400 INVALID_REQUEST with a REQUIRED detail and param: refund_type, and nothing is refunded. Before, a body with no amount and no named targets refunded everything that was still refundable.

refund_typeRefundsRequiresNot accepted
fullEverything still refundable on the targetorder_id, payment_intent_id, or gift_card_load_idamount_money, line_items, charges, tax_breakdown_refunds, tender_allocations
amountThe amount_money you sendamount_money, plus order_id, payment_intent_id, gift_card_load_id, or tender_allocationsNothing beyond the per-target rules
targetsThe named line_items, charges, and tax_breakdown_refundsAt least one of those, and an order through order_id or a payment_intent_id on an orderamount_money, tender_allocations, gift_card_load_id

Flint checks the type before it looks anything up. An unknown value returns INVALID_VALUE. A field the type doesn't accept returns NOT_ALLOWED for that field, with conflicting_fields naming refund_type and the field. amount without amount_money and targets with none of the three target lists return REQUIRED. The refund resource doesn't echo refund_type.

Breaking: tender_allocations now need refund_type: "amount" with an amount_money that equals their sum. Before, the allocations alone set the total. Named targets plus allocations use amount too. A sum that doesn't match returns INVALID_REFUND_TENDER_ALLOCATION.

Breaking: refund_method is removed from the create request and from the GET /v1/refunds filter. A body that sends it returns INVALID_REQUEST with an UNKNOWN_FIELD detail, and the query parameter returns UNSUPPORTED_QUERY_PARAM. Refunds always return to the tenders that paid, and responses still report refund_method: "original_payment".

Breaking: once Flint has recorded a refund, POST /v1/refunds returns 201 with that refund, whatever state it reached. A 4xx other than 409 IDEMPOTENCY_KEY_IN_PROGRESS now means no refund was created; that code means the first request with the key may still be running, so retry with the same key. These cases returned an error even though a refund existed and its webhooks were sent:

  • 409 REFUND_INSUFFICIENT_AVAILABLE_BALANCE is retired. The response is 201 with status: "failed" and failure_reason: "insufficient_available_balance".
  • A dispute or bank return found after Flint recorded the refund, which returned 409 PAYMENT_INTENT_DISPUTED or 409 NOTHING_TO_REFUND with reason payment_disputed, is now 201 with status: "failed" and failure_reason: "payment_disputed".

PAYMENT_INTENT_DISPUTED and NOTHING_TO_REFUND still return 409 when Flint refuses the request before it records a refund. A refund on a payment with no order is unchanged: it returns 201 with status: "pending" and fails later with the same reasons.

Retrying a failed refund with the same Idempotency-Key returns the same 201 and the same refund with Idempotency-Replayed: true, and Flint makes no new call to the processor. To try again after you fix the cause, create a new refund with a new key. The refund.updated and refund.failed events are sent as before.

Breaking: refund statuses split into two sets.

  • Refund.status is pending, succeeded, partially_succeeded, or failed. This also applies to the buyer refund, to latest_refund_status on credit notes, and to the status filter on GET /v1/refunds and GET /v1/me/refunds. Filtering by in_transit, requires_action, or canceled returns 400 INVALID_REQUEST with an INVALID_VALUE detail, because those values never matched a refund.
  • Each payment_refunds and tender_allocations entry is pending, requires_action, succeeded, failed, or canceled. in_transit is reported as pending, and partially_succeeded belongs only to the refund. Refund and Return webhook payloads use these values for payment refunds, and credit note payloads use the four refund values.

Only failed is final. A succeeded refund can become partially_succeeded or failed, and a partially_succeeded refund can become failed, when a bank rejects a credit it accepted. The Refunds guide marked partially_succeeded as final, which was wrong.

PATCH /v1/refunds/{refund_id} accepts external_reference_id as well as metadata. Leave it out to keep it, send null to clear it, and send 1 to 255 characters to set it. An empty string returns INVALID_REQUEST with an INVALID_VALUE detail. A body with neither field returns NO_FIELDS_TO_UPDATE.

Credit note refunds are unchanged: POST /v1/credit-notes/{credit_note_id}/refunds still refunds all available credit when you leave out amount_money.

To migrate:

  1. Add refund_type to every POST /v1/refunds body. Use full where you left out the amount and targets, amount where you sent amount_money or tender_allocations, and targets where you named only line items, charges, or tax breakdowns.
  2. When you send tender_allocations, send amount_money equal to their sum.
  3. Remove refund_method from create bodies and list queries.
  4. Read status on every create response. Treat failed as a refund that exists, not as an error, and remove handlers for REFUND_INSUFFICIENT_AVAILABLE_BALANCE.
  5. Stop filtering refunds by in_transit, requires_action, or canceled, and treat only failed as final.

See Refunds and Idempotency.

  • Breaking change
  • API
  • Returns

Returns use release timing and stable adjustment IDs#

Returns now use release_timing in requests, responses, previews, and webhooks. Replace refund_timing with release_timing; the old request field is rejected. Values are unchanged. The timing gates every resolution type, including exchanges and replacements. manual holds execution until you call POST /v1/return-resolutions/{return_resolution_id}/release.

PATCH /v1/return-resolutions/{return_resolution_id} now accepts adjustments: [...] instead of adjustment_set: { adjustments: [...] }. Each returned adjustment includes return_resolution_adjustment_id. Send that ID to keep and update a member, omit the ID to add one, and omit an existing member to remove it. Send [] to clear the collection. Omitting adjustments leaves it unchanged. An unchanged waived adjustment keeps its waiver attribution.

Replacing line_items, replacement_line_items, or adjustments requires expected_version. Replacing a return reason's category_handles also requires expected_version. A missing fence returns 400 INVALID_REQUEST with detail code REQUIRED and param: expected_version.

Return policies and return reasons no longer include supported_actions. Use policy status with PATCH to switch between active and inactive, and DELETE to retire a policy or a merchant-created reason. Flint-sourced reasons cannot be deleted.

Receipt, inspection, and disposition endpoints now have separate resource tags: return_receipts, return_inspections, and return_dispositions. Nested create descriptions name the top-level read paths. The operation ID for POST /v1/returns/{return_id}/process is now processReturn.

  • Breaking change
  • API
  • Risk
  • Webhooks

A failed decline refund reopens the review, and expiry shows as resolving#

New: review.reopened. When the refund for a declined review fails, the review goes back to open, and Flint sends review.reopened with the review. Before this, nothing told you. The event needs the risk.read scope, like the other review events. See Resolve reviews.

New: Review.last_resolution_failure. It is null or {refund_id, failed_at}: the most recent decline whose refund failed. It stays on the review after a later decision closes it. GET /v1/refunds/{refund_id} shows why the refund failed. Decline again with a new Idempotency-Key to start another refund.

New: updated_at on reviews and fraud warnings.

Breaking: a decline whose refund already failed returns 200. The response is the open review with last_resolution_failure set, not 202. A decline returns 202 only while the review is resolving.

Breaking: a decline's 202 has no Retry-After header. Flint can't tell how long a refund takes to settle. Wait for review.closed or review.reopened, or read the review with backoff.

Breaking: reopened reviews clear refund_id. refund_id is now set only once a refund has closed the review. Before, a reopened review kept the failed refund's ID there.

Breaking: an expiring review is resolving. It had been reported as open. While Flint cancels an authorization nobody resolved in time, the review has status: "resolving", pending_action: "expire", resolution_started_at set to when expiry began, and resolution_started_by: "system". pending_action gains the value expire, and GET /v1/reviews?status=resolving returns expiring reviews instead of status=open. Approve and decline during expiry return 409 REVIEW_RESOLUTION_IN_PROGRESS instead of PAYMENT_OPERATION_IN_PROGRESS.

Breaking: PAYMENT_OPERATION_IN_PROGRESS is declared on approve and decline. It means a capture, cancellation, refund, or payment update is running on the payment. details[0].blocking_resources names the payment intent, and nothing was started, so the same request can be sent again once that operation finishes. A claim that needs recovery returns it too, where it had returned 500 INTERNAL_ERROR.

Breaking: values that were never produced are removed. Review.ip_address_location is gone, because Flint has no IP geolocation. opened_reason is rule only, and closed_reason no longer lists other. manual can return when reviews can be opened by hand.

Breaking: risk.score on a review is omitted when no score is available, as on a payment intent, instead of being null. In the OpenAPI schema, score is optional and not nullable.

Breaking: review and fraud warning webhooks use the REST shape. The data.object of review.opened, review.closed, review.reopened, fraud_warning.created, and fraud_warning.updated matches the GET response. In particular, payment_summary carries only its documented fields, and timestamps are RFC 3339 to the second.

Regenerate SDKs and validators that read the OpenAPI document.

  • API
  • Payments
  • Risk

Risk list checks no longer fail on accepted email addresses#

Payments with an email address Flint accepts, such as john..doe@example.com, no longer fail risk evaluation because the address cannot be compared against an email block list. Values that cannot be stored in a risk list count as non-members. The rule still evaluates its other conditions, so an IP address on a block list still blocks the payment.

You can now disable or delete a risk rule whose saved predicate is no longer valid. You can also update its description. Activating the rule or changing its action or predicate still requires a valid predicate.

  • API
  • Sandboxes

Sandbox resets link archived and replacement sandboxes#

Resetting a sandbox now returns replaces_sandbox_id, the ID of the sandbox you reset. The archived sandbox exposes replaced_by_sandbox_id, the ID of its replacement, through GET /v1/developer/sandboxes/{sandbox_id} and the sandbox list. Both fields are omitted when there is no corresponding reset.

After multiple resets, each sandbox links to its immediate predecessor and replacement. Follow replaced_by_sandbox_id to recover the latest replacement ID if you lost a reset response. Retrying a reset within the idempotency window with the same Idempotency-Key still returns the original stored response.

The sandbox creation description now states that new sandboxes start empty. Send issue_test_key: true to receive a test key bound to the new sandbox.

  • API
  • Invoices
  • Credit-notes
  • Returns

Shared invoice credit limits for Returns and credit notes#

Issued credit notes and committed Return lines now share each invoice line's credit limit. A Return request reserves its quantity and value before a resolution moves money. Declining or canceling quantity releases that reservation; voiding an issued credit note releases its correction.

Credit-note corrections beyond the shared remainder return 409 CREDIT_NOTE_AMOUNT_EXCEEDS_CREDITABLE. Creating or editing a Return beyond that remainder returns 409 RETURN_ACTION_NOT_ALLOWED. Partial corrections and Returns remain available within the remaining invoice and paid settlement value.

Existing overlapping commitments also block new Return resolution effects until the overlap is resolved. Issued invoices and historical documents stay unchanged.

  • API

Enforce published text limits before normalization#

Invoice settings now enforce their published limits: 4096 characters for default_memo and default_footer, 12 for invoice_number_prefix and credit_note_number_prefix, and 255 for reply_to_email and timezone. Both prefixes accept only ASCII letters, digits, underscores, and hyphens. These new checks apply to submitted fields, so you can correct older values individually and continue editing other settings.

Feedback text, buyer address fields, delivery recipient fields, risk-list values, merchant business names, payment-method domains, buyer checkout return URLs, and return decline notes now enforce their published limits before normalization. Adding whitespace or line breaks cannot bypass a limit. Selected-option validation also checks the supplied text before trimming. Risk-list values must fit the 512-character limit both before and after normalization.

Character limits count Unicode code points. Order receipt emails now accept up to 254 characters, including multibyte characters. Payment-method hostnames remain ASCII-only, with a 253-character limit.

The schema also reflects existing normalization and reference handling. Buyer address countries are trimmed and uppercased before ISO country validation. Delivery recipient phones are trimmed and accept + followed by 7 to 15 digits, with a nonzero first digit. Modifier IDs retain their existing resource validation without an uppercase-only schema pattern. Category handles retain their existing length checks and lookup errors.

  • Breaking change
  • API
  • Subscriptions

Create and import subscriptions from offers#

POST /v1/subscriptions and create-mode POST /v1/subscription-previews now require terms. Move subscription_plan_id, quantity, billing_interval, and billing_interval_count into that object for plan subscriptions:

JSON
{
  "customer_id": "cus_123",
  "terms": {
    "type": "subscription_plan",
    "subscription_plan_id": "plan_123",
    "quantity": 1
  },
  "billing_start": { "type": "immediate" }
}

To create an offer subscription, use terms.type: "subscription_offer", an explicit billing_interval and billing_interval_count, and line_items with variant_id, subscription_offer_id, and optional quantity. Lines can come from several active offers when they share the selected interval, currency, and required delivery method.

Offer discounts are frozen when you create the subscription and apply from the first charge under the promotion's conditions. Creating or importing an offer subscription does not consume a promotion use. An unavailable promotion still blocks creation. Previews create no subscription and attach no discounts.

For subscribers who already paid elsewhere, use billing_start.type: "imported" and provide period_started_at and optional completed_cycles. Imports skip the initial charge. With a Flint-owned schedule, the next charge is at the end of the imported period. An external schedule waits for you to supply the next billing date. Using immediate collects the first payment now.

Every subscription response and subscription webhook object now includes terms_type: subscription_plan or subscription_offer. Offer subscriptions omit subscription_plan_id. Merchant subscription lists accept the terms_type filter, including repeated or comma-separated values.

  • Breaking change
  • API
  • Subscriptions
  • Webhooks

Subscription events cover every change, activation follows the first paid period, and billing-schedule initiated_by is removed#

Breaking: initiated_by is removed from PATCH /v1/subscriptions/{subscription_id}/billing-schedule. It never changed what Flint did, so both owner shapes (flint and external) stop accepting it, and a request that sends it returns 400 INVALID_REQUEST with an UNKNOWN_FIELD detail and param initiated_by. Remove it from your requests. The skip route keeps initiated_by, which now has a documented effect: Flint's skip email to the buyer says the buyer skipped for buyer, and that the store skipped otherwise.

Breaking: subscription.activated follows the first paid period. It used to be sent when a trial ended, before the first charge, and never for a subscription created already paid. Now it is sent once per subscription, in the same commit as its first paid period:

  • A subscription that starts incomplete sends it with the first subscription.payment_succeeded, whether the start is immediate or scheduled.
  • A subscription created already paid, such as from a checkout session, a payment link, or an order with subscribed lines, or one imported with billing_start.imported, sends it right after subscription.created.
  • A trial that converts sends it with the first charge. When the trial ends the status becomes active while Flint charges, and Flint sends subscription.updated. A failed charge moves the subscription to past_due, and subscription.activated is sent when a later payment succeeds.

Provision on subscription.activated. trialing subscriptions also have access. Before, checkout signups had to wait for the first renewal's subscription.payment_succeeded.

Breaking: subscription.cycle_skipped is sent alone. A skip no longer sends subscription.updated beside it.

New: subscription.updated covers every change with no more specific event. Flint now sends it for changes to metadata, external_reference_id, quantity, billing interval, payment method, and line items, for a delivery hold cleared without a charge, and for the end of a trial, along with the schedule and attention changes it already covered. A change that sends a more specific subscription event, such as subscription.delivery_updated or subscription.reactivated, sends only that event. Flint sends at most one subscription.updated per change.

New: Subscription.cancellation_reason. It is buyer or merchant when that side asked to cancel, payment_failed for a canceled subscription with no recorded request, and other for a scheduled cancellation with no recorded requester. It is present while cancel_at_period_end is true or the status is canceled, and absent otherwise. The set of values can grow.

New: Subscription.delivery.updated_by. It is buyer, merchant, or migration, and says who last changed the delivery preference. It is absent until the preference changes after creation. The set of values can grow.

Docs corrected. Subscription events carry the subscription in data.object and no event-only fields. Earlier guides listed reason, initiated_by, changed_by, and cancel_at on some events, and those never arrived. Read cancellation_reason, delivery.updated_by, and current_period_end_at on the subscription instead. Flint does not record who undid a scheduled cancellation.

See Subscription billing and the webhook event catalog.

  • Breaking change
  • API

Response warnings use typed fields#

Response warnings now always include code, severity, and message. Severity is warning or critical. Accept new warning codes as they are added.

Update code that matches these renamed warnings:

Previous codeCurrent code
MERCHANT_ONBOARDING_PAST_DUE_WARNINGmerchant_onboarding_past_due
MERCHANT_ONBOARDING_DEADLINE_WARNINGmerchant_onboarding_deadline
MERCHANT_CAPABILITY_PENDING_WARNINGmerchant_capability_pending
MERCHANT_CAPABILITY_INACTIVE_WARNINGmerchant_capability_inactive
PAYOUT_SETTINGS_RECONCILINGpayout_settings_reconciling
RULE_EVALUATION_STAGE_CHANGEDrule_evaluation_stage_changed
BOOTSTRAP_SCOPES_REDUCEDapi_key_scopes_reduced

The promotion_declined and promotion_removed codes keep their names.

Warnings no longer include context or resource. Read resource_type with resource_id when a warning identifies a resource. Read capability and current_deadline_at directly on merchant readiness warnings, previous_stage and stage on risk rule stage warnings, and removed_scopes on API key scope warnings. These fields are included when they apply to the warning.

Risk previews return the same warning shape in data.warnings that successful requests use in meta.warnings. The separate RiskRuleWarning schema has been removed. Regenerate clients from the updated OpenAPI document and replace reads of the removed fields with the typed fields above.

  • Breaking change
  • API
  • Errors

Stale versions on catalog, subscription, plan, and risk writes return VERSION_CONFLICT#

Breaking: stale versions are VERSION_CONFLICT. When the expected_version you send no longer matches, these writes return 409 VERSION_CONFLICT instead of 409 CONCURRENT_MODIFICATION:

  • PATCH and DELETE on products, product variants, and bundles, including updates that replace images.
  • Every subscription write that takes expected_version: PATCH, line items, Order now, pause, resume, cancel, reactivate, the payment method change, the billing schedule update, and skip, on merchant routes and on the buyer routes under /v1/me.
  • PATCH and DELETE on subscription plans. A plan DELETE now compares expected_version before it archives, so a stale value no longer depends on a race.
  • PATCH and DELETE on risk rules.

param is expected_version. details[0] carries resource_type (product, product_variant, bundle, subscription, subscription_plan, or risk_rule), resource_id, and current_version, the version Flint saw. Read the resource, rebuild the change, and send it with a new Idempotency-Key. Modifier group and modifier set conflicts now carry current_version too.

Breaking: CONCURRENT_MODIFICATION means a lost race, not a stale version. Flint returns it when another write landed while your request ran and your basis was still current: you sent no expected_version, or the one you sent still matches. It has no param, details[0] names the resource_type and resource_id, and nothing was applied. Retry the same request with the same Idempotency-Key. Updates that replace images on products, variants, bundles, and plans are the exception: a lost race there returns VERSION_CONFLICT, even when current_version equals the version you sent, so read the resource again and send the rebuilt request with a new Idempotency-Key. Before this change, subscription and plan writes stored this error against the key, so the same-key retry replayed the conflict for 24 hours. It now runs again.

Breaking: schedule changes and skips. A billing schedule update or skip that loses the race twice without an expected_version returns 409 CONCURRENT_MODIFICATION instead of SUBSCRIPTION_SCHEDULE_MUTATION_NOT_ALLOWED. When SUBSCRIPTION_SCHEDULE_MUTATION_NOT_ALLOWED is returned for a status, details[0].current_version is now the subscription's version, not the billing schedule revision.

Breaking: RISK_CONTROL_CONFLICT is removed. It covered a stale rule version, an archived rule or list, and internal races. They now return:

ConditionResponse
A stale expected_version on a risk rule409 VERSION_CONFLICT with resource_type risk_rule and current_version
Changing an archived rule, or adding items to or deleting items from an archived list409 ACTION_NOT_ALLOWED with current_status archived and resource_type risk_rule or risk_list
A risk control write that loses a race409 CONCURRENT_MODIFICATION

Breaking: published fence requirements. The OpenAPI schemas now publish "required when this field is present" fences, such as expected_version with images or options, as dependentRequired instead of anyOf with not. Regenerate SDKs and validators that read the old construct. PATCH /v1/inventory-counts/{inventory_count_id} now lists expected_version as required, and a request without it returns 400 INVALID_REQUEST with details[0].code REQUIRED and param expected_version.

A response replayed for an Idempotency-Key first used before this change returns the new code. A replayed stale-version conflict is VERSION_CONFLICT without current_version when the original didn't record it, and a replayed RISK_CONTROL_CONFLICT is VERSION_CONFLICT. Read the resource, then retry with a new key.

Orders, order line items, and delivery selections keep their current conflict codes for now. See Error handling.

  • Breaking change
  • API
  • Errors

Stale versions on customer, settings, invoice, and other writes return VERSION_CONFLICT with the current value#

This continues the stale version rollout for the remaining writes that take an expected_version or expected_api_version.

Breaking: INVOICE_PAYMENT_TERM_CHANGED is removed. PATCH and DELETE /v1/invoice-payment-terms/{invoice_payment_term_id} now return 409 VERSION_CONFLICT when expected_version is stale. param is expected_version, and details[0] carries resource_type invoice_payment_term, resource_id, and current_version, the term's version. DELETE now compares expected_version before it archives the term. Read the term and retry with its version and a new Idempotency-Key. A write that loses a race while your version still matches, or with no version sent, returns 409 CONCURRENT_MODIFICATION.

Breaking: customer and settings races are classified by your version. A stale expected_version on PATCH /v1/customers/{customer_id} or PATCH /v1/settings returns 409 VERSION_CONFLICT with param expected_version, the resource reference, and current_version. Customers use resource_type customer. Settings use resource_type settings and the settings ID as resource_id, and a stale catalog.expected_version is reported the same way with that param. When another write lands while the request runs and your version, if you sent one, still matches, the response is 409 CONCURRENT_MODIFICATION without param. Customer writes without a version used to return VERSION_CONFLICT for this race. Nothing changed. Retry the same request with the same Idempotency-Key.

Conflicts that lacked a current version now carry it. When another write wins after Flint compares your expected_version, the 409 VERSION_CONFLICT includes the resource reference and details[0].current_version:

  • Invoice updates, issue, void, mark uncollectible, reminders_paused, public link regeneration, and recording or reversing a manual payment, with resource_type invoice. If the race leaves your version current, these return 409 CONCURRENT_MODIFICATION without param, except issue, which returns 409 INVOICE_ISSUE_IN_PROGRESS. Retry either with the same request and Idempotency-Key.
  • Credit note updates, issue, allocations, allocation reversals, voids, and refunds, with resource_type credit_note.
  • Merchant profile and payment link updates, which return 409 CONCURRENT_MODIFICATION instead when your version is still current.
  • Modifier group and modifier set updates.

Breaking: credit note refunds retry payment changes. A refund, or an issue with a refund, that finds the invoice's payments changed while the credit note's version still matches your expected_version used to return VERSION_CONFLICT at once. Flint now retries for up to three attempts in total, as it already did without a version, and then returns 409 CONCURRENT_MODIFICATION with reason credit_note_refund_retry_exhausted. A version that moved still returns VERSION_CONFLICT with the new current_version.

Breaking: invoice void contention has no param. When a dependent resource changes while a void runs and your version, if any, still matches, void returns 409 CONCURRENT_MODIFICATION without param: invoice_id. details[0] names the invoice. Nothing is voided. Retry the same request with the same Idempotency-Key. A stale expected_version returns VERSION_CONFLICT with current_version.

New fact: current_api_version. A stale expected_api_version on a webhook endpoint or partner app update returns 409 VERSION_CONFLICT with param expected_api_version and details[0].current_api_version, the API version the resource is pinned to now. The October 18 entry promised this fact, but Flint never returned it. These conflicts have no current_version. They carry resource_type webhook_endpoint or partner_app and the resource's resource_id.

Gift card redemption create names the gift card. A stale expected_version on POST /v1/gift-card-redemptions now includes resource_type gift_card and the card's resource_id. Previously resource_id was missing because the path has no gift card ID.

Delivery configuration conflicts name the target.

  • A stale expected_version on POST /v1/delivery-revocations includes resource_type (delivery_method or delivery_rate_callback) and the target's resource_id.
  • A stale expected_catalog_default_version on POST /v1/delivery-profiles/{delivery_profile_id}/assign-to-unconfigured returns VERSION_CONFLICT with resource_type settings and current_version, the catalog version. resource_id is the settings ID and is omitted when the environment has no settings record yet.

A response replayed for an Idempotency-Key first used before this change returns the new code. A replayed INVOICE_PAYMENT_TERM_CHANGED is VERSION_CONFLICT. Read the resource, then retry with a new key.

Orders, order line items, and delivery selections keep their current conflict codes for now. See Error handling.

  • Breaking change
  • API
  • Webhooks

Resend pending webhook deliveries and create missing ones#

Breaking: resend works while Flint is still retrying. POST /v1/webhook-deliveries/{webhook_delivery_id}/resend on a pending delivery returned 409 WEBHOOK_DELIVERY_IN_FLIGHT. It now sends the event to the endpoint immediately and returns that attempt, like a resend of a failed or delivered delivery.

  • If your endpoint answers 2xx, the delivery becomes delivered and its remaining retries are canceled.
  • If the attempt fails, the delivery stays pending with the same next_retry_at. Only last_response_code and last_error change.
  • A resend never counts toward automatic_attempt_count or max_attempts. Attempt numbers count every attempt, so a resend between two retries takes the next number.

WEBHOOK_DELIVERY_IN_FLIGHT now means only that another attempt for the same delivery is running right now. It is returned before anything is sent, carries Retry-After: 10, and has remediation.retry of same_request: wait, then send the same request with the same Idempotency-Key.

Create a delivery for an event an endpoint never received. POST /v1/webhook-deliveries takes webhook_event_id, webhook_endpoint_id, and an optional reason_message. It creates the delivery, attempts it before responding, and returns 201 with the webhook delivery. Use it for events published while an endpoint was disabled, or to backfill a new endpoint from retained events.

  • The event's payload must still be retained (14 days), the endpoint must be enabled, and the endpoint must subscribe to the event type now. Otherwise the request returns 409 WEBHOOK_EVENT_PAYLOAD_EXPIRED, 409 WEBHOOK_ENDPOINT_NOT_ACTIVE, or the new 409 WEBHOOK_ENDPOINT_NOT_SUBSCRIBED.
  • Each event and endpoint pair has one delivery. A second create returns 409 ALREADY_EXISTS with the existing delivery in current_resource.
  • The first attempt has the new delivery_trigger value manual_delivery. If it fails, the delivery follows the normal automatic schedule, with its first retry after about 30 seconds.
  • Like the other webhook write routes, it requires an API key or Dashboard session with webhooks.write, not a partner install token.

Higher limits for recovery. Resend and create now use the write rate class (20 per second and 600 per minute per key) instead of external_provider_action (2 per second for resend), so recovering thousands of events takes minutes.

Disabling an endpoint suppresses its pending deliveries immediately, in the same update, not at their next attempt. Re-enabling it doesn't restart them. The webhooks guide walks through resending suppressed deliveries and creating the missing ones.

  • Breaking change
  • API
  • Webhooks

Webhook endpoint status and secret rotation#

Webhook endpoint changes apply immediately to every supported API version:

  • Send status: active or status: inactive instead of enabled on create and update. Responses return status. Inactive endpoints receive no new events and their pending deliveries are suppressed.
  • Create requires a non-empty enabled_events. Updates reject an empty list. Send ["*"] for every event type the endpoint's sources deliver, including future types. * must be the only entry and cannot be used with installed_merchants.
  • DELETE returns the endpoint with status: deleted. Its endpoint record is removed, pending deliveries are suppressed, and subsequent retrieval returns 404.
  • Secret rotation returns the endpoint, including the new secret, which is shown once. The replaced secret keeps signing for 24 hours by default. previous_secret_expires_at is present while it still signs.
  • To stop a leaked secret immediately, rotate with expire_previous_secret_at set to the current RFC 3339 time or a past time. Deploy the new secret to your receiver; verification fails until it uses that secret. You can choose a future expiry up to 7 days away. Rotating again during an overlap ends the older previous secret immediately, so deliveries carry at most two signatures. Rotation no longer returns WEBHOOK_SECRET_ROTATION_OVERLAP_ACTIVE.
  • api_version_rollback_expires_at accompanies api_version_previous while a supported previous version is available for rollback.
  • Breaking change
  • API
  • Invoices

Build invoice pages with the buyer invoice document#

GET /v1/me/invoices/{invoice_id} and the invoice in POST /v1/me/invoices/{invoice_id}/checkout-session now include the hosted invoice document. Both customer sessions and invoice access tokens receive ordered issue and balance rows, available payment options, collection mode, and merchant branding and support details. Active payment attempts and payment-option cost comparisons appear when available. Invoice lists omit these fields.

Closing total and amount-due rows are informational and state the sum of the contributing rows. Payment options are empty when the buyer cannot pay the invoice online now. A new checkout's cost comparison uses its collection amount, including a selected installment, before any payment attempt exists.

GET /v1/me/invoices/{invoice_id} with an invoice token now stamps viewed_at once. Token checkout stamps it after checkout is created or reused. Customer-session reads leave it unchanged.

Checkout creation with an invoice token is now bound to that token. If the merchant regenerates the link during creation, the request returns 409 INVOICE_LINK_UNAVAILABLE without param. The buyer must use the current invoice link.

  • API
  • Customers
  • Orders
  • Fulfillment

Buyer responses omit merchant inventory, tax configuration, and courier operations#

Buyer responses immediately omit these merchant fields on every supported API version:

  • Order.inventory_routing_source
  • Order.inventory_exception_status
  • Order.inventory_exception_reason
  • Order.tax.location.location_id
  • Order.tax.location.device_id
  • Customer.tax_exempt
  • Fulfillment.outcome.notes
  • Fulfillment.local_delivery_details.service_area_id
  • Fulfillment.local_delivery_details.external_delivery_id

This applies to customer profile reads and updates, order and fulfillment reads under /v1/me, checkout order reads, and order results nested in subscription renewal, delivery selection, and payment actions. Retrying a completed request with the same idempotency key also omits these fields immediately.

Buyers retain the applied transaction exemption at Order.tax.exemption.tax_exempt, pickup destination choices, delivery instructions, normalized outcome reasons, tracking, public courier support details, and client payment setup and action payloads. Merchant API credentials, Dashboard, and merchant webhooks retain their merchant fields.

  • Breaking change
  • API
  • Webhooks
  • Catalog
  • Promotions

Catalog changes send webhook events, and deleted promotion codes read archived#

Catalog resources now send webhook events, so a point-of-sale system, ERP, or search index can stay current without polling. Each event carries the resource as its GET returns it in data.object.

  • product.created, product.updated, and product.archived
  • product_variant.created, product_variant.updated, and product_variant.archived
  • bundle.created, bundle.updated, and bundle.archived
  • category.created, category.updated, and category.deleted
  • modifier_group.created, modifier_group.updated, and modifier_group.archived
  • modifier_set.created, modifier_set.updated, and modifier_set.archived
  • promotion.created, promotion.updated, and promotion.archived
  • promotion_code.created, promotion_code.updated, and promotion_code.archived

created follows a POST, archived and deleted follow a DELETE, and updated follows every other committed change, including a status change between active and inactive. Variants and promotion codes have their own write routes, so they send their own events:

  • POST /v1/products sends product.created and one product_variant.created for each variant it creates. A request with two variants sends three events, and the default variant is one of the two.
  • POST /v1/promotions sends promotion.created and one promotion_code.created for each code.
  • A variant or code write sends only its own event, even though it also advances the parent's version and updated_at and changes its rollups.
  • Archiving a product archives its variants, and archiving a promotion archives its codes. Only product.archived or promotion.archived is sent.
  • Options, bundle components, modifiers, and the groups in a modifier set have no routes of their own and no events. The parent's updated event covers them.

A field that one resource shows and another owns changes without an event for the resource that shows it. A product's price_range and variant_count change when a variant does, and categories[].name changes when a category is renamed. Redemptions and schedule boundaries send no events either. Join these fields by ID when you mirror.

Flint reads the resource when it prepares the event, so data.object can include a change made after the event's own. category.deleted carries the category as DELETE returned it, because a deleted category can no longer be read. Partner apps receive these events for an install that holds the matching read scope: commerce.products.read, commerce.bundles.read, commerce.catalog.read, or commerce.promotions.read.

Breaking: DELETE /v1/promotions/{promotion_id}/codes/{promotion_code_id} now returns the code with status: "archived". Before, it returned inactive, and the code then returned 404.

Breaking: a promotion code has a new status value, archived, with unavailable_reason: "archived". An archived code, including every code of an archived promotion, is returned by GET /v1/promotions/{promotion_id}/codes/{promotion_code_id} instead of 404. GET /v1/promotion-codes leaves archived codes out unless you pass status=archived, and its status filter now accepts active, inactive, and archived. The code filter finds an archived code only together with status=archived. Handle archived wherever you switch on a code's status.

Breaking: PATCH on an archived promotion code returns 409 ACTION_NOT_ALLOWED with current_status: archived in details[0]. Before, a deleted code returned 404.

GET /v1/modifier-groups/{modifier_group_id} and GET /v1/modifier-sets/{modifier_set_id} return an archived group or set with status: "archived" instead of 404, so a delayed event can render it. PATCH and DELETE on one still return 404.

updated_at on products, variants, bundles, modifier groups, modifier sets, promotions, and promotion codes no longer moves backward. Before, a variant write, an archive, or a promotion or code DELETE could set it earlier than a recent PATCH had, so a poll on updated_at could miss the change.

Catalog events lists what each event means, and Keep another system in sync covers mirroring and backfill.

  • API
  • Checkout
  • Orders

Read checkout branding, subscription choices, and order tax price mode#

GET /v1/checkout-sessions/{checkout_session_id} returns merchant_name, show_flint_mark, and, when set, merchant_logo, merchant_icon, and payment_link_image. These read-only fields are available with either an API key or the session's checkout credential. Create and launch results, lists, and webhooks omit them. If Flint can't load the merchant, the GET returns 500 INTERNAL_ERROR rather than incomplete branding.

Session theme and the public payment link read's checkout_theme use the read-only CheckoutTheme shape. Alongside title, primary_color, and accent_color, it includes branding settings' background_color, text_color, font_family, and corner_radius. Request theme objects and the link's own theme keep ThemeConfig, with only the title and two color overrides.

Session responses and webhooks also return tax when the session has tax settings: enabled as set at creation, and read-only location_fixed_by_merchant when the order already held a tax location. This adds session tax settings back to responses; the order's tax remains the source for its current calculation and amounts.

GET /v1/payment-links/{payment_link_id}/public adds show_flint_mark and, for subscription signup links, subscription_preview.billing_interval_options and subscription_preview.quantity_options. A successful response includes the link's Flint-Mode and Flint-Merchant-Id headers, plus Flint-Sandbox-Id for a sandbox link. Errors omit those headers. The endpoint still requires no credential.

Orders Flint renders after 2026-10-20 include read-only tax.price_mode, either additive or inclusive, with additive when no tax configuration is set. A response replayed for an idempotent retry of an earlier request omits it, so the response field is optional. Line items and charges keep their own tax.calculation.price_mode. Replacement orders created by a return also include read-only return_resolution_id; other orders omit it.

  • Breaking change
  • API
  • Credit-notes

Credit note tax and remaining value#

Credit notes now calculate from each frozen invoice line's remaining billed value, discount, and tax. Crediting one of two inclusive units with total 1200 and tax 200 returns total 600, including tax 100. A full credit returns total 1200 and tax 200. Tax is neither added twice nor changed to force an amount correction's total.

Breaking: remaining-pool rounding replaces the previous tax-first residual policy for all callers. Amount corrections reduce the remaining units' value without consuming quantity. Later quantity corrections use that reduced value, and exhausting a line consumes its original components exactly across issued, non-void notes. Public field names are unchanged. Tax-exclusive consideration remains total_money - tax_money.

Issue preserves previewed amounts. If another issued or voided note changes a fitting draft's allocation, issue returns 409 CONCURRENT_MODIFICATION with reason credit_note_creditable_value_changed and retry after_refresh. Retrieve the draft, replace its existing line_items corrections using its current expected_version, review the recalculated amounts, and issue with a new key.

New corrections, allocations, and linked refunds that would spend affected historical credit return 409 ACTION_NOT_ALLOWED with reason credit_note_tax_reconciliation_required and blocking credit-note references. Reads, issued amounts, and committed command retries preserve their stored results. Allocation reversal and eligible voids remain available for recovery; already-refunded money requires accounting reconciliation.

PDFs distinguish added Tax from informational Includes tax. Credit-note issue does not submit a Quaderno credit document or automatically reverse tax in Flint's built-in tax reports.

See Credit notes for rounding examples and recovery steps.

  • API
  • Fulfillment
  • Checkout

Fulfillment event types are complete, and checkout surfaces stay consistent#

The event_type vocabulary on fulfillment event responses now documents all 21 values. The newly documented values are accepted, preparing, picked, packed, ready, dispatched, completed, canceled, failed, and no_show. This vocabulary is extensible, so clients must accept values they do not recognize. Creating an event still accepts only carrier_scan and custom. The list filter keeps its existing values.

Checkout session lists and close or expire responses now preserve surface: "embedded" for embedded sessions and surface: "hosted" for hosted sessions. Older sessions created before surfaces were introduced report hosted, consistently with retrieving the same session. Responses no longer return surface: "unspecified"; the published values remain hosted and embedded.

  • Breaking change
  • API
  • Webhooks
  • Invoices

Invoice webhooks cover every status change, and paid invoices can reopen#

paid and credited close an invoice's balance but are not final. Reversing a manual payment, losing a dispute, or reversing a credit note allocation can return the invoice to open or partially_paid. void and uncollectible remain final, and refunds still change only refund_status. paid_at clears when an invoice leaves paid, and closed_at clears when it returns to open or partially_paid.

Two webhook events are new. Both need commerce.invoices.read:

  • invoice.reopened fires when a reversal returns a paid or credited invoice to open or partially_paid, or returns a partially_paid invoice to open. data.object is the invoice.
  • invoice_payment_attempt.settled fires once when an invoice payment attempt succeeds, including an attempt that pays only part of the balance and an ACH debit that leaves processing. data.object is the payment attempt with status: "settled" and settled_at. Manual payments do not create payment attempts and do not send it.

invoice.paid now fires whenever the balance reaches zero with collected payments applied, including when a credit note allocation or a late fee waiver clears the rest, and when a reversed payment is recovered. invoice.credited fires whenever credit clears the balance with no collected payments remaining. Each payment that leaves a balance still sends invoice.partially_paid. Manual payment reversals and lost disputes that reopen the balance now send invoice.reopened as well as their existing events.

Breaking: reversing a credit note allocation that reopens an invoice sends invoice.reopened instead of invoice.updated. Subscribe to invoice.reopened if your system treats paid or credited as final. Flint does not resend earlier events.

One change can send several events, such as invoice.manual_payment_reversed and invoice.reopened, and Flint does not guarantee the order they arrive in. See Invoicing.

  • API
  • Orders
  • Invoices

Order sort errors and tax and invoice values match responses#

The error for an empty sort_by on GET /v1/orders now lists created_at, updated_at, outstanding_money, and total_money. It no longer lists order_number. The error code remains INVALID_REQUEST.

TaxBreakdown.taxability_reason now documents all six values already returned by the API: standard_rated, not_taxable, customer_exempt, tax_disabled, location_required, and calculation_incomplete. The vocabulary remains extensible, so handle unrecognized values.

InvoiceDiscount.discount_class now documents only order and line_item, matching invoice responses. AppliedDiscount.discount_class continues to include service_charge.

  • Breaking change
  • API
  • Organizations

Organization membership changes require existing access#

POST /v1/organizations/{organization_id}/memberships sets roles only for users who already have an active membership in that organization or a parent organization. It returns 201 when it adds a direct membership and 200 when it changes one. Other user_id values return 404 RESOURCE_NOT_FOUND with param: "user_id", whether the account exists or not. The API cannot invite new people.

The grant request accepts admin, operator, and viewer. Sending owner returns 400 INVALID_REQUEST with detail code INVALID_VALUE and param: "role". Membership responses still include the owner role. Removing a direct membership returns status: "removed" instead of "revoked".

POST /v1/organizations/{organization_id}/transfer-ownership has been removed. Account ownership changes are reserved for Flint.

Organization writes return 403 FORBIDDEN with details[0].reason: "organization_role_insufficient" when the acting user's organization role doesn't allow the change, or "organization_live_mode_required" when the request uses test mode. These replace the generic not_permitted reason. Organizations outside the credential's access still return 404.

  • API
  • Payments

Filter payments that need action and send standalone receipts#

GET /v1/payment-intents accepts the optional action_required boolean filter. true returns payments that require capture or have a dispute that needs a merchant response. The dispute must have status needs_response or warning_needs_response, allow evidence responses, and have no deadline or a deadline that has not passed. false returns the remaining payments. Omit the filter to include both groups.

POST /v1/payment-intents/{payment_intent_id}/receipts queues a receipt for a succeeded payment with no order. Send email to choose a recipient, or omit it to use the payment's receipt_email, then the customer's email. The saved receipt email stays unchanged. Payments linked to an order continue to use POST /v1/orders/{order_id}/send-receipt.

The receipt action requires payments.payment_intents.write for API keys and partner tokens. Dashboard owners, admins, and operators can also send receipts. It returns the same action response as order receipt sends: data.success is true after the receipt is queued. Sending requires Flint-managed receipt delivery and is limited to once every five minutes per payment. Send an Idempotency-Key and reuse it when retrying the same send.

  • API

Required response fields include nullable values#

OpenAPI now marks a response field as required when Flint always includes its key. A required field can still be nullable: for example, a Customer response includes "tax_identity": null when no tax identity is set. Check the field's nullable schema separately from whether its key is required.

Response JSON is unchanged. Optional fields can still be omitted, and buyer response schemas continue to describe the fields available to buyers. Regenerate clients from the updated OpenAPI document to use the corrected required fields.

  • API
  • Disputes

Filter disputes that need a response and choose their order#

GET /v1/disputes accepts three new optional query parameters:

  • action_required=true returns disputes that need a merchant response now: their status is needs_response or warning_needs_response, evidence responses are allowed, and the evidence deadline has not passed. A dispute with no deadline can still need a response. false returns the remaining disputes; omitting the parameter applies no action filter.
  • sort_by accepts created_at, evidence_due_at, or amount. amount compares amount_money.amount in minor units. Disputes without an evidence deadline sort after dated disputes in either direction.
  • sort_direction accepts asc or desc. The default remains created_at descending, with ties broken by dispute_id in the same direction.

For example, GET /v1/disputes?action_required=true&sort_by=evidence_due_at&sort_direction=asc lists disputes that need a response by their evidence deadline.

GET /v1/disputes and GET /v1/disputes/{dispute_id} also accept dashboard sessions for merchant owners, admins, operators, and viewers. Dashboard requests use the selected live or sandbox environment.

  • Breaking change
  • API
  • Gift-cards

Gift card restrictions, a default funding disposition, and a customer filter#

Why a card is frozen. Every gift card now has restrictions, which is [] when nothing keeps it frozen. A merchant_freeze restriction comes from a freeze transition and carries its reason. A funding_dispute restriction names the dispute_id on a payment that funded the card, with dispute_status open or lost. unfreeze is supported only when every restriction is a merchant_freeze, and a lost dispute needs a funding disposition. You no longer need to scan the card's loads to find out. The field also appears on gift_card.* webhooks and on the card returned with loads and redemptions. Buyer reads under /v1/me/gift-cards don't include it. See Gift cards.

disposition is optional. POST /v1/gift-card-funding-dispositions defaults disposition to honor_value, its only value. Send dispute_id and reason_message. A request that sends honor_value and one that omits it are the same request, so either replays under the same Idempotency-Key.

Find a customer's cards. GET /v1/gift-cards?customer_id=cus_... returns the cards associated with that customer.

Breaking: no gift_card_id filter on the card list. GET /v1/gift-cards?gift_card_id= now returns 400 UNSUPPORTED_QUERY_PARAM. Retrieve one card with GET /v1/gift-cards/{gift_card_id}. Loads, redemptions, notifications, and transactions still filter by gift_card_id.

  • Breaking change
  • API
  • Errors

One code for records in the way and one for a taken key#

Breaking: RESOURCE_IN_USE. When other records stop a change, Flint returns 409 RESOURCE_IN_USE. details[0].blocking_resources lists up to 25 of them as resource_type and resource_id references, and details[0].blocking_resource_count gives the total. details[0].resource_type and resource_id name the record you tried to change. One response lists every kind of blocker the change checks, so resolve them all before you retry. Blockers appear only for resource types your credential can read, and the count covers only those: a key with only commerce.products.write gets the refusal and the product, but no order IDs. Grant the read scope for a blocker's type to see it. These codes are replaced:

Old codeBlockers listed
PRODUCT_IN_USE, PRODUCT_IN_OPEN_ORDER, VARIANT_IN_OPEN_ORDER, BUNDLE_IN_OPEN_ORDERorder
PRODUCT_IN_OPEN_CHECKOUT_SESSION, VARIANT_IN_OPEN_CHECKOUT_SESSION, BUNDLE_IN_OPEN_CHECKOUT_SESSIONcheckout_session
PRODUCT_IN_OPEN_INVOICE_PAYMENT_ATTEMPT, VARIANT_IN_OPEN_INVOICE_PAYMENT_ATTEMPT, BUNDLE_IN_OPEN_INVOICE_PAYMENT_ATTEMPTinvoice_payment_attempt
PRODUCT_IN_ACTIVE_PLAN, VARIANT_IN_ACTIVE_SUBSCRIPTION_PLAN, BUNDLE_IN_ACTIVE_SUBSCRIPTION_PLANsubscription_plan
PRODUCT_IN_ACTIVE_SUBSCRIPTION, VARIANT_IN_ACTIVE_SUBSCRIPTION, BUNDLE_IN_ACTIVE_SUBSCRIPTIONsubscription
VARIANT_IN_ACTIVE_PAYMENT_LINK, BUNDLE_IN_ACTIVE_PAYMENT_LINKpayment_link
VARIANT_IN_ACTIVE_BUNDLEbundle
INVENTORY_ITEM_IN_USEproduct_variant, inventory_level, inventory_reservation, inventory_transfer
INVENTORY_ALLOCATION_POLICY_IN_USEinventory_reservation
LOCATION_IN_USEinventory_allocation_policy, device, fulfillment, inventory_level, inventory_reservation, inventory_transfer, inventory_count, merchant
MODIFIER_SET_IN_USEproduct, product_variant, bundle
MODIFIER_GROUP_IN_USE, MODIFIER_IN_USEmodifier_set
CATEGORY_REFERENCEDproduct, bundle, promotion, return_policy, return_reason
LIST_IN_USErisk_rule (risk_rule_ids stays, with the same read filter)
DELIVERY_RESOURCE_HAS_DEPENDENCIESunchanged
INVOICE_PAYMENT_TERM_IN_USE, INVOICE_HAS_ISSUED_CREDIT_NOTE, CREDIT_NOTE_HAS_ALLOCATIONSunchanged
ORGANIZATION_HAS_ACTIVE_DESCENDANTS, ORGANIZATION_LINKED_TO_MERCHANTunchanged, up to 10

Changing tracked inventory on an order line while an active inventory reservation holds it also returns RESOURCE_IN_USE, listing the inventory_reservation, instead of INVENTORY_RESERVATION_EXISTS.

Breaking: ALREADY_EXISTS. When a unique key or exclusive slot is taken, Flint returns 409 ALREADY_EXISTS. param names the field, and with merchant credentials details[0].current_resource names the record that has it, as {"resource_type": "product_variant", "resource_id": "var_..."}, when your credential can read that record's type. These codes are replaced:

Old codecurrent_resource
SKU_ALREADY_EXISTS, BARCODE_ALREADY_EXISTSthe product, product_variant, or bundle with that SKU or barcode
PRODUCT_OPTION_ALREADY_EXISTS, PRODUCT_OPTION_VALUE_ALREADY_EXISTSthe product_option or product_option_value
VARIANT_OPTION_COMBINATION_ALREADY_EXISTSthe product_variant that selects the combination
CATEGORY_HANDLE_EXISTSthe category
PAYMENT_METHOD_DOMAIN_ALREADY_EXISTSthe payment_method_domain, replacing payment_method_domain_id
RISK_LIST_ALIAS_ALREADY_EXISTSthe risk_list
SANDBOX_ALREADY_EXISTSthe sandbox
INVENTORY_RESERVATION_EXISTS (the owner key already holds a reservation)the inventory_reservation

Breaking: duplicates within one request are 400. Two variants in one product create that select the same combination or share a SKU or barcode, or two options with the same name, return 400 INVALID_REQUEST with detail INVALID_VALUE and param naming the later entry, such as variants[1].selected_options, instead of 409 VARIANT_OPTION_COMBINATION_ALREADY_EXISTS or PRODUCT_OPTION_ALREADY_EXISTS. Fix the request; nothing stored conflicts.

Breaking: DANGLING_EXPANSION_REFERENCE is INTERNAL_ERROR. When an expansion finds a stored reference to a record that no longer exists, Flint returns 500 INTERNAL_ERROR. It is a Flint data failure, not a problem with your request. Retry, or read the resource without that expand value, and contact support with the request_id.

CHECKOUT_SESSION_ALREADY_EXISTS, PAYMENT_METHOD_HAS_ACTIVE_SUBSCRIPTIONS, and PROMOTION_HAS_ACTIVE_SUBSCRIPTION_OFFERS are unchanged. A response replayed for an Idempotency-Key first used before this change carries the new code without blockers or current_resource, and so does a refusal whose holder Flint can't read at that moment. See Error handling.

  • Breaking change
  • API
  • Fulfillment

Packages dispatch and complete shipments, and the buyer gets one email per moment#

A shipment fulfillment with packages now moves itself. It reaches dispatched once every unit is in an outbound package and every package has shipped, and completed once every package is delivered, in the same request as the package transition. dispatched_at is the latest package's shipped_at, and completed_at is the latest package's delivered_at. fulfillment.completed therefore fires at carrier delivery instead of at your complete, and returns become possible from then, with a delivered return window counting from that delivery. A package in exception or returned blocks the automatic complete, and a completed fulfillment is never reopened. dispatch and complete still work for shipments without packages and as overrides. See How packages move the fulfillment.

Breaking: POST /v1/fulfillments/{fulfillment_id}/events records only entries that change no status. event_type accepts carrier_scan and custom; shipped, in_transit, out_for_delivery, delivered, delivery_attempted, exception, returned, and tracking_updated return 400 INVALID_REQUEST with param: event_type. Send the matching package transition instead. The request no longer takes buyer_notification_behavior, and the response no longer has fulfillment_notifications, because these entries never email the buyer. carrier_scan is a new event_type value on fulfillment events.

Breaking: once a shipment fulfillment has a package, PATCH /v1/fulfillments/{fulfillment_id} returns 409 ACTION_NOT_ALLOWED with param: completed_at and details[0].reason: derived_from_packages when it sets completed_at, because the packages set it.

Breaking: the buyer gets one email per moment. Packages send shipped, out for delivery, delivery attempted, delivered, exception, and returned, once each per package. When packages move the fulfillment, they send nothing more, and a dispatch or complete you send stays silent once a package sent that moment. Without packages, a shipment's dispatch sends the shipped email and complete the delivered email. In transit scans and tracking changes no longer email, so buyer_notification_behavior is removed from POST /v1/shipments/{shipment_id}/packages, PATCH /v1/packages/{package_id}, and shipment.package on POST /v1/orders/{order_id}/fulfillments. Transitions and voids keep it. See What the buyer receives.

  • API
  • Subscriptions
  • Payment-methods
  • Errors

Retry PAYMENT_METHOD_NOT_READY with the same Idempotency-Key#

Same key, once the card is ready. Changing a subscription's card with a card that is still pending returns 409 PAYMENT_METHOD_NOT_READY and changes nothing. That refusal now releases the request's Idempotency-Key, and its remediation.retry is same_request. Send the same request with the same key once payment_method.saved arrives, and it attaches the card. Before, a retry with that key returned 409 IDEMPOTENCY_KEY_IN_PROGRESS until the key's lease expired, so each attempt needed a new key. This applies to POST /v1/subscriptions/{subscription_id}/payment-method, POST /v1/me/subscriptions/{subscription_id}/payment-method, and POST /v1/subscriptions.

Other card refusals replay. On the card change routes, RESOURCE_NOT_FOUND for the card, PAYMENT_METHOD_CUSTOMER_MISMATCH, PAYMENT_METHOD_NOT_ACTIVE, and PAYMENT_METHOD_ON_SESSION_ONLY can't change by asking again, so the same key returns the same refusal. Use a new key for a different card. See Saved cards.

  • API
  • Orders
  • Payments

Payments made with your API key take Apple Pay and Google Pay#

Payment intents you create with your API key, and payments on orders you create with it, now accept Apple Pay and Google Pay. Before, a wallet on these payments returned 400 PAYMENT_OPTION_NOT_ALLOWED ("apple_pay is not supported for api payments"). Orders from payment links and Flint-hosted checkout already took wallets.

  • A wallet ConfirmationToken or a saved wallet card works with your API key on POST /v1/orders/{order_id}/pay and POST /v1/payment-intents/{payment_intent_id}/confirm, including an off-session charge of a saved wallet card.
  • apple_pay and google_pay are accepted in payment_options on POST /v1/payment-intents and POST /v1/orders/{order_id}/payment-intents.
  • Wallets on these payments have the same limits as cards: up to $100,000 per payment, or $250,000 with an approved higher limit. A limit you set in the dashboard for payments made with your API key also applies to wallets.

A one-shot POST /v1/orders/{order_id}/pay sends no list of payment options, so Flint starts from your checkout settings. The payment intent it creates now leaves out the options this payment can't use, and its payment_options lists the rest:

  • Affirm, when the request has no checkout session. Affirm needs a page to send the buyer back to, and on this route only a checkout session provides one.
  • Options outside your payment limits for the amount, such as ACH debit on a payment below its minimum.

Before, any of these refused the whole payment, even one made with a card. For example, with Affirm turned on and wallets off, a card payment between $50 and $30,000 returned 400 FIELD_REQUIRED for payment_return_url, and an ACH debit maximum on your account refused a larger card payment with AMOUNT_EXCEEDS_LIMIT. When an option you request yourself, or the credential you pay with, is outside a limit, Flint still refuses it with the same error.

  • Breaking change
  • API

Create API keys from a scope preset#

POST /v1/api-keys, POST /v1/developer/sandboxes with issue_test_key: true, and POST /v1/developer/sandboxes/{sandbox_id}/test-key accept scope_preset in place of scopes. full_access grants every scope a key in the new key's mode can hold that the caller can grant; a key grants only scopes it holds. read_only grants the read scopes of that set. Flint resolves the preset when it creates the key and stores the result as the key's scopes, so scopes added later aren't added to the key. {"name": "ci", "scope_preset": "full_access"} is a complete request. See Scope presets.

Breaking: each of these routes takes exactly one of scopes or scope_preset. Sending neither returns 400 INVALID_REQUEST with detail REQUIRED and param: scopes, including from an onboarding session, which no longer gets a default scope set on the sandbox routes. Sending both returns detail NOT_ALLOWED with both fields in conflicting_fields. POST /v1/developer/sandboxes without issue_test_key: true now rejects scopes, scope_preset, and test_key_name with detail NOT_ALLOWED instead of ignoring them. The sandbox routes' scopes items now publish the scope enum.

Breaking: the POST /v1/developer/sandboxes response no longer has a top-level secret_key. The issued test key at data.api_key has the same shape as POST /v1/api-keys data, so read the secret from data.api_key.secret_key.

  • Breaking change
  • API

Products and bundles reference categories by handle only#

Breaking: POST /v1/products, PATCH /v1/products/{product_id}, POST /v1/bundles, and PATCH /v1/bundles/{bundle_id} take category_handles instead of categories. Each value must exactly match the handle of a category that already exists. Flint no longer matches category names, normalizes the value, or creates a category from a product or bundle write. Create categories with POST /v1/categories first.

A handle that matches no category returns 404 RESOURCE_NOT_FOUND with param naming the entry, such as category_handles[2], and details[0].resource_type set to category. Nothing is saved. A handle sent twice returns 400 INVALID_REQUEST. The INVALID_CATEGORY and AMBIGUOUS_CATEGORY codes are removed.

Sending category_handles on a product or bundle update replaces the categories without expected_version. If you send expected_version, Flint still checks it. Responses still return categories as {category_id, handle, name} objects.

PATCH /v1/categories/{category_id} accepts metadata: null to remove all of your metadata keys. Concurrent category updates are applied one after the other, so the CATEGORY_MODIFIED code is removed.

  • Breaking change
  • API

Product and promotion create return their variants and codes#

POST /v1/products returns variants: every variant the request created, ordered by position and then variant_id, in the same shape GET /v1/products/{product_id}/variants/{variant_id} returns. A simple product returns its default variant there. You no longer need to list the variants and match them back by SKU to learn their IDs. Other product responses leave variants out; GET /v1/products/{product_id}/variants lists them.

POST /v1/promotions returns codes: the codes the request created, in request order, each with its promotion_code_id. An automatic promotion returns codes: []. Other promotion responses leave codes out; GET /v1/promotion-codes?promotion_id= lists them.

Breaking: product create accepts at most 100 variants, and promotion create accepts at most 100 codes. More returns 400 INVALID_REQUEST with param set to variants or codes. Add more variants with POST /v1/products/{product_id}/variants and more codes with POST /v1/promotions/{promotion_id}/codes.

  • Breaking change
  • API
  • Customers
  • Errors

Customers without an email, create-or-return by email, and null for empty fields#

Email is optional. POST /v1/customers creates a customer without email, for example a buyer at the register who doesn't give one. Any number of customers can have no email. Customer.email and the expanded customer on orders, payments, invoices, and other resources are null for them.

Create or return. Send on_email_conflict: "return_existing" with email, and Flint returns the customer that already has that email, unchanged, with 200. When none does, Flint creates the customer and returns 201. The default, error, keeps today's conflict. return_existing without email returns 400 INVALID_REQUEST with detail REQUIRED and param email. Two requests racing for one new email both get the same customer. A replay with the same Idempotency-Key returns the original status and body. See Headless storefront.

Duplicate emails return ALREADY_EXISTS. 409 CUSTOMER_EMAIL_ALREADY_USED is now 409 ALREADY_EXISTS with param email, on POST /v1/customers, PATCH /v1/customers/{customer_id}, and /v1/me/email-change-requests. With merchant credentials, details[0].current_resource names the customer that has the email, {"resource_type": "customer", "resource_id": "cus_..."}, replacing blocking_resources and blocking_resource_count. Buyer credentials don't receive the other customer.

Merchant email changes. When your store's customer_account.mode is merchant_hosted, PATCH /v1/customers/{customer_id} accepts email and null clears it. The change keeps the buyer's customer sessions, cancels an email change the buyer had started, and sends customer.updated. With a Flint-hosted account (flint_hosted, the default), the email is how the buyer signs in, so a different email or null returns 409 CUSTOMER_EMAIL_BUYER_MANAGED; the buyer still changes it through /v1/me/email-change-requests. See Customers.

Empty fields are null. Customer responses and customer.* webhooks always include name, email, phone, internal_note, external_reference_id, and segment, with null when there's no value. Before, a cleared or never-set field was omitted. Customer sessions reading /v1/me still don't receive internal_note, external_reference_id, or segment. The expanded customer summary always includes name, email, and phone, with null when empty.

Gift card purchases need the buyer's email. Buying a gift card with a payment whose customer has no email returns 409 GIFT_CARD_BUYER_EMAIL_REQUIRED instead of a verification error. Gift card purchase limits are tracked by the buyer's email.

  • Breaking change
  • API
  • Errors

Stale versions return VERSION_CONFLICT, and status refusals return ACTION_NOT_ALLOWED#

Correction: This entry originally said the change covered every endpoint. The October 18 rollout covered the routes whose retired codes are listed below. It did not cover products, variants, bundles, subscriptions, subscription plans, risk rules, orders, order line items, or delivery selection fences. It also did not add the promised current_api_version fact to API version pin conflicts.

Breaking: on the routes listed below, a stale expected_version, or another stale expected_* value, returns 409 VERSION_CONFLICT. param names the field you sent, and details[0] carries resource_type, resource_id, and current_version when known. Read the resource and retry with its current version. These codes are removed:

  • SETTINGS_CONCURRENT_MODIFICATION, INVOICE_DRAFT_CHANGED, CREDIT_NOTE_CHANGED, MERCHANT_CHANGED, MODIFIER_GROUP_CHANGED, MODIFIER_SET_VERSION_CONFLICT, PAYMENT_LINK_CHANGED, GIFT_CARD_VERSION_CONFLICT, SUBSCRIPTION_OFFER_VERSION_CONFLICT, API_VERSION_CHANGED, and LOCATION_CHANGED.
  • INVENTORY_CHANGED, INVENTORY_COUNT_CHANGED, and INVENTORY_TRANSFER_CHANGED. Inventory no longer picks a code from the URL.

When a write loses internal contention before any effects begin and your basis is still current, it returns 409 CONCURRENT_MODIFICATION. This includes requests without a fence and requests whose fence still matches. The failed request applied no changes. Retry the same request with the same Idempotency-Key.

A few writes can lose that race after part of the change has started, so retrying the same request isn't safe for them. They keep their codes without param when you sent no expected_version, and return VERSION_CONFLICT when you did: FULFILLMENT_CHANGED, SHIPMENT_CHANGED, PACKAGE_CHANGED, RETURN_VERSION_CONFLICT, and DELIVERY_RESOURCE_VERSION_CONFLICT. Re-read the resource and retry.

A missing expected_version where a write requires one returns 400 INVALID_REQUEST with details[0].code REQUIRED, and one that isn't a positive integer returns INVALID_VALUE, both with param expected_version. ORDER_REVISION_REQUIRED, INVALID_EXPECTED_VERSION, and DELIVERY_EXPECTED_VERSION_INVALID are removed. This replaces the October 16 note that kept ORDER_REVISION_REQUIRED.

A refusal because of the resource's status returns 409 ACTION_NOT_ALLOWED, with resource_type, resource_id, current_status, and supported_actions when the resource publishes them:

  • Inventory transfers, counts, and reservations. INVENTORY_TRANSFER_ACTION_NOT_ALLOWED and INVENTORY_RESERVATION_CLOSED are removed; a closed reservation keeps closed_reason. Committing a reservation after owner.expires_at returns ACTION_NOT_ALLOWED with details[0].reason expired instead of INVENTORY_RESERVATION_EXPIRED.
  • Changing an archived product or subscription plan, with current_status: archived. PRODUCT_ARCHIVED and PLAN_ARCHIVED are removed.

Also:

  • Applying an inventory count after stock at a counted level moved returns 409 INVENTORY_COUNT_PHYSICAL_REVISION_CHANGED as the top-level code, the code its details already used.
  • An unknown category handle on a promotion rule, return policy, or return reason returns 404 RESOURCE_NOT_FOUND with param naming the entry and details[0].resource_type category, like product and bundle writes. UNKNOWN_CATEGORY_HANDLE is removed.
  • A Location that is still in use returns LOCATION_IN_USE in details[0].code as well as the top-level code. The LOCATION_CONFLICT detail code is removed.
  • When Flint can't authenticate checkout session credentials because of a server problem, the request returns 503 SERVICE_UNAVAILABLE with remediation.retry same_request instead of 401 CHECKOUT_SESSION_AUTH_UNAVAILABLE.

See Error handling.

  • Breaking change
  • API

Inventory adjustments say what happened#

Breaking: adjustment lines no longer take signed changes. on_hand_quantity_delta, quality_control_quantity_delta, damaged_quantity_delta, and quarantined_quantity_delta are removed from the request, and reason now decides what each line carries:

  • received and returned add quantity to on hand; sold, theft, and loss remove it. An optional condition (sellable by default, quality_control, damaged, or quarantined) says where the units land or come from.
  • condition_changed moves quantity from from_condition (sellable by default) to to_condition and leaves on hand unchanged.
  • counted and correction set any of on_hand_quantity, quality_control_quantity, damaged_quantity, and quarantined_quantity to the values you send and leave the others unchanged. correction requires note.

Breaking: the reasons received_stock, damage, manual_correction, and other are removed, from the request, InventoryAdjustment.reason, the reason list filter, and movement reasons. Use received for received_stock, condition_changed with to_condition: "damaged" (or loss with condition: "damaged") for damage, and correction for manual_correction and other.

New: sold records a sale made outside Flint in one call. It doesn't check availability, so selling into claimed stock creates a shortage and flags the affected reservations; only a sale that would take stock below zero returns 409 INVENTORY_INSUFFICIENT.

New: a counted or correction line accepts expected_physical_version (0 for a level that doesn't exist yet), which returns 409 VERSION_CONFLICT with current_physical_version when on-hand or a condition changed since you read the level. Holds and commitments don't change it. It also accepts source_observation_sequence, which returns 409 INVENTORY_SOURCE_SEQUENCE_CHANGED and applies nothing unless the value is newer than the last one accepted for that source, item, and Location. Counts from the same source share the sequence.

Breaking: InventoryAdjustmentLine.inventory_movement_id can be null. A counted or correction line that already matches the level writes no movement; the level still appears in resulting_inventory_levels. Each line now always includes its four *_quantity_delta fields, including zeros, so the change a line made is readable without a default.

An adjustment sent without source_system now records source_system.type: "manual", as reservation transitions and safety stock changes already do.

  • Breaking change
  • API

One PATCH for Locations, with a flat allocation status#

Breaking: PATCH /v1/locations/{location_id}/geography and PATCH /v1/locations/{location_id}/inventory are removed. Send address, timezone, coordinate, coordinate_source, and inventory_allocation_status to PATCH /v1/locations/{location_id}, alone or together with name, status, metadata, and external_reference_id. expected_geography_revision and expected_inventory_revision are gone; the optional expected_version covers every field. A Location's version now advances on every change, including geography and allocation.

address is still replaced as a whole. A request that sends address must also send coordinate, as the new coordinate or null to clear it; without it the request returns 400 INVALID_REQUEST with detail REQUIRED and param: coordinate. An address change no longer clears an omitted coordinate. coordinate_source is sent with a coordinate and cleared with it. geography_version stays on the Location as a read-only value that advances only when the geography changes.

Breaking: the Location inventory object, with its allocation_status and inventory_version, is replaced by inventory_allocation_status (active or inactive) on every Location response. It is returned with merchants.locations.read alone, and the inventory_allocation_status list filter no longer needs commerce.inventory.read. On POST /v1/locations, send inventory_allocation_status instead of the inventory block; it defaults to inactive. Writing it on create or PATCH requires commerce.inventory_locations.write.

Breaking: every Location can hold stock. Adjustments, counts, transfers, and Return restocks no longer return 409 INVENTORY_LOCATION_NOT_CONFIGURED; that code, LOCATION_GEOGRAPHY_CHANGED, and LOCATION_INVENTORY_CHANGED are removed. inventory_allocation_status still decides whether reservations, allocation policies, order holds, and pickup use the Location's stock. Routing no longer reports the inventory_not_configured eligibility reason, and an active allocation no longer blocks setting a Location to inactive.

  • Breaking change
  • API

Mode and sandbox_id name the environment everywhere#

The API now names where a request runs with mode (test or live) and, in test mode only, sandbox_id.

Breaking: responses no longer carry Flint-Merchant-Environment-Id. Read Flint-Mode, and Flint-Sandbox-Id in test mode, which every test-mode response now carries, including public checkout, payment link, invoice, and gift card reads. A CLI OAuth request that sends a Flint-Sandbox-Id header that disagrees with the token's sandbox returns 403 FORBIDDEN with details[0].reason oauth_context_mismatch and param Flint-Sandbox-Id.

Breaking: GET /v1/oauth/authorize takes sandbox_id instead of environment_id, and GET /v1/oauth/authorize/preview accepts it too. Send it only with mode=test; omit it to install into the merchant's default sandbox. With mode=live it returns invalid_request. PartnerEnvironmentGrant.environment_id becomes sandbox_id, present only on test grants, on the install routes and in partner_app_install.* events. Installed-merchant partner events carry source.sandbox_id in test mode instead of source.environment_id.

Breaking: partner webhook endpoints drop mode (test, live, or both). An endpoint whose event_sources includes installed_merchants sends installed_merchant_modes, ["test"], ["live"], or ["test", "live"]; it is required there and returns 400 INVALID_REQUEST with detail NOT_ALLOWED on any other endpoint. Lifecycle events from partner_app arrive in both modes, so one endpoint can now carry lifecycle events and live installed-merchant events together.

Breaking: POST /v1/api-keys no longer takes sandbox_id. A test key mints a test key in its own sandbox, and a live key mints a live key. SANDBOX_ID_NOT_ALLOWED is removed. To mint a test key with a live key, use POST /v1/developer/sandboxes/{sandbox_id}/test-key.

Breaking: POST /v1/oauth/device/authorize and POST /v1/oauth/device/reauthorize take mode (test or live, default test) instead of environment.

Breaking: ResourceTimeline and its entries drop environment_id. The timeline belongs to the environment of the key that reads it.

An onboarding request whose partner install token names a different sandbox now returns 403 FORBIDDEN with details[0].reason sandbox_selection_forbidden, the same reason as other credentials, instead of environment_selection_forbidden.

  • Breaking change
  • API
  • Promotions

Promotions and codes report availability apart from status#

Breaking: a promotion's status is now active, inactive, or archived. It no longer reads expired, not_yet_started, exhausted, or no_active_codes. Those states move to two new fields on every promotion:

  • available is true when the promotion can apply right now: it is active, inside its schedule, under its max_uses, and, when code-gated, has a code a buyer can redeem.
  • unavailable_reason says why when available is false: inactive, archived, not_yet_started, expired, exhausted, or no_active_codes.

So a promotion you set to active with a future schedule.starts_at reads status: active, available: false, and unavailable_reason: not_yet_started.

Breaking: a promotion code's status is now active or inactive. It no longer reads expired or exhausted. Codes gain available and unavailable_reason too, and a code's availability now includes its promotion: an active code under an inactive, ended, or archived promotion reads available: false with unavailable_reason: promotion_unavailable. The code's own inactive, expired, or exhausted comes first.

Breaking: the status filter on GET /v1/promotions takes active, inactive, and archived, and on GET /v1/promotion-codes it takes active or inactive. Both lists take a new available=true|false filter. To list promotions that can apply now, use GET /v1/promotions?available=true instead of status=active.

Breaking: the promotion decline reason disabled is renamed inactive, in PROMOTION_DECLINED errors, meta.warnings[].reason, and discount preview candidates.

Errors that report a promotion's or code's details[0].current_status, such as PROMOTION_DECLINED and PROMOTION_NOT_REDEEMABLE, now carry its status value. SUBSCRIPTION_OFFER_PROMOTION_UNAVAILABLE adds the promotion's unavailable_reason as details[0].reason.

Promotion status has the details.

  • Breaking change
  • API

A Return lists its children by ID and expands them in one read#

Breaking: receipt_count, inspection_count, disposition_count, resolution_count, and shipment_count are removed from the Return, and resolution_count from each Return line. In their place, every Return response carries return_receipt_ids, return_inspection_ids, return_disposition_ids, return_resolution_ids, and shipment_ids, oldest first, and each line carries return_resolution_ids. An array keeps every child in every status, so its length is the count the removed field reported. A /v1/me read carries only return_resolution_ids.

GET /v1/returns/{return_id} now expands return_receipts, return_inspections, return_dispositions, return_resolutions, and shipments, each in the order of its ID array. Each expansion returns up to 100 objects; past that the read returns 400 INVALID_REQUEST with detail INVALID_VALUE and param: "expand", and you page the collection from its list route with return_id. Expanding shipments also needs commerce.orders.read. See Read a whole Return.

An expanded shipment summary, on Returns and on fulfillments, now includes return_id when the shipment sends a Return's items back.

  • Breaking change
  • API
  • Subscriptions

Subscriptions show next_billing_at only while a renewal will run#

Breaking: next_billing_at on a subscription is now present only while a trialing or active subscription has a billing date and isn't set to cancel at the end of its period. A canceled, paused, past_due, or incomplete subscription, a scheduled start, and one set to cancel have none, even when they had a date before. Read scheduled_resume_at for a pause with a length, next_retry_at for a payment retry, starts_at for a scheduled start, and current_period_end_at for when a canceling subscription ends. The same applies to subscriptions in webhook payloads, on /v1/me, and in expansions.

Breaking: next_billing_after and next_billing_before match only subscriptions that have a next_billing_at. With sort_by=next_billing_at, subscriptions without one sort last in both directions; ascending used to list them first.

A past_due subscription reports missed_billing_at, the billing date whose payment failed. When it's before current_period_end_at, the first paid period was never paid, and canceling ends the subscription right away whatever cancel_immediately says.

billing_interval, billing_interval_count, recurring_amount_money, and line_items are always present on a subscription, including the response to POST /v1/subscriptions. line_items is [] when there are none.

Buyers reading with a customer session now receive starts_at and awaiting_billing_schedule.

Subscriptions has the details.

  • Breaking change
  • API
  • Subscriptions

Stop selling a subscription plan without ending its subscriptions#

A subscription plan's status is now active, inactive, or archived. Send status on POST /v1/subscription-plans and PATCH /v1/subscription-plans/{subscription_plan_id} to set active (the default on create) or inactive. Only DELETE archives a plan; sending archived returns 400 INVALID_REQUEST with detail INVALID_VALUE.

An inactive plan takes no new subscribers. Creating or previewing a subscription on it, creating a checkout session or payment link for it, and paying an order that signs a buyer up for it return 409 PLAN_NOT_ACTIVE with current_status: "inactive". Its current subscriptions keep renewing, the plan stays editable, and setting it back to active takes signups again. To change a plan's price, create a new plan and set the old one to inactive.

Setting a plan to inactive is refused with 409 RESOURCE_IN_USE while an active payment link sells it. Subscriptions and open checkout sessions don't block it; a buyer already in checkout for the plan gets 409 PLAN_NOT_ACTIVE when they pay. GET /v1/subscription-plans returns active and inactive plans by default, and status=inactive lists only inactive ones.

Breaking: DELETE /v1/subscription-plans/{subscription_plan_id} archives an active or inactive plan. When subscriptions, active payment links, or open checkout sessions remain, it returns 409 RESOURCE_IN_USE instead of PLAN_HAS_ACTIVE_SUBSCRIPTIONS, PLAN_HAS_ACTIVE_PAYMENT_LINKS, or PLAN_HAS_OPEN_CHECKOUT_SESSIONS. details[0].blocking_resources lists up to 25 subscription, payment_link, and checkout_session references, and blocking_resource_count gives the total.

Breaking: status on subscription plans and on expanded plan summaries can be inactive. Handle the new value wherever you branch on plan status.

A product, variant, or bundle on an inactive plan can't be deleted or archived, the same as one on an active plan, because the plan can be activated again.

Stop selling a plan has the details.

  • API

New in the API: week of October 12, 2026#

408 additions across Inventory, Gift cards, and 41 more areas. These are additions, so nothing you already use needs to change.

Related: Payments made with your API key take Apple Pay and Google Pay, Create API keys from a scope preset, Products and bundles reference categories by handle only, Product and promotion create return their variants and codes, Customers without an email, create-or-return by email, and null for empty fields, Stale versions return VERSION_CONFLICT, and status refusals return ACTION_NOT_ALLOWED, Inventory adjustments say what happened, One PATCH for Locations, with a flat allocation status, Mode and sandbox_id name the environment everywhere, Promotions and codes report availability apart from status, A Return lists its children by ID and expands them in one read, Subscriptions show next_billing_at only while a renewal will run, Stop selling a subscription plan without ending its subscriptions, Pay an order with your API key while its checkout session is open, Lists include archived records only when status names them, Checkout sessions take tax from the order, Count observations keep the conditions you didn't count, Not-found, authentication, and server errors use generic codes, One shape for references that can point at more than one resource type, Routes that move money or stock require an Idempotency-Key, and other writes never do, Inventory reservations need only a key, an item, and a quantity, Create and issue an invoice in one call, and sell tracked stock with quick pay, List time ranges exclude their _before bound, and sort values name response fields, Declined payments return 2xx with last_payment_error, and 402 is gone, List a payout's balance transactions with payout_id, Balance transactions add up to your balance, and payouts in progress show only in held_money, Flint chooses the balance source for a payout, Every report covers up to 366 days, and the report list filters by type and status, Variants select options by name, Webhooks and delivery rate callbacks are signed only with Standard Webhooks, Checkout sessions stay open when you edit the order, Validation errors use INVALID_REQUEST, and enum values match exactly, Consistent names for versions, references, and checkout settings, Clear fields with null; empty strings are rejected, Webhook events carry their resource in data.object, and event names follow one rule, Buyer lists accept only buyer-visible filters, and rejected customer sessions return 401, Catalog updates handle null consistently and reject changes to archived products, Duplicate customer emails name the existing customer, Customer session secrets end with their refresh token, Archived devices report archived and release their fingerprint, Dispute field names, review refunds, and risk previews, Effective settings include promotion defaults, Fulfillment command results, external references, and delivery configuration lists, Gift card command responses, funding limit errors, and customer null handling, Inventory errors name the field, and every inventory resource reads by ID, Location errors name the field, and country codes must be uppercase, Manual discount scope must match discount_class, Analytics stays in one currency, and payouts and balances match their webhooks, Order field renames, revision checks, and null handling, Payment intent, payment method, and refund request fixes, Payment links keep opening after you turn promotions off, Promotion updates change only the subfields you send, Promotion removal warnings reflect the returned order, Returns and credit note errors, fences, and a preview rename, Archived promotions use the disabled decline reason, Catalog, checkout, subscription, and account state conflicts return 409, A charge's tax category decides its automatic tax, Merchant credentials can apply code-gated promotions by ID, Discount errors name the field you sent, Exhausted promotions are removed before payment, Idempotency replays include metadata and keys come from callers, Invoice installments carry the tip first, Invoice lists use cursor page tokens, Order create resolves requested promotions together, Orders report the requested tip only in tips, Order totals failures return 500 ORDER_TOTALS_UNAVAILABLE, Order, delivery, and promotion state conflicts return 409, Payments in progress return 409 PAYMENT_IN_PROGRESS, Creating a payment intent returns it flat in data, Payment intent tips are validated, payment_intent events carry the full payment intent, Payment, refund, and invoice state conflicts return 409, Promotions without eligibility rules omit `eligibility_rules`, Promotion write and discount removal errors name the field you sent, Promotion declines use 409 for promotion state, and displaced promotions are reported, New promotion decline reason `superseded_by_higher_priority`, Promotion rules reject unknown enum values, Percent promotions apply to the remaining balance, Promotion updates keep omitted combines_with classes, Refund amounts default to the sum of named targets, Repeated query parameters are validated, Request body field validation, Refunds, wallet cards, and gift card refund allocations use consistent response fields, Subscription payments completed after cancellation are refunded, Timestamps carry whole seconds, Partly paid orders keep their unpaid tip payable, Delivery rate callbacks receive the quote's expiry, Delivery rate callback URL errors use INVALID_URL and INVALID_URL_HOST, Checkout sessions reject an empty delivery method list, Delivery errors name the field to fix, Delivery methods no longer return offer_windows, Dimensions use millimeter, centimeter, or inch, Location sets accept stores without inventory allocation, Pickup search distances use meter, kilometer, and mile, State conditions recognize military addresses and can't be passed with an unknown state

Inventory
27 additions
Gift cards
10 additions
Checkout sessions
12 additions
Promotions
6 additions
Delivery quotes
3 additions
Orders
37 additions
  • Query parameter
    expected_versionon DELETE /v1/orders/{order_id}/charges/{order_charge_id}

    Order version from the last order read.

  • Query parameter
    expected_versionon DELETE /v1/orders/{order_id}/gift-cards/{gift_card_id}

    Order version from the last order read.

  • Query parameter
    expected_versionon DELETE /v1/orders/{order_id}/line-items/{order_line_item_id}

    Order version from the last order read.

  • Query parameter
    payment_link_idon GET /v1/orders

    Filter by the payment link whose checkout created the order.

  • Query parameter
    activity_typeon GET /v1/orders/{order_id}/activities

    Filter by activity type.

  • Request field
    expected_versionon PATCH /v1/orders/{order_id}

    Order version from the last order read.

  • Request field
    expected_versionon PATCH /v1/orders/{order_id}/charges/{order_charge_id}

    Optional order version from the last order read.

  • Request field
    expected_versionon POST /v1/orders/{order_id}/charges

    Optional order version from the last order read.

Subscriptions
31 additions
Returns
25 additions
Customer account
21 additions
  • Query parameter
    due_afteron GET /v1/me/invoices

    Inclusive lower bound on due_at.

  • Query parameter
    due_beforeon GET /v1/me/invoices

    Exclusive upper bound on due_at.

  • Query parameter
    has_outstanding_balanceon GET /v1/me/invoices

    Filter by whether outstanding_money is greater than zero.

  • Query parameter
    activity_typeon GET /v1/me/orders/{order_id}/activities

    Filter by activity type.

  • Query parameter
    next_billing_afteron GET /v1/me/subscriptions

    Inclusive lower bound on next_billing_at.

  • Query parameter
    next_billing_beforeon GET /v1/me/subscriptions

    Exclusive upper bound on next_billing_at.

  • Request field
    expected_versionon POST /v1/me/subscriptions/{subscription_id}/cancel

    integer

  • Request field
    expected_versionon POST /v1/me/subscriptions/{subscription_id}/pause

    integer

Payment links
13 additions
  • Query parameter
    has_subscription_planon GET /v1/payment-links

    Filter by whether the payment link references a subscription plan.

  • Query parameter
    updated_afteron GET /v1/payment-links

    Inclusive lower bound on updated_at.

  • Query parameter
    updated_beforeon GET /v1/payment-links

    Exclusive upper bound on updated_at.

  • Request field
    sales_end_aton PATCH /v1/payment-links/{payment_link_id}

    RFC3339 timestamp after which the link stops starting checkouts and resolve returns PAYMENT_LINK_SALES_ENDED.

  • Request field
    sales_end_aton POST /v1/payment-links

    RFC3339 timestamp after which the link stops starting checkouts and resolve returns PAYMENT_LINK_SALES_ENDED.

  • Field

    RFC3339 timestamp after which the link stops starting checkouts and resolve returns PAYMENT_LINK_SALES_ENDED.

  • Field

    RFC3339 timestamp after which the link stops starting checkouts and resolve returns PAYMENT_LINK_SALES_ENDED.

  • Field

    True when the link's sales end has passed: payment_link.sales_end_at, or event_config.event_at for an event link without sales_end_at.

Customers
12 additions
  • Request field
    is_merchant_verifiedon PATCH /v1/customers/{customer_id}

    Flag you set on the customer, for example after checking ID in store.

  • Request field
    segmenton PATCH /v1/customers/{customer_id}

    Free-form label you assign, such as wholesale, so promotion and delivery rules can match groups of customers.

  • Request field
    is_merchant_verifiedon POST /v1/customers

    Flag you set on the customer, for example after checking ID in store.

  • Request field
    segmenton POST /v1/customers

    Free-form label you assign, such as wholesale, so promotion and delivery rules can match groups of customers.

  • Field

    Flag you set on the customer, for example after checking ID in store.

  • Field

    Free-form label you assign, such as wholesale, so promotion and delivery rules can match groups of customers.

  • Values
  • Error code

    The deletion request's status does not allow this decision.

Delivery revocations
12 additions
Fulfillment records
12 additions
  • Query parameter
    expandon PATCH /v1/fulfillments/{fulfillment_id}

    Supported expansions: order, packages, shipments.

  • Request field
    occurred_aton POST /v1/fulfillments/{fulfillment_id}/transitions

    RFC3339 timestamp.

  • Request field
    occurred_aton POST /v1/fulfillments/{fulfillment_id}/transitions

    RFC3339 timestamp.

  • Request field
    reason_messageon POST /v1/fulfillments/{fulfillment_id}/transitions

    Your note explaining this transition.

  • Request field
    reason_messageon POST /v1/fulfillments/{fulfillment_id}/transitions

    Your note explaining this transition.

  • Value
  • Event

    A materialized order fulfillment completed all non-canceled obligated quantity.

  • Event

    A committed fulfillment execution leg materialized as an order fulfillment.

Invoices
10 additions
  • Query parameter
    due_afteron GET /v1/invoices

    Inclusive lower bound on due_at.

  • Query parameter
    due_beforeon GET /v1/invoices

    Exclusive upper bound on due_at.

  • Query parameter
    has_outstanding_balanceon GET /v1/invoices

    Filter by whether outstanding_money is greater than zero.

  • Query parameter
    activity_typeon GET /v1/invoices/{invoice_id}/activities

    Filter by activity_type.

  • Request field
    inventory_routing_sourceon POST /v1/invoices

    Where this invoice backing order's tracked demand is routed: a fixed Location, or an allocation policy.

  • Request field
    issueon POST /v1/invoices

    Issue the invoice immediately after creating it.

  • Field

    Version of the invoice payment term captured in this snapshot.

  • Value
Products
10 additions
  • Request field
    category_handleson PATCH /v1/products/{product_id}

    Relationship set of category handles that must already exist.

  • Request field
    external_reference_idon PATCH /v1/products/{product_id}/variants/{variant_id}

    Caller-owned identifier for this resource in an external system.

  • Request field
    selected_optionson PATCH /v1/products/{product_id}/variants/{variant_id}

    Option name and value selections for this variant.

  • Request field
    category_handleson POST /v1/products

    Relationship set of category handles that must already exist.

  • Request field
    option_nameon POST /v1/products

    Name of one of the product's options.

  • Request field
    selected_optionson POST /v1/products

    Option name and value selections for this variant.

  • Request field
    valueon POST /v1/products

    Value of the named option.

  • Field

    The variants this request created, ordered by `position` and then `variant_id`.

Webhooks
9 additions
  • Field

    Last event sent before a backpressure disconnect.

  • Values

    credit_note_allocation.createdcredit_note_allocation.reversedcustomer_deletion_request.completedcustomer_deletion_request.createdcustomer_deletion_request.rejectedfulfillment_event.created

  • Values

    credit_note_allocation.createdcredit_note_allocation.reversedcustomer_deletion_request.completedcustomer_deletion_request.createdcustomer_deletion_request.rejectedenvironment_grant.created

  • Values

    credit_notecredit_note_allocationcustom_domaincustomer_deletion_requestdelivery_location_setdelivery_method

  • Values

    credit_note_allocation.createdcredit_note_allocation.reversedcustomer_deletion_request.completedcustomer_deletion_request.createdcustomer_deletion_request.rejectedfulfillment_event.created

  • Values

    credit_note_allocation.createdcredit_note_allocation.reversedcustomer_deletion_request.completedcustomer_deletion_request.createdcustomer_deletion_request.rejectedenvironment_grant.created

  • Values

    credit_note_allocation.createdcredit_note_allocation.reversedcustomer_deletion_request.completedcustomer_deletion_request.createdcustomer_deletion_request.rejectedenvironment_grant.created

  • Value
Money movement
7 additions
Settings
7 additions
Sandboxes
6 additions
  • Request field
    scope_preseton POST /v1/developer/sandboxes

    A predefined set of scopes to grant instead of listing them.

  • Request field
    scope_preseton POST /v1/developer/sandboxes/{sandbox_id}/test-key

    A predefined set of scopes to grant instead of listing them.

  • Value
  • Values

    accounts.api_keys.readaccounts.api_keys.writeaccounts.devices.readaccounts.devices.writeaccounts.organizations.readaccounts.organizations.write

  • Values

    accounts.api_keys.readaccounts.api_keys.writeaccounts.devices.readaccounts.devices.writeaccounts.organizations.readaccounts.organizations.write

  • Error code

    The user's merchant role does not allow sandbox management.

Delivery configuration
5 additions
Locations
5 additions
  • Request field
    addresson PATCH /v1/locations/{location_id}

    Complete replacement postal address, validated as on create.

  • Request field
    coordinateon PATCH /v1/locations/{location_id}

    Complete replacement coordinate.

  • Request field
    coordinate_sourceon PATCH /v1/locations/{location_id}

    Source of the coordinate sent in the same request.

  • Request field
    timezoneon PATCH /v1/locations/{location_id}

    IANA timezone for local schedules, validated as on create.

  • Field

    Read-only version that advances only on geography changes and is pinned by tax and delivery dependencies.

Organizations
5 additions
Analytics
4 additions
Refunds
4 additions
Subscription plans
4 additions
API request logs
3 additions
  • Field

    How to recover: same_request resends the identical request with the same Idempotency-Key; after_refresh re-reads the resource and rebuilds the request with a new key; none requires correcting the request or following next_steps or next_actions.

  • Field

    How to recover: same_request resends the identical request with the same Idempotency-Key; after_refresh re-reads the resource and rebuilds the request with a new key; none requires correcting the request or following next_steps or next_actions.

  • Values

    discountflint_walletflint_wallet_cardinventory_transfer_lineinvoice_line_itemproduct_option_value

Credit notes
3 additions
  • Request field
    line_itemson PATCH /v1/credit-notes/{credit_note_id}

    Full replacement of the draft's owned line collection; requires expected_version.

  • Request field
    line_itemson POST /v1/credit-notes

    Initial draft corrections.

  • Field

    Itemized corrections against frozen invoice lines.

Devices
3 additions
Disputes
3 additions
Payments
3 additions
Return inspections
3 additions
Bundles
2 additions
  • Request field
    category_handleson PATCH /v1/bundles/{bundle_id}

    Relationship set of category handles that must already exist.

  • Request field
    category_handleson POST /v1/bundles

    Relationship set of category handles that must already exist.

Delivery selections
2 additions
Partner OAuth
2 additions
  • Query parameter
    response_typeon GET /v1/oauth/authorize/preview

    OAuth response type.

  • Query parameter
    stateon GET /v1/oauth/authorize/preview

    Opaque state value returned to the partner callback.

Reports
2 additions
  • Query parameter
    report_typeon GET /v1/reports

    Return reports with any of these report types.

  • Query parameter
    statuson GET /v1/reports

    Return reports in any of these statuses.

Risk controls
2 additions
Delivery previews
1 addition
Merchants
1 addition
Payment method domains
1 addition
Return dispositions
1 addition
Return resolutions
1 addition
Webhook events
59 additions
  • Field
    Balance.observed_at

    When Flint observed these amounts, as an RFC 3339 UTC instant at second precision.

  • Field
    DeliveryEligibilityExpression.customer_merchant_verified

    Matches the is_merchant_verified flag you set on the customer against value.

  • Field
    DeliveryEligibilityExpression.customer_segment

    Matches the free-form segment you assign to the customer against values.

  • Field
    ExpandedPayoutSummary.arrival_date

    Calendar date the bank is expected to receive the funds, in YYYY-MM-DD format using the UTC date of the expected arrival.

  • Field
    GiftCardPurchaseRefundValueAllocation.amount_money

    Monetary amount represented as integer minor units plus an ISO 4217 currency code.

  • Field
    GiftCardRedemption.order_payment_attempt_id

    The order payment attempt the redemption belongs to.

  • Field
    GiftCardRedemption.sale_reference

    Your reference for the sale the redemption pays for.

  • Field
    GiftCardRedemption.source_type

    string

Errors
19 additions
  • API
  • Checkout
  • Orders
  • Payments
  • Delivery

Pay an order with your API key while its checkout session is open#

Paying an order with your API key while its checkout session was open returned 409 ORDER_COLLECTION_IN_PROGRESS, and for an order with delivery, the selection made on the session was out of reach. Now the payment runs, and it uses the session's current delivery selection, so the delivery charge on the order is the one charged. You can quote, select, and pay a phone or server-side order with your API key alone. See Take the order yourself.

While the session is open, the payment must send expected_outstanding_money, the order's outstanding_money you read. Without it the payment returns 400 FIELD_REQUIRED, and a balance that changed returns 409 ORDER_CHANGED_REFRESH_REQUIRED with the current balance in details[0].current_money. Paying an order with no open session keeps the field optional.

Only one payment for the order runs at a time. While the buyer is paying, your request returns 409 ORDER_PAYMENT_ATTEMPT_ACTIVE. While yours runs, a payment or an order change with the checkout credential returns 409 PAYMENT_IN_PROGRESS, and reading the session returns the new problems[] code payment_in_progress, which names neither your payment nor its amount. The hosted page tells the buyer a payment for the order is already being processed.

A payment with your API key that collects money, the whole balance or part of it, commits the delivery selection and ends the session as invalidated with the new terminal_reason value order_paid_elsewhere. Flint sends checkout_session.invalidated, the session's checkout credential can still read it, and the hosted page tells the buyer the order was paid another way. A declined payment leaves the session open with the buyer's selection in place. See Paying the order with your API key.

  • Breaking change
  • API

Lists include archived records only when status names them#

Every list follows one rule now: it leaves out records whose status is archived or deleted unless your status filter names that status. Leave status out and you get every other status. Pagination states the rule.

Breaking: include_archived is removed from these lists, and sending it returns UNSUPPORTED_QUERY_PARAM. Name the statuses you want in status instead, such as status=active,inactive,archived.

  • GET /v1/delivery-profiles, /v1/delivery-zones, /v1/delivery-location-sets, /v1/delivery-methods, and /v1/delivery-rate-callbacks
  • GET /v1/risk-rules and /v1/risk-lists
  • GET /v1/payout-settings/destinations

Breaking: these lists used to include archived records when you left out status. They now leave them out:

  • GET /v1/subscription-offers
  • GET /v1/return-policies and /v1/return-reasons
  • GET /v1/invoice-payment-terms
  • GET /v1/developer/sandboxes, which also drops the all status value. Send status=active,archived to list every sandbox.

Breaking: GET /v1/locations no longer lists only active locations by default. Leave out status and you get active and inactive locations. Archived ones appear only with status=archived.

The status filter on the delivery lists, payout destinations, subscription offers, invoice payment terms, and sandboxes accepts several values, as repeated keys or one comma-separated value. Risk rules and risk lists now publish a read-only status, active or archived, and their lists take a status filter that works the same way.

  • Breaking change
  • API
  • Checkout

Checkout sessions take tax from the order#

An order has one tax setting, its own tax. Hosted checkout now asks the buyer for a tax location whenever the order's automatic tax needs one and the order doesn't ship, whatever created the session. Before this change, hosted checkout asked only when the session was created with tax.enabled: true. An order with automatic tax whose session left tax out never asked, so paying failed with ORDER_TAX_NOT_READY. This also affected sessions created from quick_pay_item or subscription_plan_id without tax, when the merchant's tax.default_enabled setting turned tax on for the new order. Flint now also uses a prefilled address to set the tax location for those orders, as it already did when tax.enabled was sent.

Breaking: POST /v1/checkout-sessions accepts tax only with quick_pay_item or subscription_plan_id, where it sets the tax of the order Flint creates. With order_id, sending tax returns 400 INVALID_REQUEST with detail NOT_ALLOWED and param: tax. Set tax on the order with PATCH /v1/orders/{order_id} instead.

Breaking: checkout sessions no longer return tax, in responses or in checkout_session.* webhooks. Read tax on the session's order. Payment links keep tax: a link's tax still sets the tax of each order it creates.

Breaking: an order's tax no longer returns taxability_reason or failure_reason. Both repeated tax.status, which is unchanged: not_required, requires_location, calculated, exempt, or incomplete. A calculated order with nothing taxable has pricing_amounts.tax_money of zero. This applies to every order response, including /v1/me orders and order webhooks. Each row in tax.tax_breakdowns keeps its own taxability_reason.

  • API

Count observations keep the conditions you didn't count#

PATCH /v1/inventory-counts/{inventory_count_id} now requires only inventory_item_id and counted_on_hand_quantity in each observation. counted_quality_control_quantity, counted_damaged_quantity, and counted_quarantined_quantity are optional.

When you leave a condition quantity out, Flint saves the line's expected_<condition>_quantity for it, and the saved line reports that value as counted. Applying the count then leaves that condition unchanged. Send 0 only when you counted none of that condition. Before this change, a counting screen that sent 0 for every empty box zeroed damaged, quality control, and quarantined stock nobody had counted, and those units became available to sell.

If counted_on_hand_quantity is below the sum of the three condition quantities, including the ones Flint filled in, the PATCH returns 400 INVALID_REQUEST with param set to observations[i].counted_on_hand_quantity. That refusal used to be INVENTORY_CONDITION_INVALID on the condition field; no route returns that code any more.

The Dashboard's count entry form now sends only the boxes you fill in, and each empty condition box shows the quantity it keeps.

  • Breaking change
  • API
  • Errors

Not-found, authentication, and server errors use generic codes#

Breaking: the public API now returns a small set of generic error codes for missing resources, credential failures, and server failures. Branch on code, find the input in param, and read the facts in details[0]. Error handling and Authentication describe each code.

Not found#

An unknown ID in the path, the query, or the body returns 404 RESOURCE_NOT_FOUND. param names where the ID came from, such as order_id, promotion_id, or to_delivery_method_id. details[0].resource_type is the missing resource's type, such as promotion, or delivery_method for to_delivery_method_id, and details[0].resource_id is the ID you sent. It replaces every per-resource code, such as ORDER_NOT_FOUND, GIFT_CARD_NOT_FOUND, PROMOTION_NOT_FOUND, DISCOUNT_NOT_FOUND, SUBSCRIPTION_OFFER_NOT_FOUND, PAYMENT_LINK_NOT_FOUND, CHECKOUT_SESSION_NOT_FOUND, INVOICE_NOT_FOUND, PAYMENT_METHOD_NOT_FOUND, DISPUTE_NOT_FOUND, IMAGE_ASSET_NOT_FOUND, REPORT_NOT_FOUND, the INVENTORY_*_NOT_FOUND codes, DELIVERY_RESOURCE_NOT_FOUND, and CUSTOMER_ACCOUNT_RESOURCE_NOT_FOUND. A missing Flint Wallet, formerly FLINT_WALLET_NOT_FOUND, has details[0].resource_type flint_wallet and no param.

These lookups returned another status and now return 404:

Former codeFormer status
CREDIT_NOTE_INVOICE_LINE_NOT_FOUND, MERCHANT_ENVIRONMENT_NOT_FOUND, ORDER_CHARGE_NOT_FOUND, ORDER_LINE_ITEM_NOT_FOUND, REFUND_ADJUSTMENT_NOT_FOUND, RISK_LIST_ALIAS_NOT_FOUND, SANDBOX_NOT_FOUND400
PAYOUT_DESTINATION_NOT_FOUND, PAYOUT_NOT_FOUND409
FULFILLMENT_METHOD_UNAVAILABLE for a delivery method that doesn't exist409

FULFILLMENT_METHOD_UNAVAILABLE stays 409 for a method that isn't active, or for a zone, location set, or rate callback that is missing or not active.

Authentication and authorization#

A credential failure at 401 uses one of two codes:

  • AUTHENTICATION_REQUIRED when no credential was sent. It replaces API_KEY_REQUIRED, AUTH_REQUIRED, CHECKOUT_SESSION_REQUIRED, CHECKOUT_SESSION_AUTH_REQUIRED, CHECKOUT_CREDENTIAL_REQUIRED, ONBOARDING_SESSION_REQUIRED, CUSTOMER_SESSION_REQUIRED, and BUYER_INVOICE_CREDENTIAL_REQUIRED.
  • INVALID_CREDENTIAL when a credential was sent but can't be used. details[0].reason says why, and remediation.next_steps says what to do for that reason:
details[0].reasonFormer codes
malformedINVALID_AUTHORIZATION_HEADER, INVALID_API_KEY, CHECKOUT_SESSION_SECRET_REQUIRED
unknownAPI_KEY_NOT_FOUND, INVALID_OAUTH_TOKEN, INVALID_PARTNER_TOKEN, INVALID_BEARER_TOKEN, INVALID_CHECKOUT_SESSION, INVALID_ONBOARDING_SESSION, INVALID_CUSTOMER_SESSION, BUYER_INVOICE_CREDENTIAL_INVALID, AUTHENTICATION_FAILED
revokedAPI_KEY_REVOKED, and API_KEY_SANDBOX_UNAVAILABLE for a key whose sandbox was archived or is inactive
expiredAPI_KEY_EXPIRED, CUSTOMER_SESSION_EXPIRED
mode_mismatchAPI_KEY_MODE_MISMATCH, SANDBOX_SELECTION_REQUIRED for a test key not bound to a sandbox, and API_KEY_SCOPE_UNSUPPORTED
wrong_credential_typeEXTERNAL_API_KEY_REQUIRED, CHECKOUT_CREDENTIAL_REQUIRED for merchant authentication on a checkout-only route, PARTNER_AUTH_UNSUPPORTED, ONBOARDING_PARTNER_AUTH_UNSUPPORTED

The reason set can grow, so treat an unfamiliar reason like unknown. EXTERNAL_API_KEY_REQUIRED and the merchant-authentication case of CHECKOUT_CREDENTIAL_REQUIRED returned 403 and now return 401.

A 403 is INSUFFICIENT_SCOPE, unchanged, or FORBIDDEN with details[0].reason set to the former code in lower case, such as checkout_recovery_restricted. FORBIDDEN replaces MERCHANT_ACCESS_REQUIRED, SANDBOX_MANAGEMENT_REQUIRES_LIVE_KEY, PARTNER_APP_MANAGEMENT_REQUIRES_LIVE_KEY, DEMO_SESSION_ROUTE_NOT_ALLOWED, CHECKOUT_LAUNCH_TOKEN_RESTRICTED, CHECKOUT_RECOVERY_RESTRICTED, CHECKOUT_RECOVERY_ATTEMPT_MISMATCH, CHECKOUT_SESSION_PAYMENT_REQUIRED, CHECKOUT_SESSION_ORDER_MISMATCH, OAUTH_CONTEXT_MISMATCH, SANDBOX_NOT_ACCESSIBLE, SANDBOX_SELECTION_FORBIDDEN, and ENVIRONMENT_SELECTION_FORBIDDEN. AUTHORIZATION_FAILED becomes FORBIDDEN with reason not_permitted. Paying an order with your API key and save_payment_method: true stays 403, now FORBIDDEN with reason not_checkout_session and param save_payment_method, because only the buyer's checkout session can save a card.

These keep their codes: 400 AMBIGUOUS_AUTH, 400 SANDBOX_SELECTION_REQUIRED for an onboarding request when the merchant has no default sandbox, 400 ME_CUSTOMER_ID_FORBIDDEN, the 400 cases of CHECKOUT_SESSION_REQUIRED and INVALID_CUSTOMER_SESSION, the refresh-token codes CUSTOMER_SESSION_REFRESH_EXPIRED and CUSTOMER_SESSION_REFRESH_REUSED, the verification codes INVALID_VERIFICATION, VERIFICATION_ALREADY_USED, and VERIFICATION_ATTEMPTS_EXCEEDED, 401 CHECKOUT_SESSION_AUTH_UNAVAILABLE, and refusals that depend on the request rather than the credential, such as CHECKOUT_SESSION_UPDATE_FIELD_NOT_ALLOWED and REQUEST_BLOCKED.

Server errors#

  • 500 INTERNAL_ERROR replaces the 500 codes that asked you to retry the same request, such as IDEMPOTENCY_LOAD_FAILED, SANDBOX_LIST_FAILED, CHECKOUT_SESSION_LOOKUP_FAILED, AUTH_CONTEXT_MISSING, ENVIRONMENT_LOOKUP_FAILED, IDENTITY_RESOLUTION_FAILED, and PAYMENT_METHOD_DOMAINS_UNAVAILABLE.
  • 502 EXTERNAL_SERVICE_ERROR replaces ORDER_TAX_CALCULATION_FAILED and PAYMENT_PROCESSOR_ERROR.
  • 503 SERVICE_UNAVAILABLE replaces the 503 codes, such as DELIVERY_SERVICE_UNAVAILABLE, DELIVERY_PREVIEW_UNAVAILABLE, CUSTOMER_VERIFICATION_UNAVAILABLE, GIFT_CARDS_UNAVAILABLE, RISK_EVALUATION_UNAVAILABLE, PAYOUT_ENTRIES_UNAVAILABLE, REPORTING_UNAVAILABLE, PAYMENT_METHOD_DOMAIN_REGISTRATION_FAILED, PROCESSING_FEE_PRICING_UNAVAILABLE, IDENTITY_UNAVAILABLE, and OAUTH_UNAVAILABLE. Retry-After is still sent when Flint knows the delay.
  • 504 REQUEST_TIMEOUT replaces the other 504 codes.

These server codes stay, because their recovery differs from a plain retry: AUTOMATIC_TAX_UNAVAILABLE, ORDER_TOTALS_UNAVAILABLE, PAYMENT_PROCESSING_UNAVAILABLE, PROFILE_UPDATE_STATUS_UNKNOWN, IDEMPOTENCY_RECOVERY_REQUIRED, DANGLING_EXPANSION_REFERENCE, FLAT_TAX_REFUND_STATE_INVALID, FULFILLMENT_EVENT_SUBJECT_INVALID, SENSITIVE_IDEMPOTENCY_RESULT_INVALID, and the image ingestion codes IMAGE_PROCESSING_FAILED, IMAGE_PROCESSING_CAPACITY_EXCEEDED, IMAGE_PROCESSING_TIMEOUT, IMAGE_SOURCE_TIMEOUT, and IMAGE_SOURCE_UNAVAILABLE. An image that fails processing in a way a retry can't fix returns 400 IMAGE_FORMAT_UNSUPPORTED instead of IMAGE_PROCESSING_FAILED. *_UNAVAILABLE codes returned with a 4xx status, such as PAYMENT_OPTION_UNAVAILABLE and INVENTORY_UNAVAILABLE, keep their names.

Validation#

Four more validation codes now return 400 INVALID_REQUEST, with the same param:

Former codedetails[0].code
NULL_NOT_ALLOWEDINVALID_VALUE
INVALID_FIELD_TYPEINVALID_VALUE
UNKNOWN_FIELDUNKNOWN_FIELD
INVALID_EXPAND, EXPANSION_LIMIT_EXCEEDEDINVALID_VALUE, with param expand

Per-operation error codes#

Each operation's x-flint-error-codes is now generated from the route's authentication schemes, body, idempotency, pagination, expand support, path parameters, and handler. It lists exactly the codes the operation can return, top-level code and details[].code alike, and only public codes. A route that takes no body no longer lists body-size or content-type errors, and a route without expand no longer lists expand errors.

  • Breaking change
  • API
  • Money-movement
  • Gift-cards
  • Inventory
  • Returns
  • Checkout

One shape for references that can point at more than one resource type#

Breaking: the fields and filters below change shape with no alias. An old query parameter returns UNSUPPORTED_QUERY_PARAM.

A field that can point at more than one kind of resource is now always an object with resource_type and resource_id, and the field's name says what it is (related_resource, source, linked_resource). resource_type is the resource's ID field name without _id, such as refund or gift_card_load. OpenAPI publishes every value as one extensible ResourceType enum, which errors, request logs, resource timelines, webhook events, and these references share. Accept values you don't recognize.

WhereOldNew
Balance transactionrelated_resource: {"type": "refund", "refund_id": "ref_..."}related_resource: {"resource_type": "refund", "resource_id": "ref_..."}
GET /v1/balance-transactionsrelated_object_type, related_object_idrelated_resource_type, related_resource_id. reserve and adjustment are no longer values; filter by type instead. A missing type or a mismatched ID returns INVALID_REQUEST with details[0].code REQUIRED or INVALID_VALUE
Inventory movementsource_reference: {"source_reference_type", "source_reference_id"}source_reference: {"resource_type", "resource_id"}, where resource_type is fulfillment, inventory_reservation_line, inventory_count, inventory_transfer, or product_variant. Movements from a return receipt carry return_id and no source_reference. The source_reference_type and source_reference_id filters keep their names and take those values
Gift card transactionsource_type, source_idsource: {"resource_type", "resource_id"}, or null on adjustments and cash-outs
GET /v1/gift-card-transactionssource_type values such as refund and gift_card_purchase_refundsource_type takes ResourceType values: gift_card_load, gift_card_redemption, gift_card_funding_disposition, refund_allocation, or purchase_refund_allocation. Select adjustments and cash-outs with the new multi-value transaction_type filter, such as transaction_type=adjustment,cash_out
Return resolution execution_blockers[]linked_resource_type, linked_resource_idlinked_resource: {"resource_type", "resource_id"}, returned to merchant credentials only
Checkout session delivery_pinned_dependencies[]delivery_dependency_type, delivery_dependency_iddependency: {"resource_type", "resource_id"}. A location geography pin is location, with its version in location_geography_version; a location set revision is delivery_location_set, with the revision in revision_id. The internal evaluation schema pin is no longer listed
Error blocking_resources[]resource_type: "other"Every blocker is a ResourceType value. A delivery method or profile held by your checkout or catalog defaults is reported as your merchant; change those defaults with PATCH /v1/settings

On a gift card transaction, a refund paid back to the card was source_type: "refund" with the refund's tender allocation ID in source_id. It is now refund_allocation, matching refund_allocation_id in the refund's tender_allocations. A refund of the card's purchase is purchase_refund_allocation, matching purchase_refund_allocation_id in the load's purchase_refunds. transaction_type still says what each posting did.

  • Breaking change
  • API

Routes that move money or stock require an Idempotency-Key, and other writes never do#

Every route has one idempotency class, set by the route and never by the request body: required for routes that create a new money or stock movement each time they run, optional for every other write, and none for reads, previews, and lookups. Each endpoint's API reference entry marks its class, and the Idempotency guide lists every required route.

Breaking: these routes now reject a request with no Idempotency-Key:

  • POST /v1/payment-intents and POST /v1/orders/{order_id}/payment-intents
  • POST /v1/payouts
  • POST /v1/invoices/{invoice_id}/manual-payments and POST /v1/invoices/{invoice_id}/manual-payments/reverse
  • POST /v1/subscriptions
  • POST /v1/credit-notes/{credit_note_id}/issue, with or without refund
  • POST /v1/orders/{order_id}/pay, for every action
  • POST /v1/refunds, for every refund

Breaking: a missing key on a required route returns 400 INVALID_REQUEST with details[0] { "code": "REQUIRED", "param": "Idempotency-Key" } instead of 400 IDEMPOTENCY_KEY_REQUIRED. Flint rejects it before reading the body, and nothing runs. IDEMPOTENCY_KEY_REQUIRED is removed.

A required route keeps its key for as long as the command's effects exist, so a retry that arrives days later returns the original result instead of charging, paying out, or refunding again. A replay returns a one-time secret, such as a full gift card code or the client_secret in an order payment's pending action, only within 24 hours of the original request.

These routes no longer require a key and accept an optional one: PATCH /v1/gift-cards/{gift_card_id}, gift card transitions and code rotation, gift card notifications and their cancellation, gift card redemption capture and cancel, inventory counts and their updates, apply, and cancel, PATCH /v1/inventory-levels/{inventory_level_id}, inventory reservation commit, consume, and release, inventory transfer transitions, POST /v1/payment-links/{payment_link_id}/resolve, POST /v1/subscription-delivery-migrations, POST /v1/me/flint-wallet/payment-methods/{id}/store-setups, POST /v1/webhook-endpoints/{webhook_endpoint_id}/test-events, and POST /v1/webhook-deliveries/{webhook_delivery_id}/resend. A request without a key runs once and is not replayed.

Flint's SDKs and CLI already send a key on every write. Raw HTTP callers add the header to the routes above.

  • Breaking change
  • API
  • Inventory

Inventory reservations need only a key, an item, and a quantity#

Holding stock from your own cart takes less input, and reservations no longer show values that only Flint's checkout uses.

  • owner.expires_at is optional. It defaults to 15 minutes after the reservation is created, and a value you send must still be within 15 minutes. A retry with the same Idempotency-Key and body returns the original reservation and deadline.
  • demands[].demand_key is optional and defaults to the demand's inventory_item_id. When the same inventory_item_id appears in more than one demand, send demand_key on each of them; otherwise the request returns 400 INVALID_REQUEST with param set to that demand's demand_key.
  • demands[].splitting_behavior is optional. Without it, the routing source's rule decides.
  • On allocation policies, configuration.maximum_locations_per_assignment is optional. It defaults to 10 for split_when_required, and single_location always uses 1. Sending another value with single_location returns 400 INVALID_REQUEST.

Breaking: POST /v1/inventory-reservations no longer accepts assignments, destination_fingerprint, demands[].order_line_item_id, demands[].bundle_component_id, or owner.type. Sending any of them returns 400 INVALID_REQUEST.

Breaking: a reservation's owner is {"type": "merchant", "key", "expires_at"} for one you created, or {"type": "order", "order_id", "expires_at"} for stock Flint holds for an order or checkout. order_id is absent only on a closed reservation whose order Flint can no longer identify. Earlier responses could show five owner types with Flint's internal key. GET /v1/inventory-reservations now lists every owner by default; owner_type accepts merchant or order, and owner_key matches only reservations you created.

Breaking: reservations drop inventory_action_required.next_actions, inventory_routing_source.location_ids, and lines[].location_geography_version. inventory_action_required.reason is inventory_shortage, hold_expired, payment_pending, or other.

Breaking: when other writes keep changing the same stock while Flint applies an inventory command, Flint retries internally and, if that fails, returns 409 CONCURRENT_MODIFICATION with no param. Retry the same request with the same Idempotency-Key. Before, these requests could return 409 INVENTORY_CHANGED for an expected_version you never sent. INVENTORY_CHANGED now means only that the expected_version you sent is stale. A safety stock change with PATCH /v1/inventory-levels/{inventory_level_id} that sends no expected_version now applies over a concurrent change instead of failing.

  • Breaking change
  • API
  • Invoices
  • Inventory
  • Idempotency

Create and issue an invoice in one call, and sell tracked stock with quick pay#

POST /v1/invoices can now create and issue an invoice in one request, and quick pay invoices can sell catalog items whose stock Flint tracks.

  • Add issue to the create request, for example "issue": {"delivery_mode": "email"}, and the response is 201 with the invoice already open, carrying invoice_number, issued_at, and public_url. issue.delivery_mode takes the issue endpoint's values and defaults to merchant_default. If a check fails before the invoice is created, such as RECIPIENT_EMAIL_REQUIRED, nothing is created. If issuing fails after the invoice is created, it stays a draft, the error's details name it (resource_type invoice and its resource_id), and retrying with the same Idempotency-Key finishes issuing that invoice without sending the email or starting an automatic charge twice. issue together with scheduled_send_at returns 400 INVALID_REQUEST with detail NOT_ALLOWED and param issue.
  • quick_pay.inventory_routing_source takes the same shape as an order's inventory_routing_source. It is required when any line item tracks inventory; without it, the request returns 400 INVALID_REQUEST with detail REQUIRED and param quick_pay.inventory_routing_source. Before, a quick pay invoice couldn't include tracked items at all. Flint holds, commits, and releases the stock as it does for any order.
  • A replay of POST /v1/invoices includes public_url for 24 hours. After that, read the current link with GET /v1/invoices/{invoice_id}.

Breaking: POST /v1/invoices requires the Idempotency-Key header, because a create that issues an automatic-collection invoice charges the customer. A request without it returns 400 INVALID_REQUEST with detail REQUIRED and param Idempotency-Key. The Flint SDKs and CLI send a key for you.

The invoicing guide now also says that collection and payment_due are optional and default to merchant_default, as the API already behaved.

  • Breaking change
  • API

List time ranges exclude their _before bound, and sort values name response fields#

Breaking: on every list, a {field}_before filter excludes the instant it names, and {field}_after includes it. created_after=2026-09-01T00:00:00Z&created_before=2026-10-01T00:00:00Z is exactly September, and the next window starts at 2026-10-01T00:00:00Z without sharing a record. Before, most lists included the _before instant too, and GET /v1/reviews and the returns lists, such as GET /v1/returns, /v1/me/returns, and /v1/return-inspections, excluded the _after instant. A fractional-second bound compares as the instant you send. arrival_after and arrival_before on GET /v1/payouts don't change.

Breaking: two range filters drop _at from their names:

  • GET /v1/invoices and GET /v1/me/invoices: due_at_after and due_at_before are now due_after and due_before.
  • GET /v1/subscriptions and GET /v1/me/subscriptions: next_billing_at_after and next_billing_at_before are now next_billing_after and next_billing_before.

The old names return UNSUPPORTED_QUERY_PARAM.

Breaking: sort_by values are the response field they sort by:

  • GET /v1/payment-intents, /v1/refunds, /v1/me/payments, and /v1/me/refunds: amount is now amount_money.
  • GET /v1/products/{product_id}/variants: unit_price is now unit_price_money.

Breaking: GET /v1/inventory-movements takes sort_direction instead of order. It still defaults to asc, oldest first, and order returns UNSUPPORTED_QUERY_PARAM.

In the OpenAPI document, every sort_by and sort_direction parameter publishes its default, and every range parameter's description reads "Inclusive lower bound on created_at." or "Exclusive upper bound on created_at." for its field. Pagination states the rules once.

  • Breaking change
  • API

Declined payments return 2xx with last_payment_error, and 402 is gone#

A declined, blocked, or failed payment is now resource state, never an error. Read status and last_payment_error in the response body; Declines and payment attempts has the table.

Breaking: Flint no longer returns 402, and the payment_error error type is removed. These routes return 2xx with the resulting resource when a payment declines or a risk rule blocks it:

  • POST /v1/payment-intents/{payment_intent_id}/confirm returns the payment intent, at requires_payment_method with last_payment_error after a decline or block. POST /v1/payment-intents and POST /v1/payment-intents/{payment_intent_id}/capture no longer list 402 either.
  • POST /v1/subscriptions/{subscription_id}/renew and POST /v1/me/subscriptions/{subscription_id}/renew return 200 with a renewal result, subscription, order, and payment_attempt, instead of the bare subscription. A declined charge has payment_attempt.status: "failed" with last_payment_error.
  • POST /v1/invoices/{invoice_id}/collect returns 200 with the invoice and the failed invoice_payment_attempt.
  • POST /v1/orders/{order_id}/pay already returned 200 for declines; its reference no longer lists 402.

Breaking: the decline error codes are removed, including CARD_DECLINED, INSUFFICIENT_FUNDS, PAYMENT_BLOCKED, PAYMENT_FAILED, PAYMENT_NOT_COMPLETED, SUBSCRIPTION_RENEW_PAYMENT_FAILED, and INVOICE_PAYMENT_FAILED. The reason is last_payment_error.code.

Breaking: every payment attempt reports a failure in last_payment_error, with code, message, failed_at, and remediation, the same shape a payment intent uses:

  • Order payment attempts drop failure_code and failure_message. A failure caused by a payment sets last_payment_error; any other reason an attempt ended, such as inventory_unavailable, is in the new extensible status_reason, with other for a reason Flint can't name. attempt_failed is retired.
  • Invoice payment attempts drop failure_code and failure_message.
  • Subscription payment retries drop failure.

PaymentErrorSummary now always carries all four fields. A payment intent's last_payment_error follows fixed rules: a processor timeout or outage returns a 5xx and never writes it, cancellation and expiry keep the last failure, and a later attempt that succeeds or needs buyer action clears it.

The decline vocabulary adds lost_card, stolen_card, do_not_honor, and fraudulent, which used to read as card_declined. Show the buyer a plain decline for lost_card, stolen_card, and fraudulent, not the reason. Responses to a buyer credential (/v1/me and checkout session credentials) report all four as card_declined with buyer copy; merchant credentials, API keys and the CLI, see the real code.

  • Breaking change
  • API
  • Payouts
  • Balances

List a payout's balance transactions with payout_id#

Reconciling a deposit is one paginated call that returns full balance transactions.

  • GET /v1/balance-transactions?payout_id=po_... lists the balance transactions a paid payout carried, with amount_money, fee_money, net_money, order_id, and related_resource. Their net_money adds up to the payout's amount_money. The payout's own payout row isn't included, and the filter combines with the list's other filters.
  • With payout_id, a payout that doesn't exist returns 404 RESOURCE_NOT_FOUND, a payout that isn't paid returns 409 ACTION_NOT_ALLOWED with details[0].current_status, and a paid payout whose contents are still being prepared returns 503 SERVICE_UNAVAILABLE with Retry-After. If Flint re-prepares a payout's contents while you page through them, the next page_token returns 400 INVALID_PAGE_TOKEN; start again from the first page.
  • GET /v1/balance-transactions filters by order_id and accepts expand=order, resolved for the whole page at once.
  • GET /v1/payouts accepts expand=payout_destination.

Breaking: GET /v1/payouts/{payout_id}/entries and the PayoutEntry schema are removed. Use GET /v1/balance-transactions?payout_id=.... Each entry's balance_transaction_id, type, occurred_at, and net_money are on the balance transaction with the same names. Rows come newest first, like the rest of the list, where entries came oldest first. A payout that isn't paid returns 409 instead of an empty list. The payouts_itemized_v1 report is unchanged.

  • Breaking change
  • API
  • Payouts
  • Webhooks
  • Reports

Balance transactions add up to your balance, and payouts in progress show only in held_money#

Summing net_money over balance transactions in any window now gives the change in your balance, including while a payout you created is still in progress. The funds a pending payout sets aside show in one place: held_money on the balance.

Breaking: balance transactions no longer include payout hold rows.

  • GET /v1/balance-transactions, GET /v1/balance-transactions/{balance_transaction_id}, balance_transaction.created, balance_transaction.updated, and the balance_transactions_itemized_v1 report leave out the rows Flint used to set funds aside for a payout and release them. Fetching one of those rows by ID returns 404 RESOURCE_NOT_FOUND.
  • type no longer takes payout_hold or payout_hold_release, and hold_detail is removed.
  • status is pending or available. reserved, superseded, reversed, and failed are removed from the field and from the status filter, which returns 400 for them.
  • A list without a status filter returns every row. Before, it left out reversed and superseded rows, so a row could drop out of later lists.

Breaking: the balance and the payout drop fields.

  • GET /v1/balances and balance.updated drop held_by_type and unavailable_money. held_money is the amount set aside for payouts you created that haven't been debited from your balance yet, and available_money already leaves it out.
  • Payouts drop held_money on REST responses, expanded payouts, and payout webhooks. Read the total from the balance.

Breaking: GET /v1/balance-transactions page tokens issued before October 17, 2026 return 400 INVALID_PAGE_TOKEN. Start the list again without page_token.

These fixes apply now:

  • Each payout you create has exactly one payout balance transaction. Before, a payout you created could also appear a second time when Flint imported your processor's history, so a window was off by the payout amount.
  • A failed payout's return posts as payout_failure and a canceled payout's as payout_cancellation, each exactly once, so the payout nets to zero. Before, both could post as payout_reversal.
  • Canceling a payout succeeds once your balance has been debited, and a payout_cancellation returns the amount to available_money. Before, the cancel could fail with 404 PAYOUT_NOT_FOUND and leave the payout pending.
  • If your processor rejects a payout you create, the amount goes back to available_money. Before, it could stay in held_money.
  • GET /v1/balances and balance.updated leave active payout holds out of available_money every time. Before, some balance reads and events left them in.
  • Breaking change
  • API
  • Payouts

Flint chooses the balance source for a payout#

A payout draws from one balance source: card funds or bank funds. You no longer have to name the source when your balance holds both.

  • Omit balance_source_type on POST /v1/payouts and Flint chooses card when card funds cover the amount, otherwise bank_account. Before, the request failed whenever both sources had funds, even if one covered the amount.
  • Each source's amount is what available_by_source_type on GET /v1/balances shows, which already leaves out funds held for your other payouts.
  • A retry with the same Idempotency-Key returns the same payout and source.

Breaking: BALANCE_SOURCE_TYPE_REQUIRED is retired. When no single source covers the amount, or the source you named doesn't, POST /v1/payouts returns 409 INSUFFICIENT_AVAILABLE_BALANCE. details[0].available_by_source_type lists what each source has available. To pay out more than one source holds, create one payout per source.

Breaking: balance_source_type on POST /v1/payouts and the GET /v1/payouts filter no longer accept fpx, and return 400 INVALID_REQUEST for it. The *_by_source_type maps on the balance are an open set of keys; US accounts show card and bank_account.

Breaking: payouts always include balance_source_type, on REST responses, expanded payouts, and payout webhooks. An earlier payout created without a source shows card, and GET /v1/payouts?balance_source_type=card includes it.

Breaking: STANDARD_PAYOUT_REQUIRES_MANUAL_SCHEDULE no longer sets param, and details[0].current_interval shows your current interval.

  • API
  • Reports

Every report covers up to 366 days, and the report list filters by type and status#

Every report type now accepts an interval of up to 366 calendar days in the report's timezone. Before, only payments_itemized_v1 and tax_summarized_v1 accepted more than 90 days, so a calendar quarter of 91 or 92 days returned 400 REPORT_INTERVAL_TOO_LARGE for the other types. A calendar quarter or a calendar year, including a leap year, is now one report.

The documented row limit is now 100,000 source or output rows, down from 1,000,000, which matches what a single database read allows. A report also fails when one database read returns more than 64 MiB or runs longer than 20 seconds. Before, those reads were retried for up to about five hours and the report then failed with generation_failed. Now the report fails on the first attempt with failure_reason: limit_exceeded and no file. The 500 MiB, 25-minute, and 10,000-group limits don't change. Split a long interval into smaller reports when a report fails this way.

GET /v1/reports accepts two optional filters:

  • report_type: one or more report types.
  • status: one or more of pending, succeeded, and failed.

Each takes repeated keys or one comma-separated value. Page tokens are bound to the filters they were issued with. Tokens from lists without filters keep working.

A succeeded report keeps status: succeeded after its download expires. Read download.expires_at to know whether the file is still available; after that instant the download route returns 410 REPORT_DOWNLOAD_EXPIRED, and a new POST /v1/reports produces a fresh file.

See Reports.

  • Breaking change
  • API

Variants select options by name#

Breaking: a variant selects its option values with selected_options, a list of {option_name, value} pairs. It replaces selected_option_values on POST /v1/products (default_variant and variants) and on POST /v1/products/{product_id}/variants. Names match the product's options and values case-insensitively. client_option_key and client_value_key are gone from product options, option values, and selections, and a selection no longer takes option_value_id. Sending any of them returns 400 INVALID_REQUEST with detail UNKNOWN_FIELD.

JSON
"selected_options": [{ "option_name": "Size", "value": "16 oz" }]

On product create, a name that matches no option or value in the same request returns 400 INVALID_REQUEST with detail INVALID_VALUE, and param names the entry, such as variants[1].selected_options[0].value. On the variant routes, a name that matches no stored option or value returns 404 RESOURCE_NOT_FOUND with details[0].resource_type set to product_option or product_option_value.

Breaking: POST /v1/products/{product_id}/variants takes the variant's fields at the top level of the body. Remove the variant wrapper: {"variant": {...}} now returns 400 INVALID_REQUEST with detail UNKNOWN_FIELD. Error param values on this route no longer start with variant..

PATCH /v1/products/{product_id}/variants/{variant_id} accepts selected_options. Send the variant's complete selection to move it to other values without recreating it. The variant_id stays the same, so inventory items, payment links, and bundles keep pointing at the variant, and orders placed before the change keep the values they were placed with. The update uses the same rules as create, and the product's version advances. A combination another variant uses returns 409 VARIANT_OPTION_COMBINATION_ALREADY_EXISTS. A variant in an active bundle can't change its values for active options and returns 409 VARIANT_IN_ACTIVE_BUNDLE; set the bundle to inactive first. Resending the variant's current selected_options is always allowed, including entries for options or values that are no longer active. Leaving out an option that is no longer active keeps the variant's value for it, unless the update selects an active option with the same name, which replaces it.

The same PATCH also accepts external_reference_id. Send a string to set it or null to clear it.

  • Breaking change
  • API
  • Webhooks

Webhooks and delivery rate callbacks are signed only with Standard Webhooks#

Breaking: webhook deliveries and delivery rate callbacks carry one signature: the Standard Webhooks headers webhook-id, webhook-timestamp, and webhook-signature. These headers are removed:

  • X-Flint-Signature, the older t=<unix>,v1=<hex> signature. Verify webhook-signature instead, as Step 4 of the Webhooks guide shows. The Flint SDKs' verifyWebhook and any Standard Webhooks library already check these headers.
  • X-Flint-Webhook-ID. webhook-id carries the same event ID, so deduplicate on it.
  • X-Flint-Event-Type, which repeated the body without a signature. Read event_type from the verified body. On a delivery rate callback, a connection check's body is {"type":"connection_check"} and a test delivery has "test": true.
  • X-Flint-Key-ID on delivery rate callbacks. X-Flint-Key-IDs still lists every key with a signature on the request, current key first.

X-Flint-Webhook-Endpoint-ID stays, so one URL that serves several endpoints can choose the secret before verifying. The OpenAPI document's x-flint-webhooks.signing no longer lists legacy_headers.

  • Breaking change
  • API
  • Checkout
  • Orders
  • Payments

Checkout sessions stay open when you edit the order#

Changing an order's line items, discounts, charges, tax, or tip with your API key ended its open checkout session with terminal_reason: "order_mutated" and sent checkout_session.invalidated. The session now stays open with the same ID, URL, and checkout credential. It keeps the buyer's saved contact and time zone, and a buyer who confirmed their email with a code keeps that credential. Flint re-prices the order in place and sends order.updated. This applies to hosted and embedded sessions.

The buyer approves the new total before paying. A payment with the checkout credential already requires expected_outstanding_money and returns 409 ORDER_CHANGED_REFRESH_REQUIRED when it no longer matches the order. POST /v1/orders/{order_id}/pay now also accepts an optional expected_version with the pay, confirm_payment_intents, and setup actions. A mismatch returns the same 409 with param: expected_version, which catches an edit that changes the order but not its total. Starting a payment advances the order's version, so after a declined or failed attempt, use the order in the pay response or read it again before retrying. If you also send accepted_gift_card_allocation, its order_version must match, or the request returns 400 INVALID_GIFT_CARD_SELECTION.

An edit that keeps what delivery was priced from, such as a charge, tip, or tax change, keeps the buyer's delivery selection. A change to line items, quantities, prices, or discounts releases it, quotes from before the edit return DELIVERY_QUOTE_STALE, and paying returns FULFILLMENT_SELECTION_REQUIRED until the buyer selects from a new quote.

Closing the order, or an edit that leaves nothing to pay, still ends the session with order_mutated. Sessions for an invoice, a return, or a subscription plan keep their current behavior. A session already past expires_at ends as expired when you edit the order. Changing customer_id while a session is open still returns 409 ORDER_CUSTOMER_CHECKOUT_ACTIVE.

Breaking: creating a new session after an order edit now returns 409 CHECKOUT_SESSION_ALREADY_EXISTS, with the open session in existing_checkout_session_id. Keep using that session, or replace it on purpose with replace_checkout_session_id. Don't wait for checkout_session.invalidated after an edit. See Changing the order during checkout.

  • Breaking change
  • API
  • Errors

Validation errors use INVALID_REQUEST, and enum values match exactly#

Breaking: validation failures that had a different code on each endpoint now return INVALID_REQUEST (400), with param naming the field and message saying what to send. details[0] repeats param and message with code REQUIRED for a missing field or INVALID_VALUE for a value Flint rejects. When no single field is at fault, such as an update that sends none of its fields, there is no param and details[0].code is INVALID_REQUEST. Branch on INVALID_REQUEST once instead of per endpoint. It replaces about 320 codes, for example:

  • Missing fields: LINE_ITEM_NAME_REQUIRED, EXPECTED_VERSION_REQUIRED, AMOUNT_MONEY_REQUIRED, RANGE_REQUIRED, FIELD_REQUIRED, and the MISSING_* codes.
  • Unsupported values: INVALID_STATUS, INVALID_PAYMENT_STATUS, INVALID_REFUND_STATUS, and the other INVALID_<RESOURCE>_STATUS filter codes, INVALID_SORT_BY, INVALID_SORT_FIELD, INVALID_SORT_DIRECTION, INVALID_CAPTURE_METHOD, INVALID_ENUM_VALUE, and similar.
  • Malformed timestamps: INVALID_CREATED_AFTER, INVALID_UPDATED_BEFORE, INVALID_INTERVAL_START_AT, INVALID_TIMESTAMP, and similar.
  • Inverted ranges: INVALID_CREATED_RANGE, INVALID_REPORT_INTERVAL, SHIPMENT_CREATED_RANGE_INVALID, the min_quantity above max_quantity case of INVALID_QUANTITY_RANGE, and INVALID_RANGE. param names the later bound.
  • Malformed IDs: INVALID_ID, INVALID_ID_FORMAT, INVALID_RESOURCE_ID, INVALID_SOURCE_ENVIRONMENT_ID, and similar.
  • Boolean query parameters: INVALID_ACTIONABLE, INVALID_HAS_PLAN, INVALID_BOOLEAN, and similar.
  • Other malformed requests: VALIDATION_ERROR, INVALID_REQUEST_BODY, INVALID_GIFT_CARD_REQUEST, INVALID_CUSTOMER_ACCOUNT_REQUEST, and REQUEST_FAILED on a 400.

Also:

  • Enum values in bodies, query parameters, and headers must match the published value exactly. ACTIVE, " active", and "active " return INVALID_REQUEST. Enum list filters still trim spaces around each comma-separated value.
  • An explicit null on a field that can't be cleared returns NULL_NOT_ALLOWED everywhere, including fields that returned INVALID_METADATA or INVALID_WEBHOOK_ENDPOINT for it.
  • Codes that ask you to do something other than correct a field keep their names, such as ORDER_REVISION_REQUIRED, IDEMPOTENCY_KEY_REQUIRED, MERCHANT_ONBOARDING_REQUIRED, and DELIVERY_RECIPIENT_REQUIRED.
  • When a request is complete but would leave a saved resource without something it needs, the specific code stays: BUNDLE_COMPONENTS_REQUIRED for an update that leaves an active bundle without components, and INVENTORY_ROUTING_SOURCE_REQUIRED for paying an order, or updating a plan or payment link, whose tracked line items have no routing source. The same fields missing from a create request return INVALID_REQUEST.
  • Some update-only codes keep their names, such as NAME_REQUIRED and CHARGE_NAME_REQUIRED.
  • A payment link line item with min_quantity or max_quantity below 1 returns INVALID_QUANTITY with param naming the field, such as line_items[0].min_quantity, instead of INVALID_QUANTITY_RANGE with param line_items.

Each operation's x-flint-error-codes lists INVALID_REQUEST, REQUIRED, and INVALID_VALUE, and Error handling describes it.

  • Breaking change
  • API
  • Orders
  • Checkout
  • Payments
  • Gift-cards
  • Customers

Consistent names for versions, references, and checkout settings#

Breaking: every field, filter, and value below is renamed with no alias. A request that sends an old field returns UNKNOWN_FIELD, and an old query parameter returns UNSUPPORTED_QUERY_PARAM. Error param values and messages use the new names. Error codes keep their names, such as ORDER_REVISION_REQUIRED and CHECKOUT_SESSION_REVISION_CONFLICT.

A counter that guards against concurrent changes is now always version, its fence is expected_version, and a conflict reports current_version. A counter for one part of a resource is <part>_version. revision now names only snapshots with their own IDs, such as return policy revisions and delivery configuration revisions, and those names are unchanged.

Orders#

WhereOldNew
Orderorder_revisionversion
Update, close, line item add, charge add and update, discount apply, remove, and reprice, gift card selection, and pay requestsorder_revisionexpected_version
DELETE line item, charge, and gift card routes, queryorder_revisionexpected_version
gift_card_estimate and accepted_gift_card_allocationorder_revisionorder_version
ORDER_CHANGED_REFRESH_REQUIRED errororder_revisioncurrent_version, with param naming the fence you sent

Every order DELETE returns the order, so its expected_version is the order's version.

Inventory#

WhereOldNew
Inventory level, including a movement's resulting_inventory_levelinventory_level_physical_revisionphysical_version
Inventory level, including a movement's resulting_inventory_levelinventory_level_claim_revisionclaim_version
Inventory movementinventory_level_revisioninventory_level_version
Inventory movementinventory_level_physical_revisioninventory_level_physical_version
Inventory movementinventory_level_claim_revisioninventory_level_claim_version
Inventory count linecaptured_physical_revisioncaptured_physical_version
Inventory count conflict errorcaptured_physical_revisioncaptured_physical_version
Inventory count conflict errorcurrent_physical_revisioncurrent_physical_version
Inventory count conflict error, conflict_type valuephysical_revisionphysical_version

Locations and delivery#

WhereOldNew
Locationgeography_revisiongeography_version
location.inventoryinventory_revisioninventory_version
Inventory assignments, reservation lines, and checkout delivery pinned dependenciesgeography_revisionlocation_geography_version
Delivery revocation targetslocation_geography_revisionlocation_geography_version
Delivery quotes, delivery selections, and the delivery rate callback requestdelivery_quote_revisiondelivery_quote_version
Delivery rate callback request, execution_legs[]origin_geography_revisionorigin_geography_version
Delivery quotes and delivery selectionseligibility_context_revisioneligibility_context_version
DELIVERY_ELIGIBILITY_CONTEXT_CHANGED errorparam: expected_eligibility_context_revisionNo param, because no request field caused it
WhereOldNew
Checkout session and payment link tip, and tipping settingsis_custom_tip_enabledcustom_tip_enabled
Checkout session and payment link tip, and tipping settingsis_smart_tips_enabledsmart_tips_enabled
receipts settingsis_auto_email_enabledauto_email_enabled
Checkout session and payment link customer_collectionenable_address_autocompleteaddress_autocomplete_enabled
checkout settingspromotion_code_entry_enabledpromotion_codes_enabled
Checkout sessionrecovery_modeis_in_recovery
Checkout sessionsave_payment_method_offeredcan_save_payment_method
Checkout sessionsave_payment_method_phone_offeredcan_save_payment_method_with_phone
GET /v1/payment-links filterhas_planhas_subscription_plan
Checkout session customer verification (cscv_)customer_verification_idcheckout_session_customer_verification_id
Checkout session customer verification confirm route, path parameter{customer_verification_id}{checkout_session_customer_verification_id}
resource_type for a cscv_ IDcustomer_verificationcheckout_session_customer_verification
Checkout session modifier conflict, conflict_details[]latest_revisioncurrent_version

Customer verifications at /v1/customer-verifications (cver_) keep customer_verification_id.

Returns and credit notes#

WhereOldNew
Credit notes, their webhooks, and create and update requestscredit_note_linesline_items
Credit note line itemcredit_note_line_idcredit_note_line_item_id
Credit note line itemdescriptionname
Return eligibility preview line itemis_self_service_enabledself_service_enabled
Return inspection, its create request, and the GET /v1/return-inspections filterlocation_idinspection_location_id
POST /v1/return-policies/{return_policy_id}/revisionsexpected_current_return_policy_revision_idRemoved

Publishing a return policy revision now takes one fence: the policy's version as expected_version, which is required. Without it the request returns 400 naming expected_version. Publishing and policy updates both raise version, so it detects a concurrent publish or edit.

In the OpenAPI spec, CreditNoteLine is CreditNoteLineItem, CreditNoteLineRequest is CreditNoteLineItemInput, and the ReturnEligibilityCheck schemas are ReturnEligibilityPreview schemas.

Invoices, subscriptions, and timelines#

WhereOldNew
Invoice payment terms snapshotrevisionversion
subscription.delivery, including its webhooksrevisiondelivery_version
Resource timeline entryimage_revisionimage_version

Gift cards#

WhereOldNew
POST /v1/gift-card-redemptionsexternal_reference_idsale_reference
Gift card redemptions and their webhooksexternal_reference_idsale_reference, null for source_type: flint_order
Gift card redemptions from a Flint order's paymentexternal_reference_id, which held the payment attempt IDorder_payment_attempt_id
GET /v1/gift-card-redemptions filterexternal_reference_idsale_reference
POST /v1/gift-cards/{gift_card_id}/cash-outsexternal_reference_idpayout_reference
Gift card transactions and gift_card_transaction.createdexternal_reference_idpayout_reference, set only on cash_out transactions
GET /v1/gift-card-transactions filterexternal_reference_idpayout_reference

sale_reference is your reference for the sale a redemption pays for. payout_reference is your reference for the payout you made outside Flint, such as a check number or transfer ID. Gift cards and funding sources keep external_reference_id.

Customers#

WhereOldNew
Customer and its create and update requestsis_verifiedis_merchant_verified
Customer and its create and update requestsgroup_idsegment
Promotion rule attributecustomer.is_verifiedcustomer.is_merchant_verified
Promotion rule attributecustomer.group_idcustomer.segment
Delivery eligibility conditioncustomer_verifiedcustomer_merchant_verified
Delivery eligibility conditioncustomer_groupcustomer_segment

is_merchant_verified is a flag you set, for example after checking ID in store. Flint never sets it, and it is unrelated to customer verifications. segment is a free-form label, such as wholesale, not an ID.

Payments#

WhereOldNew
Payment method domainpayment_optionspayment_option_statuses
PaymentIntent confirm, pay payment_source and setup_payment_source, and pay payment_intents[]confirmation_tokenstripe.confirmation_token
PaymentIntent, including buyer payments and payment webhooksoriginRemoved; read payment_flow
GET /v1/payment-intents and GET /v1/me/payments filteroriginRemoved; filter by payment_flow

payment_option_statuses reports each wallet's domain status. Send a Stripe ConfirmationToken as {"stripe": {"confirmation_token": "ctoken_..."}}; errors about it name stripe.confirmation_token, prefixed by its containing object, such as payment_source.stripe.confirmation_token. payment_flow names the channel for other payments (checkout, payment_link, virtual_terminal, api), and for an invoice or subscription payment it names that flow instead (invoice, subscription_initial, subscription_renewal). To see the channel an invoice payment came through, read origin on its checkout session or order, which keep it.

Organizations and payout destinations#

WhereOldNew
Organization status and the GET /v1/organizations status filterdeletedarchived
Payout destination status, its webhooks, and the GET /v1/payout-settings/destinations status filterdeletedarchived
GET /v1/payout-settings/destinationsinclude_deletedinclude_archived

A deleted organization or payout destination stays readable by ID, so it reports archived. The payout_destination.deleted event keeps its name, and its payload reports status: archived. Categories still report deleted, because deleting one removes it.

  • Breaking change
  • API

Clear fields with null; empty strings are rejected#

Breaking: "" in any request string field returns 400 INVALID_REQUEST, with param naming the field, such as line_items[2].note. This includes enums, IDs, timestamps, URLs, and nested fields. Before, many update routes treated "" as a clear, and other fields stored it. The message says when null clears the field. The exceptions are state and postal_code in an AddressInput or OrderDeliveryDestinationAddressInput and state in a saved address, which are "" in countries that have none. Saved addresses, under /v1/me/addresses and /v1/customers/{customer_id}/addresses, require postal_code.

Breaking: a metadata value can't be "". {"metadata": {"color": ""}} returns INVALID_REQUEST with param set to metadata.color. To delete a key on an update, send {"metadata": {"color": null}}. "metadata": null still clears all of your keys.

Breaking: to clear a field, send null. Fields that used to clear on "" now clear on null, including:

  • PATCH /v1/merchant: email, phone, support_email, support_phone, support_url, and website_url. Before, null on these was ignored.
  • PATCH /v1/categories/{category_id}: description and external_reference_id. Before, null was ignored.
  • PATCH /v1/delivery-methods/{delivery_method_id}: description.
  • PATCH /v1/settings: customer_account.merchant_account_url, customer_account.presentation.account_name, customer_account.presentation.custom_domain, and each customer_account.route_templates entry; the branding colors and font_family; receipts.header_text and receipts.footer_text; the four legal URLs; tax.default_line_item_tax_category and tax.default_delivery_tax_category; and the invoices text fields.
  • PATCH /v1/bundles/{bundle_id}: a component's delivery_profile_id. Leaving it out still keeps the inherited profile.
  • PATCH /v1/payment-links/{payment_link_id}: description, inactive_message, external_reference_id, redirects.success_redirect_url, and redirects.cancel_redirect_url; theme.primary_color, theme.accent_color, and theme.title; payments.payment_note; the four legal URLs; expiration.expiration_url; custom_text.shipping_address_label and custom_text.order_summary_message; and event_config.venue and event_config.ticket_prefix.
  • PATCH /v1/products/{product_id}: default_variant_id, where the product's options allow it. PATCH /v1/products/{product_id}/variants/{variant_id}: name.
  • PATCH /v1/payout-settings: statement_descriptor.
  • POST /v1/onboarding/advance: profile.email, profile.support_email, profile.support_phone, profile.support_url, and profile.website_url.
  • PATCH /v1/orders/{order_id}: delivery_destination.address.line2, delivery_destination.recipient.name, and delivery_destination.recipient.phone. PATCH /v1/orders/{order_id}/line-items/{order_line_item_id}: gift_card_recipient.name and gift_card_recipient.message.
  • PATCH /v1/subscriptions/{subscription_id} and POST /v1/me/subscriptions/{subscription_id}/delivery: delivery.recipient.name, delivery.recipient.email, and delivery.recipient.phone. POST /v1/checkout-sessions/{checkout_session_id}/delivery-selections: recipient.name, recipient.email, and recipient.phone.
  • PATCH /v1/customers/{customer_id}: phone, internal_note, external_reference_id, segment, and default_invoice_payment_term_id.
  • PATCH /v1/customers/{customer_id}/addresses/{customer_address_id} and PATCH /v1/me/addresses/{customer_address_id}: phone and label. PATCH /v1/me: name and phone.
  • PATCH /v1/payment-intents/{payment_intent_id}: external_reference_id and receipt_email.
  • PATCH /v1/orders/{order_id}: buyer_note, internal_note, and external_reference_id.
  • PATCH /v1/invoices/{invoice_id}: memo, footer, po_number, reference, recipient_email, and external_reference_id.
  • PATCH /v1/subscription-plans/{subscription_plan_id}: description and external_reference_id.
  • PATCH /v1/webhook-endpoints/{webhook_endpoint_id}: description.

Breaking: an empty string that used to return a field-specific code, such as INVALID_CAPTURE_METHOD, INVALID_CHECKOUT_SURFACE, or INVALID_EXTERNAL_REFERENCE_ID, now returns INVALID_REQUEST.

Breaking: where a field trimmed its value and then treated an empty result as a clear, such as PATCH /v1/settings text fields, a category description, and order notes, a value that is only spaces now returns INVALID_REQUEST instead of clearing the field.

Breaking: null on a field that can't be cleared returns 400 NULL_NOT_ALLOWED on routes that ignored it before, including the merchant's business_name and reporting_timezone, a category, API key, modifier group, or modifier set name, an address recipient_name, and a location geography's timezone.

Request string fields in the OpenAPI spec publish minLength: 1, and fields you can clear are nullable, with a description that says to send null. Where a schema is shared with responses that can return "", requests use a separate copy named with an Input suffix, such as AddressInput, ThemeConfigInput, and PromotionRuleInput; response schemas don't change.

  • Breaking change
  • API
  • Webhooks

Webhook events carry their resource in data.object, and event names follow one rule#

Breaking: every webhook event's data now has one member, object: the affected resource as its GET returns it, without expansions or the few caller-specific fields the event catalog lists. This replaces the four shapes events used before (flat fields, the bare resource, a {"payment_intent": ...} style wrapper, and {"object": ...} on a few events). The envelope's payload_version is now 2. Events that carried only an ID now carry the full resource, and every event in the OpenAPI document has a typed schema: event_type is narrowed to its one value and data.object references the resource's schema.

JSON
{
  "webhook_event_id": "whev_1kmn0aExample",
  "event_type": "order.paid",
  "payload_version": 2,
  "api_version": "2026-09-07",
  "mode": "test",
  "merchant_id": "mer_1kmn0aExample",
  "created_at": "2026-07-03T14:00:00Z",
  "test": false,
  "request": null,
  "data": {
    "object": {
      "order_id": "ord_1kmn0aExample",
      "status": "closed",
      "payment_status": "paid"
    }
  }
}

The object shown is abbreviated; deliveries carry the order as GET /v1/orders/{order_id} returns it.

Fields that only existed on events are gone. Read them from the resource or from the event that owns them:

  • Transition details such as previous_status, current_status, changed_fields, previous_values, reason, cause, and initiated_by are no longer sent. Fulfillment transitions are recorded on fulfillment_event.created, whose fulfillment event carries previous_status and current_status.
  • resource_updated_at and revision copies such as inventory_level_revision are gone. Use the resource's updated_at and version.
  • subscription.payment_succeeded and subscription.payment_failed no longer carry order_id or payment_intent_id. The renewal order sends order.paid, and the payment sends payment_intent.*.
  • Invoice events no longer carry payment attempt, delivery attempt, refund, or late fee details. Payment attempts have their own events (below), late fees are in the invoice's late_fees, and delivery attempts are listed at GET /v1/invoices/{invoice_id}/delivery-attempts.
  • custom_domain.status_changed no longer carries previous_status. Custom domain status objects, including those in GET /v1/settings, now include domain_type.
  • merchant.readiness_updated carries the merchant, so merchant_environment_id is gone.
  • delivery_revocation.created carries the delivery revocation as GET /v1/delivery-revocations/{delivery_revocation_id} returns it, so its estimated impact uses active_quote_count and current_selection_count instead of active_quotes and provisional_selections.

Breaking: the envelope's test field is always present: true on synthetic events from the test-events endpoint and false on every other event. It used to be omitted unless true. Partner app envelopes carry it too.

Breaking: partner app deliveries no longer add routing fields to data. Installed-merchant deliveries used to merge source_type, mode, merchant_id, partner_app_install_id, environment_grant_id, and environment_id into data, which overwrote resource fields with the same name, such as a gift card transaction's source_type. The partner envelope now carries them in source, and data is byte-identical to the merchant event:

JSON
{
  "webhook_event_id": "whev_1kmn0aExample",
  "event_type": "customer.created",
  "payload_version": 2,
  "api_version": "2026-09-07",
  "partner_app_id": "pa_1kmn0aExample",
  "created_at": "2026-07-03T14:00:00Z",
  "test": false,
  "source": {
    "type": "installed_merchants",
    "merchant_id": "mer_1kmn0aExample",
    "partner_app_install_id": "pinst_1kmn0aExample",
    "mode": "test",
    "environment_grant_id": "egrt_1kmn0aExample",
    "environment_id": "test_1kmn0aExample"
  },
  "data": { "object": { "customer_id": "cus_1kmn0aExample" } }
}

Partner app lifecycle events carry source.type partner_app with the install's merchant_id and partner_app_install_id.

Breaking: event types are named <resource_type>.<event> with exactly one dot, where <resource_type> is the resource in data.object. These types were renamed:

Previous nameNew name
credit_note.allocation_createdcredit_note_allocation.created
credit_note.allocation_reversedcredit_note_allocation.reversed
customer.deletion_requestedcustomer_deletion_request.created
customer.deletion_completedcustomer_deletion_request.completed
customer.deletion_rejectedcustomer_deletion_request.rejected
inventory.action_requiredinventory_reservation.action_required
inventory.count.appliedinventory_count.applied
inventory.level.updatedinventory_level.updated
inventory.receipt.createdinventory_receipt.created
inventory.reservation.at_riskinventory_reservation.at_risk
inventory.reservation.closedinventory_reservation.closed
inventory.reservation.committedinventory_reservation.committed
inventory.reservation.consumedinventory_reservation.consumed
inventory.reservation.createdinventory_reservation.created
inventory.reservation.hold_expiredinventory_reservation.hold_expired
inventory.reservation.releasedinventory_reservation.released
inventory.shortage.detectedinventory_level.shortage_detected
inventory.transfer.closedinventory_transfer.closed
inventory.transfer.departedinventory_transfer.departed
inventory.transfer.lostinventory_transfer.lost
inventory.transfer.receivedinventory_transfer.received
inventory.transfer.returnedinventory_transfer.returned
invoice.payment_processinginvoice_payment_attempt.processing
invoice.payment_failedinvoice_payment_attempt.failed
invoice.payment_attempt_canceledinvoice_payment_attempt.canceled
invoice.payment_attempt_expiredinvoice_payment_attempt.expired
merchant.readiness.updatedmerchant.readiness_updated
order.fulfillment.createdfulfillment.created
order.fulfillment.updatedfulfillment.updated
order.fulfillment.completedfulfillment.completed
order.fulfillment.status_changedfulfillment.status_changed
order.fulfillment.event.createdfulfillment_event.created
order.fulfillment.shipment.createdshipment.created
order.fulfillment.shipment.updatedshipment.updated
order.fulfillment.package.createdpackage.created
order.fulfillment.package.updatedpackage.updated
order.inventory_exception.createdorder.inventory_exception_created
order.inventory_exception.resolvedorder.inventory_exception_resolved
payment_intent.fulfillment_hold.updatedpayment_intent.fulfillment_hold_updated
payout_destination.deletedpayout_destination.archived
partner_app.install.createdpartner_app_install.created
partner_app.install.updatedpartner_app_install.updated
partner_app.install.permissions_updatedpartner_app_install.permissions_updated
partner_app.install.revokedpartner_app_install.revoked
partner_app.install.environment_grant.createdenvironment_grant.created
partner_app.install.environment_grant.revokedenvironment_grant.revoked

The invoice payment attempt events now carry the payment attempt, including failure_code and failure_message, instead of the invoice.

Breaking: two duplicate events were removed:

  • order.inventory_action_required was always sent with order.inventory_exception.created. Use order.inventory_exception_created.
  • delivery_rate.created, delivery_rate.updated, and delivery_rate.archived were always sent with delivery_method.created or delivery_method.updated, whose delivery method includes its rates. Use the delivery method events.

Events Flint queued before this change and delivers afterwards arrive with payload_version: 1, their previous data shape, and their new event name, so switch on payload_version while those drain. Webhook endpoints still accept the previous names in enabled_events for one release. Flint stores and returns the new name, removed events map to the event that replaces them, and existing endpoints subscribed to a previous name receive the new event. Deliveries use only the new names, so update handlers that switch on event_type.

  • Breaking change
  • API
  • Customers

Buyer lists accept only buyer-visible filters, and rejected customer sessions return 401#

Each /v1/me list now accepts an explicit set of filters on fields the buyer can see. Other filters return 400 with UNSUPPORTED_QUERY_PARAM. The removed filters are:

  • GET /v1/me/orders: external_reference_id, return_id, return_resolution_id
  • GET /v1/me/payments: external_reference_id, risk_level, dispute_status, return_id, return_resolution_id, query
  • GET /v1/me/refunds: payment_intent_id, reason, refund_method, external_reference_id, return_resolution_id, query
  • GET /v1/me/fulfillments: external_reference_id, location_id, query
  • GET /v1/me/shipments and GET /v1/me/packages: external_system, external_reference_id, query
  • GET /v1/me/invoices: external_reference_id, query
  • GET /v1/me/subscriptions: awaiting_billing_schedule, subscription_offer_id
  • GET /v1/me/returns: external_reference_id, query, receiving_location_id, resolution_type, work_type

query on GET /v1/me/orders searches the order ID, order number, buyer note, and line item names. It no longer matches the merchant note, the external reference ID, or the buyer email.

Payments read with a customer session no longer include dispute_status or settlement_status, so a buyer can't tell that a payment was disputed. GET /v1/me/payments now documents its items as BuyerPaymentIntent, which also omits the fee, net, and risk fields buyers never received.

Refunds read with a customer session now include return_id, so GET /v1/me/refunds?return_id= filters on a field the buyer receives.

Draft invoices are no longer visible to buyers. GET /v1/me/invoices leaves them out and rejects status=draft, and getting a draft invoice, its PDF, or its credit notes returns 404.

A /v1/me request whose customer session secret is malformed, unknown, or revoked now returns 401 with INVALID_CUSTOMER_SESSION instead of 404 with CUSTOMER_SESSION_NOT_FOUND. Treat it as a sign-out: authenticate the buyer again and create a new customer session. An expired secret still returns 401 with CUSTOMER_SESSION_EXPIRED. See Customer sessions.

POST /v1/me/subscriptions/{subscription_id}/payment-method with another customer's payment method now returns 404 with PAYMENT_METHOD_NOT_FOUND, before any check of that payment method's status. With an API key, another customer's payment method still returns PAYMENT_METHOD_CUSTOMER_MISMATCH, now also before status checks. On both routes, a payment method that doesn't exist returns PAYMENT_METHOD_NOT_FOUND with param set to payment_method_id, instead of RESOURCE_NOT_FOUND.

POST /v1/me/email-change-requests counts every attempt toward its limit of three per 15 minutes, including attempts refused because the address is already used. Over the limit, it returns 429 with EMAIL_CHANGE_RATE_LIMITED and a Retry-After header.

POST /v1/customer-sessions/{customer_session_id}/revoke now returns 404 for a session in another sandbox, matching revoke-all.

Update, 2026-10-23: Customer session authentication failures now return 401 INVALID_CREDENTIAL. details[0].reason is unknown for a malformed, unknown, or revoked customer session secret, and expired for an expired secret. Re-authenticate the buyer for unknown; refresh the session and retry for expired. These current codes replace the authentication codes described above. See Customer sessions.

  • Breaking change
  • API
  • Catalog

Catalog updates handle null consistently and reject changes to archived products#

Breaking: on PATCH for products, variants, bundles, devices, modifier groups, and modifier sets, null clears an optional field, such as description, sku, barcode, modifier_set_id, or metadata. null on a required field or a list now returns 400 NULL_NOT_ALLOWED with the field in param. Before, null on some fields was ignored, and null on options, categories, and components returned INVALID_OPTIONS, INVALID_CATEGORIES, or INVALID_COMPONENTS.

Breaking: metadata: null on a modifier group or modifier set now clears its metadata. Before, it was ignored.

Breaking: PATCH /v1/modifier-groups/{modifier_group_id} changes only the text settings you send. Before, sending {"text": {"max_length": 80}} reset required, multiline, and min_length. null on text.min_length or text.max_length clears it, and an empty text object returns 400 FIELD_REQUIRED.

Breaking: metadata limits apply to the result of a PATCH, after your changes are merged into the stored keys, on products, variants, bundles, categories, devices, and Locations. A patch that would leave more than 50 keys returns 400.

Breaking: PATCH /v1/products/{product_id} on an archived product returns 409 PRODUCT_ARCHIVED with current_status: archived. Before, it returned 404.

Empty categories, selected_options, components, modifiers, and modifier_groups arrays are now returned as [] instead of being left out. DELETE responses for products, variants, and bundles include modifier_set_id like GET does.

  • API
  • Customers
  • Errors

Duplicate customer emails name the existing customer#

POST /v1/customers with an email that another customer in the same environment already uses still returns 409 CUSTOMER_EMAIL_ALREADY_USED with param set to email. The error now also includes blocking_resources with that customer's ID, such as [{"resource_type": "customer", "resource_id": "cus_..."}], and blocking_resource_count of 1, so you no longer need GET /v1/customers?email= to find it.

Only merchant credentials receive blocking_resources. Buyer email changes through /v1/me/email-change-requests return the same code without naming the other customer. See Headless storefront.

  • API
  • Customers

Customer session secrets end with their refresh token#

A customer session's secret could outlast its refresh_token. A session created with a 24 hour expires_in_seconds and a 1 hour refresh_expires_in_seconds had a secret valid for 24 hours, and a refresh just before the refresh deadline issued a secret valid past it.

POST /v1/customer-sessions and POST /v1/customer-sessions/refresh now end the secret no later than refresh_token_expires_at, and expires_at returns that earlier time. When the refresh token ends, the buyer signs in again, as the refresh window promises.

INVALID_CUSTOMER_SESSION errors for an out-of-range lifetime now name expires_in_seconds, refresh_expires_in_seconds, and account_url_expires_in_seconds, the fields you send. See Customer sessions.

  • Breaking change
  • API
  • Devices

Archived devices report archived and release their fingerprint#

Breaking: a device retired with DELETE /v1/devices/{device_id} now reports status: archived instead of deleted, and GET /v1/devices?status=archived lists archived devices. PATCH on an archived device returns 409 DEVICE_ARCHIVED with current_status: archived, replacing DEVICE_DELETED.

Archiving a device releases its hardware_fingerprint, which the archived device no longer returns. POST /v1/devices with that fingerprint creates a new device instead of returning the archived one. When two registrations with the same fingerprint race, both return the same device, one with already_existed: true.

Breaking: a location_id that doesn't match a location now returns 404 RESOURCE_NOT_FOUND on device create and update, and a malformed location_id on create returns 400. Before, both returned 409 LOCATION_UNAVAILABLE, which now means only that the location isn't active.

Breaking: the status and sort_by filters on GET /v1/devices match exactly. Values in another letter case or with surrounding spaces return 400.

  • Breaking change
  • API
  • Disputes
  • Risk

Dispute field names, review refunds, and risk previews#

Breaking: dispute fields are renamed in API responses and dispute.* webhooks:

BeforeAfter
evidence_deadline_passedis_evidence_deadline_passed
evidence_response_allowedcan_respond_with_evidence
evidence_submission_past_dueis_evidence_submission_past_due
action_requiredis_action_required

Disputes no longer include metadata, which was always empty.

Breaking: reviews report their refund as refunded_money instead of refunded_amount_money, in API responses and review.* webhooks. GET /v1/reviews also accepts created_before.

Breaking: POST /v1/risk-previews returns 200 for a rule with problems, with valid: false, analysis: null, and every problem in issues. It previously returned a 400 for the first problem only. Malformed requests still return 400. Previews now need only risk.read.

Risk lists include origin and editable, like rules. Flint's default lists have editable: false: renaming one now returns LIST_RESERVED, as archiving one already did. You can still add and remove their values.

Reusing an Idempotency-Key on a risk rule, list, or list item route for a different rule, list, or item now returns an idempotency conflict instead of replaying the first response.

  • API
  • Settings
  • Promotions

Effective settings include promotion defaults#

GET /v1/settings/effective now returns all three promotions fields. Unset values default to automatic_enabled: true, codes_enabled: true, and max_promotions_per_order: 2. Existing promotion settings and legacy coupon settings continue to apply.

GET /v1/settings continues to return stored values without filling in promotion defaults.

  • Breaking change
  • API
  • Fulfillment
  • Delivery

Fulfillment command results, external references, and delivery configuration lists#

Breaking: external_system and external_reference_id no longer deduplicate shipments and packages. A second create with the same reference makes a second shipment or package, and an update can reuse a reference. To retry a create safely, send the same Idempotency-Key. The replayed field and the FULFILLMENT_SHIPMENT_PROVIDER_REFERENCE_CONFLICT and SHIPMENT_PACKAGE_PROVIDER_REFERENCE_CONFLICT errors are gone.

Breaking: fulfillment, shipment, and package commands that produce events or emails return data as the resource plus events, fulfillment_notifications, and unchanged, all always present. The event array is always named events, replacing event on transitions and voids and fulfillment_event on package create and update. Package transitions no longer return package_status_update. Commands without side effects return the resource itself in data:

Routedata
PATCH /v1/fulfillments/{fulfillment_id}The fulfillment. It previously returned the order
POST /v1/fulfillments/{fulfillment_id}/shipmentsThe shipment
PATCH /v1/shipments/{shipment_id}The shipment

Breaking: fulfillment transitions name every action's time occurred_at. complete takes occurred_at instead of completed_at, and cancel accepts it too. hold and mark_no_show accept reason_message beside reason, shown as active_hold.reason_notes and outcome.notes. When you leave out expected_version and another change lands first, FULFILLMENT_CHANGED, SHIPMENT_CHANGED, and PACKAGE_CHANGED no longer name expected_version in param.

Breaking: GET /v1/fulfillment-events filters event_type by the values events report: status_changed and the carrier and tracking event types.

Breaking: delivery profile, zone, location set, method, and rate callback lists leave archived resources out unless you send status=archived or include_archived=true. A DELETE on an archived resource returns 200 with the resource, and any other change returns 409 DELIVERY_STATE_CONFLICT with current_status. Profiles, zones, and location sets publish active and archived as their statuses; methods and rate callbacks add inactive. revoked is no longer listed, because no configuration resource reaches it.

Breaking: POST /v1/delivery-rate-callbacks/{delivery_rate_callback_id}/rotate-secret and /check-connection return 200 instead of 201. Delivery revocations name signing-key targets delivery_rate_callback_signing_key and take expected_version at the top level of the request. Delivery previews drop selection_authority, always include merchant_diagnostics for your API key, and reject a currency that isn't an uppercase ISO code. Delivery methods always include display_position and type.

GET /v1/delivery-quotes/{delivery_quote_id} reads one issued quote. GET /v1/delivery-quotes validates checkout_session_id and order_id and parses created_after and created_before as RFC 3339 timestamps.

  • Breaking change
  • API
  • Gift-cards
  • Customers

Gift card command responses, funding limit errors, and customer null handling#

Breaking: these gift card and customer changes rename fields and change response shapes.

  • Gift card writes return the resource they created or changed in data instead of a command wrapper. Card updates and transitions return the gift card. Issuance and code rotation return the gift card with code and secret_recovery_expires_at. Loads and redemptions return the load or redemption with the updated card in gift_card, and capture and cancel return the redemption the same way. Adjustments and cash-outs return the gift card with gift_card_transaction_ids, now with status 200. Notification create and cancel return the notification. See what each command returns.
  • The funding source field reference_id is now external_reference_id, on requests, loads, refund provenance, and webhooks.
  • GET /v1/gift-card-loads filters with funding_source_type instead of source_type and source_external_reference_id instead of source_id. GET /v1/gift-card-transactions rejects unknown source_type values.
  • POST /v1/orders/{order_id}/gift-cards takes the gift card code as code instead of gift_card_code, matching POST /v1/gift-cards/lookup.
  • Redemptions include source_type, and automatic redemptions return expires_at: null.
  • A load, import, or adjustment over a funding limit returns 409 GIFT_CARD_FUNDING_LIMIT_EXCEEDED with the limit in reason and maximum_money, instead of GIFT_CARD_STATE_CONFLICT.
  • A stale expected_version on a gift card command returns 409 GIFT_CARD_VERSION_CONFLICT with current_version. Other gift card state conflicts include current_status.
  • An unexpected gift card failure returns 500 INTERNAL_ERROR instead of 503 GIFT_CARDS_UNAVAILABLE.
  • Replaying an issuance or code rotation after the code was replaced omits code, because the old code no longer works.
  • On PATCH /v1/customers/{customer_id}, null clears name, phone, internal_note, external_reference_id, and group_id, and returns 400 NULL_NOT_ALLOWED on tax_exempt and is_verified. Previously, null was ignored.
  • Customer deletion requests return 409 for state conflicts, with the request's current_status: CUSTOMER_DELETION_REQUEST_NOT_PENDING, CUSTOMER_DELETION_ALREADY_APPROVED, and CUSTOMER_DELETION_STATE_CHANGED. These were 400 INVALID_CUSTOMER_ACCOUNT_REQUEST. The status filter and decision must match their values exactly; values with other letter case or surrounding spaces, such as Approve, now return 400.

These changes need no action unless you use one of them:

  • GET /v1/gift-card-funding-dispositions/{gift_card_funding_disposition_id} and GET /v1/gift-card-funding-dispositions retrieve and list funding dispositions, which now include idempotency_key.
  • Gift card validation errors name the field in param. External references count characters, not bytes, and are stored exactly as sent.
  • Capturing or canceling a redemption and canceling a notification accept an empty body.
  • POST /v1/email-preference-links/lookup is a read: it no longer stores or replays results by Idempotency-Key.
  • Breaking change
  • API
  • Inventory

Inventory errors name the field, and every inventory resource reads by ID#

Breaking: several inventory errors change code or status so the error names what went wrong.

  • A command that references a missing resource returns that resource's not-found code with the field in param. Creating a transfer with an unknown item returns 404 INVENTORY_ITEM_NOT_FOUND for lines[0].inventory_item_id instead of INVENTORY_TRANSFER_NOT_FOUND. An unknown Location returns 404 LOCATION_NOT_FOUND instead of 409 INVENTORY_LOCATION_NOT_CONFIGURED, and an inactive or archived Location returns 409 LOCATION_NOT_ACTIVE. Reservation and transfer lines that aren't on the resource return 404 INVENTORY_RESERVATION_LINE_NOT_FOUND or 404 INVENTORY_TRANSFER_LINE_NOT_FOUND, and an unknown policy version returns 404 INVENTORY_POLICY_VERSION_NOT_FOUND.
  • An inactive or archived allocation policy returns 409 INVENTORY_POLICY_UNAVAILABLE with its current_status, instead of INVENTORY_ITEM_UNAVAILABLE.
  • Validation errors carry param and a message that names the field. Request shape problems that previously returned a resource code because of the route, such as an invalid ID on a transfer route, return 400 VALIDATION_ERROR. INVENTORY_POLICY_INVALID, INVENTORY_TRANSFER_INVALID, and INVENTORY_RECEIPT_INVALID cover only the policy configuration, transfer body, and receipt line problems they describe.
  • Conflicts with current state return 409 instead of 400: an adjustment that would take stock below zero returns INVENTORY_INSUFFICIENT (previously 400 INVENTORY_CONDITION_INVALID), applying a count with unobserved lines returns INVENTORY_COUNT_INCOMPLETE, a transfer edit that changes a departed line, removes a line with canceled quantity, or sets requested_quantity below the canceled quantity returns INVENTORY_TRANSFER_LINE_CONFLICT, transfer targets the line can't accept return INVENTORY_QUANTITY_CONFLICT, and a receipt above the consumed quantity returns INVENTORY_RECEIPT_CONFLICT.
  • A transition on a closed reservation returns 409 INVENTORY_RESERVATION_CLOSED with closed_reason, instead of INVENTORY_RESERVATION_EXPIRED. commit after owner.expires_at returns 409 INVENTORY_RESERVATION_EXPIRED.
  • Reservation transition lines accept only their action's target fields, and null on a field that can't be cleared, such as an item's name, returns 400 NULL_NOT_ALLOWED instead of being ignored.

Breaking: has_available_quantity, has_unavailable_condition, has_shortage, and has_at_risk_quantity set to false now select the resources without that condition. Previously false was ignored. The movement list's type and reason filters reject values a movement can't carry.

Resending targets that are already applied now succeeds on a closed reservation or a fully departed or closed transfer. A receive without a condition split keeps the split recorded earlier and adds the new quantity to the line's condition. Scalar list filters that hold free-form identifiers, such as sku, barcode, and external_reference_id, accept values containing commas. Item and policy metadata limits apply to the merged result of a PATCH. A lock timeout no longer returns a 409; Flint retries it, and a failure that remains returns 500. A 409 or 404 from an inventory PATCH or DELETE no longer locks its Idempotency-Key, so a retry runs once the state changes. The OpenAPI description now lists provenance as optional on reservation consume and transfer transitions, as Flint already accepted; without it, movements record the time Flint received the request and a null source_system.

New routes read one inventory resource by ID: GET /v1/inventory-levels/{inventory_level_id}, GET /v1/inventory-movements/{inventory_movement_id}, GET /v1/inventory-adjustments/{inventory_adjustment_id}, GET /v1/inventory-reservations/{inventory_reservation_id}, GET /v1/inventory-receipts/{inventory_receipt_id}, GET /v1/inventory-counts/{inventory_count_id}, and GET /v1/inventory-transfers/{inventory_transfer_id}. The reservation route also reads reservations Flint holds for orders and checkout, which are read-only.

  • Breaking change
  • API
  • Locations

Location errors name the field, and country codes must be uppercase#

LOCATION_INVALID and LOCATION_GEOGRAPHY_INVALID errors now set param to the field at fault, such as address.country, timezone, or metadata, and the message says what to send. Before, most of them said only that the request was invalid.

Breaking: address.country must be an uppercase ISO 3166-1 alpha-2 code, such as US. A lowercase code or one with surrounding spaces returns 400 LOCATION_GEOGRAPHY_INVALID instead of being corrected.

Breaking: Location metadata uses the same limits as other resources, checked after a PATCH is merged into the stored keys. A violation returns 400 LOCATION_INVALID with param: metadata.

coordinate.latitude and coordinate.longitude are always returned, including when the value is 0.

  • Breaking change
  • API
  • Orders
  • Invoices

Manual discount scope must match discount_class#

Breaking: manual discounts that explicitly send scope: order with discount_class: line_item, or scope: line_item with discount_class: order, now return 400 INVALID_DISCOUNT_SCOPE. Previously, Flint accepted these conflicting pairs and let discount_class win.

This applies when creating an order, applying a discount, previewing a discount, or creating an invoice with quick_pay.discounts. The error's param identifies the manual discount's scope field in the request. Omit scope and send discount_class.

Matching pairs, requests that send only one field, and scope: order with discount_class: service_charge keep their existing behavior. scope: line_item with discount_class: service_charge still returns SERVICE_CHARGE_DISCOUNT_SCOPE_INVALID.

  • Breaking change
  • API
  • Payouts
  • Webhooks

Analytics stays in one currency, and payouts and balances match their webhooks#

Breaking: these analytics, money movement, report, and Flint billing changes apply now.

Analytics#

  • Overview and payment volume amounts are always in one currency, returned as currency. Omit the currency query parameter to get your account's default currency. Before, an omitted currency added every currency together.
  • active_subscriptions on the overview is replaced by new_subscriptions: subscriptions created in the window that are now trialing, active, paused, or past_due.
  • Payment volume buckets rename period_start and period_end to period_start_at and period_end_at.
  • An invalid timezone returns 400 INVALID_TIMEZONE, and an unsupported query parameter returns 400 UNSUPPORTED_QUERY_PARAM. range accepts and returns last_90_days.
  • currencies and the subscription analytics money lists are [] when empty instead of missing.

Payouts and balances#

  • Payout.arrival_at is now arrival_date, a calendar date such as 2026-10-17, on REST responses, expanded payouts, and payout webhooks.
  • Payout entries rename amount_money to net_money, the entry's effect after fees.
  • Payout webhooks and the API share one set of values. fee_amount_status is estimated, final, or not_applicable, never unknown. method can be standard, instant, or other. initiated_by, payout settings interval, payout destination type, and balance transaction type can be other for a value Flint doesn't map.
  • balance.updated carries the same Balance object as GET /v1/balances and no longer includes instant payout fields. Balances add observed_at, and the list response documents next_page_token.
  • POST /v1/payouts/{payout_id}/cancel for a payout that doesn't exist returns 404 PAYOUT_NOT_FOUND instead of 409. PAYOUT_NOT_CANCELABLE includes error.current_status.
  • Payout settings created_at and updated_at are null when Flint has no stored settings, instead of the request time. payout_settings_id differs between environments.
  • available_by_source_type now adds up to available_money after active payout holds.

Reports and Flint billing#

  • gift_card_liability_v1 with a currency other than USD returns 400 REPORT_CURRENCY_NOT_SUPPORTED instead of INVALID_CURRENCY. Malformed report request bodies return the standard JSON body errors.
  • Merchant subscription invoices rename period_start and period_end to period_start_at and period_end_at, including in invoice webhooks.
  • A malformed or unknown ID returns 404 MERCHANT_SUBSCRIPTION_INVOICE_NOT_FOUND or 404 MERCHANT_BILLING_BALANCE_NOT_FOUND, instead of 400 INVALID_RESOURCE_ID or 404 RESOURCE_NOT_FOUND.
  • Breaking change
  • API
  • Orders

Order field renames, revision checks, and null handling#

Breaking: these order changes rename fields and tighten validation.

  • The order's close note is now closed_reason_message, matching the reason_message you send to POST /v1/orders/{order_id}/close. closed_reason is no longer returned on orders.
  • GET /v1/orders/{order_id}/activities filters with activity_type instead of type, matching the field on each activity. An empty entry such as activity_type=payment,,refund returns 400 INVALID_ACTIVITY_TYPE.
  • GET /v1/orders sorts by order total with sort_by=total_money instead of sort_by=total. The status filter accepts repeated or comma-separated values, like the other status filters.
  • DELETE /v1/orders/{order_id}/gift-cards/{gift_card_id} takes order_revision as a required query parameter and no longer accepts a body.
  • On PATCH /v1/orders/{order_id}, null clears buyer_note, internal_note, and external_reference_id, and returns 400 NULL_NOT_ALLOWED on fields that can't be cleared, such as customer_id. Line item and charge updates follow the same rule: null clears description and metadata and is rejected on other fields. Previously, null on these fields was ignored.
  • An order's payment_status now follows the same rule as the payment_status filter: an order is paid once its collection completes.
  • reason_message on close is limited to 500 characters (400 INVALID_ORDER_CLOSE_REASON), and buyer_note and internal_note count their 1,000-character limit in characters.
  • Writing or deleting a metadata key Flint reserves for order bookkeeping returns 400 RESERVED_METADATA_KEY, which names the key. metadata: null now clears your keys and keeps Flint's instead of failing.
  • Order responses always include their arrays, as [] when empty, and gift_card_tender_enabled as false.

These changes need no action unless you use one of them:

  • A request that changes only metadata, buyer_note, internal_note, or external_reference_id now works on closed and paid orders, and on orders with an invoice, and no longer ends an open checkout session. It still returns 409 while a payment is in progress.
  • order_revision is accepted on line item adds and removals, charge writes, discount apply, remove, and reprice, and close, in the body or as a query parameter on DELETE. A stale revision returns 409 ORDER_CHANGED_REFRESH_REQUIRED. It is now optional when selecting a gift card.
  • POST /v1/orders/{order_id}/close accepts an empty body.
  • ORDER_CLOSED errors include the order's current_status.
  • Sending both payment_source_selection and payment_options to POST /v1/orders/{order_id}/payment-intents returns 400 PAYMENT_SOURCE_SELECTION_CONFLICT, as documented.
  • Breaking change
  • API
  • Payments
  • Refunds

Payment intent, payment method, and refund request fixes#

Breaking: payment intents no longer include settlement_status. Read refund_status and dispute_status instead.

Breaking: DELETE /v1/payment-methods/{payment_method_id} returns the removed payment method, with status: "removed", instead of {"success": true}. Removing a payment method that is already removed returns it again.

Breaking: an explicit empty string is rejected for capture_method when creating a payment intent with POST /v1/payment-intents or POST /v1/orders/{order_id}/payment-intents, refund_method when creating a refund, and type when saving a payment method. Leave the field out to use its default. capture_method is no longer trimmed on either route, so " manual " returns INVALID_CAPTURE_METHOD.

PATCH /v1/payment-intents/{payment_intent_id} accepts null to clear tip_money, external_reference_id, and receipt_email. customer_id and amount_money can't be cleared and return NULL_NOT_ALLOWED.

receipt_email must be a valid email address on create and update, or the request returns INVALID_EMAIL_FORMAT.

Metadata limits on payment intent and refund updates are checked against the metadata after the update, not only the keys you send.

POST /v1/payment-intents/{payment_intent_id}/confirm accepts an empty body, or {}, to continue with the payment source already attached. An empty string in payment_source_token, payment_method_id, or confirmation_token returns 400. null in any of those fields, or a null body, returns NULL_NOT_ALLOWED.

If a payment needs buyer authentication but Flint can't produce its client action yet, confirm and GET now return the payment intent with current_payment_action: null instead of 500 PAYMENT_CLIENT_ACTION_UNAVAILABLE. Retrieve the payment intent again to get the action.

GET /v1/refunds rejects empty and unknown values in reason, like other multi-value filters.

  • Breaking change
  • API
  • Promotions

Promotion updates change only the subfields you send#

Breaking: PATCH /v1/promotions/{promotion_id} now updates application_method, schedule, and exclusivity by subfield. Subfields you leave out keep their current value. Before, sending any of these objects replaced it, so sending only amount_off_money reset allocation to the class default, and sending only schedule.ends_at removed starts_at and started a scheduled promotion early.

  • To change a promotion's effect, send application_method.type with the new effect's fields. discounted_item_rules carries over unless you send new rules or null. Sending another effect's field without type returns 400 INVALID_APPLICATION_METHOD.
  • null clears optional subfields, such as schedule.starts_at, exclusivity.priority, and application_method.discounted_item_rules. null on a required field returns 400 NULL_NOT_ALLOWED, and an empty application_method, schedule, or exclusivity object returns 400 FIELD_REQUIRED.
  • PATCH on an archived promotion returns 409 PROMOTION_ARCHIVED with current_status: archived. Before, it returned 404.

GET /v1/promotions/{promotion_id}/codes/{promotion_code_id} returns one promotion code. A deleted code, including the codes of an archived promotion, returns 404.

See updating a promotion for the full rules.

  • API
  • Orders
  • Promotions

Promotion removal warnings reflect the returned order#

Applying a manual discount or promotion with POST /v1/orders/{order_id}/discounts no longer returns a promotion_removed warning for an automatic promotion that is still pending on the returned order.

  • Breaking change
  • API
  • Returns
  • Credit-notes

Returns and credit note errors, fences, and a preview rename#

Breaking: these Returns and credit note changes rename a field and change error codes and statuses.

  • Return resolution previews return based_on_return_version instead of based_on_return_revision.
  • A buyer request on a customer session that sets a field only you can set, such as external_reference_id, metadata, or a preview's pricing_basis, returns 400 RETURN_FIELD_NOT_ALLOWED_FOR_BUYER with param naming the field. A request the return policy doesn't open to self-service returns 409 RETURN_SELF_SERVICE_NOT_ALLOWED. Both replace 403 RETURN_DECISION_SCOPE_REQUIRED.
  • Returns validation errors carry param with the field path, such as line_items[0].requested_quantity, and messages no longer start with "return request validation failed" or "return request conflicts with current state". null on a field that can't be cleared returns NULL_NOT_ALLOWED, an unknown field returns UNKNOWN_FIELD, an unsupported enum value returns INVALID_ENUM_VALUE with the accepted values, a bad expected_version returns INVALID_EXPECTED_VERSION, and a bad timestamp filter returns INVALID_TIMESTAMP.
  • PATCH /v1/return-policies/{return_policy_id} and PATCH /v1/return-reasons/{return_reason_id} on an archived policy or reason return 409 RETURN_ACTION_NOT_ALLOWED with current_status: archived. Previously a name or metadata change was applied.
  • Retrying a resolution whose Return line was canceled or is otherwise no longer approved returns 409 RETURN_ACTION_NOT_ALLOWED with the line's current_status. An approved line without enough quantity left still returns 409 RETURN_VALUE_EXCEEDED.
  • POST /v1/return-resolutions/{return_resolution_id}/checkout-session and its /v1/me route return 200 when they return the open checkout session again (reused_existing: true), and 201 only when they create one.
  • Credit note reason and the status filter match exactly: Goodwill or goodwill returns INVALID_CREDIT_NOTE_REASON. reason: null returns 400 NULL_NOT_ALLOWED.
  • Credit note line errors are specific: CREDIT_NOTE_INVOICE_LINE_NOT_FOUND for a line that isn't on the invoice, CREDIT_NOTE_CURRENCY_MISMATCH for an amount in another currency, and CREDIT_NOTE_LINE_EXCEEDS_INVOICE_LINE for a correction above the source line, each with param naming the field.

These changes need no action unless you use one of them:

  • PATCH /v1/returns/{return_id} accepts expected_version, and DELETE /v1/returns/{return_id}/line-items/{return_line_item_id} accepts it as a query parameter. Receipt line verification and inspection line decisions compare expected_version with the parent Return's version, and their 409 RETURN_VERSION_CONFLICT includes the Return's current version.
  • RETURN_ACTION_NOT_ALLOWED, CREDIT_NOTE_NOT_DRAFT, and CREDIT_NOTE_NOT_ISSUED include the blocking resource's current_status, and the credit note messages say which operation the status blocks.
  • Return policy supported_actions now lists activate, deactivate, and archive, and Return reason supported_actions lists archive, when your key can take them. Every Returns supported_actions array publishes its values in the OpenAPI spec.
  • Each Returns operation's x-flint-error-codes lists the codes it can return, including RETURN_NOT_FOUND in place of RESOURCE_NOT_FOUND.
  • Breaking change
  • API
  • Orders
  • Promotions

Archived promotions use the disabled decline reason#

Breaking: applying an archived promotion with POST /v1/orders/{order_id}/discounts, or sending one in discounts on POST /v1/orders or in quick_pay.discounts on POST /v1/invoices, now returns 409 PROMOTION_DECLINED with reason: disabled and current_status: archived. Before, these requests returned PROMOTION_NOT_REDEEMABLE.

PROMOTION_NO_ACTIVE_CODES is retired; no request returns it.

Money thresholds now decline with structured reasons on every money attribute. An unmet order.merchandise_subtotal_before_discounts minimum returns minimum_not_met with required_money, current_money, and gap_money, the same as order.subtotal. A money threshold with no amount for the order's currency returns currency_mismatch for every money attribute. Before, both returned not_eligible.

  • Breaking change
  • API
  • Catalog
  • Checkout
  • Subscriptions

Catalog, checkout, subscription, and account state conflicts return 409#

Requests that are valid on their own but blocked by current catalog, payment link, checkout, subscription, or account state return 409 conflict_error. Lifecycle conflicts include error.current_status when the blocking resource has a public status, using that status value. Request shape and immutable reference mismatches continue to return 400 validation_error.

The following codes move their remaining 400 responses to 409:

  • ACTIVE_BUNDLE_COMPONENTS_IMMUTABLE
  • ACTIVE_OPTION_SELECTOR_COLLAPSE
  • ACTIVE_VARIANTS_REQUIRE_OPTION_BACKFILL
  • AMOUNT_NOT_ADJUSTABLE
  • API_KEY_ALREADY_REVOKED
  • BUNDLE_COMPONENT_PRODUCT_NOT_ACTIVE
  • BUNDLE_COMPONENT_VARIANT_NOT_ACTIVE
  • BUNDLE_IN_ACTIVE_PAYMENT_LINK
  • BUNDLE_IN_ACTIVE_SUBSCRIPTION
  • BUNDLE_IN_ACTIVE_SUBSCRIPTION_PLAN
  • BUNDLE_IN_OPEN_CHECKOUT_SESSION
  • BUNDLE_IN_OPEN_INVOICE_PAYMENT_ATTEMPT
  • BUNDLE_IN_OPEN_ORDER
  • BUNDLE_NOT_SELLABLE
  • CAPABILITIES_IMMUTABLE
  • CATALOG_INVENTORY_RELATIONSHIP_INVALID
  • CHECKOUT_CREDENTIAL_CHANGED
  • CHECKOUT_CUSTOMER_NOT_SET_ON_ORDER
  • CHECKOUT_SESSION_NOT_OPEN
  • COUNTRY_IMMUTABLE
  • CUSTOMER_VERIFICATION_NOT_CONFIRMED
  • CUSTOMER_VERIFICATION_NOT_OFFERED
  • CUSTOM_DOMAIN_NOT_VERIFIED
  • DEFAULT_ENVIRONMENT_REQUIRED
  • IMAGE_ASSET_NOT_ATTACHABLE
  • INVENTORY_ITEM_INACTIVE
  • MERCHANT_ACCOUNT_DOMAIN_NOT_VERIFIED
  • MERCHANT_ACCOUNT_SESSION_TARGETED_REMEDIATION_UNAVAILABLE
  • MERCHANT_ACCOUNT_SESSION_UNMAPPABLE_REQUIREMENT_ID
  • ORGANIZATION_CYCLE
  • PAYMENT_METHOD_NOT_READY
  • PLAN_NOT_ACTIVE
  • QUANTITY_NOT_ADJUSTABLE
  • RULE_LIMIT_EXCEEDED
  • SUBSCRIPTION_DELIVERY_LINES_NOT_COMBINABLE
  • SUBSCRIPTION_DELIVERY_PROFILE_ACTION_REQUIRED
  • SUBSCRIPTION_DELIVERY_PROFILE_MISSING
  • SUBSCRIPTION_DELIVERY_UNAVAILABLE
  • SUBSCRIPTION_NEXT_BILLING_AT_BEFORE_PERIOD_START
  • SUBSCRIPTION_NEXT_BILLING_AT_TOO_FAR
  • SUBSCRIPTION_OFFER_PROMOTION_UNAVAILABLE
  • VARIANT_IN_ACTIVE_BUNDLE
  • VARIANT_IN_ACTIVE_PAYMENT_LINK
  • VARIANT_IN_ACTIVE_SUBSCRIPTION
  • VARIANT_IN_ACTIVE_SUBSCRIPTION_PLAN
  • VARIANT_IN_OPEN_CHECKOUT_SESSION
  • VARIANT_IN_OPEN_INVOICE_PAYMENT_ATTEMPT
  • VARIANT_IN_OPEN_ORDER
  • VARIANT_NOT_SELLABLE

Deleted organization references now return 409 ORGANIZATION_NOT_ACTIVE with current_status: deleted. Updating a revoked API key returns 409 API_KEY_ALREADY_REVOKED with current_status: revoked; authentication continues to return 401 API_KEY_REVOKED.

Risk predicates exceeding 100 nodes or 10 nesting levels return 400 INVALID_PREDICATE; exceeding the enabled rule count remains 409 RULE_LIMIT_EXCEEDED. Blank targeted requirement IDs return 400 VALUE_REQUIRED with the indexed targeted_requirement_ids parameter. A merchant with no default live environment returns 409 DEFAULT_ENVIRONMENT_REQUIRED.

  • Breaking change
  • API
  • Orders
  • Tax

A charge's tax category decides its automatic tax#

Automatic tax now decides whether it can calculate a taxable order charge from the charge's tax.charge_tax_category, not its type. Charges in shipping or delivery are taxed at the general rate where the order goes, whatever their type, so an other charge or a paid pickup in the delivery category is taxed. type still describes what the fee is for.

A charge that leaves out charge_tax_category takes it from type: delivery_fee is delivery, shipping_fee is shipping, service_fee is service_fee, and handling_fee is handling. Other types take no category. The default isn't stored, so responses still omit the field and the category follows a later change to type. Existing shipping and delivery fees without a category keep their tax.

Breaking: a taxable charge whose category is not shipping or delivery, such as a delivery_fee with charge_tax_category: "service_fee" or a shipping_fee in handling, now fails with 400 AUTOMATIC_TAX_CHARGE_UNSUPPORTED instead of 400 INVALID_TAX_CATEGORY. When the order's tax location is known, the request that adds or changes the charge fails. param, or remediation.missing_or_invalid_fields for a charge already on the order, names tax.charge_tax_category when you set the category or the charge has none, and type when the category came from the type. Changing charge_tax_category, or changing type on a charge without one, recalculates the charge's tax.

  • API
  • Orders
  • Promotions

Merchant credentials can apply code-gated promotions by ID#

Merchant credentials can now use promotion_id to apply a code-gated promotion through POST /v1/orders, POST /v1/orders/{id}/discounts, and POST /v1/discount-previews. Promotions with active or no_active_codes status are accepted.

An applied discount records promotion_id, omits promotion_code and promotion_code_id, and counts only toward the promotion's max_uses. Promotion availability and the merchant's codes_enabled setting continue to apply. Checkout credentials must use promotion_code.

  • API
  • Discounts

Discount errors name the field you sent#

  • Apply a discount. POST /v1/orders/{id}/discounts returns param such as promotion.promotion_code, promotion.promotion_id, promotion, or manual.amount_money.currency.
  • Preview a discount. POST /v1/discount-previews keeps the discount wrapper: discount.promotion.promotion_code, discount.promotion.promotion_id, discount.promotion, and discount.manual.
  • Create an order. POST /v1/orders includes the discount's array index, such as discounts[1].promotion.promotion_code, discounts[1].promotion.promotion_id, or discounts[0].promotion.
  • Invoice discount validation. POST /v1/invoices includes the array index for quick-pay discount validation, such as quick_pay.discounts[1].promotion.promotion_id or quick_pay.discounts[1].promotion.
  • Policy errors keep their existing params, such as promotion_config.codes_enabled.
  • Promotion warnings. A promotion applied by ID that was accepted but did not apply returns the message Promotion was accepted but did not apply. Promotions applied by code keep Promotion code was accepted but did not apply.
  • API
  • Orders
  • Promotions
  • Checkout

Exhausted promotions are removed before payment#

Promotions and promotion codes that reach their max_uses are now removed from unpaid orders when the order changes, is repriced, or starts payment. When POST /v1/orders/{order_id}/pay removes a discount, it saves the updated order and returns 409 ORDER_CHANGED_REFRESH_REQUIRED with param: applied_discounts and the new outstanding amount. No payment starts. Read the order and review the updated total before paying with a new idempotency key.

Partly paid orders, invoice orders, subscription renewals, and resumed or retried payment attempts keep their discounts. A discount also keeps its use when this order already has a reserved or redeemed redemption for it.

If another buyer takes the last use while payment is starting, pay still returns 409 PROMOTION_NOT_REDEEMABLE with current_status and keeps the discount. The error includes reason: exhausted. Retry with a new idempotency key to remove the discount and receive ORDER_CHANGED_REFRESH_REQUIRED.

PROMOTION_NOT_REDEEMABLE is retryable only with reason exhausted; invoice, partly paid, and renewal payments honor discounts at max_uses.

  • Breaking change
  • API
  • Webhooks

Idempotency replays include metadata and keys come from callers#

Every successful idempotency replay sets Idempotency-Replayed: true and meta.idempotency_replayed: true. The Idempotency-Key response header only echoes a key you sent. A webhook's request.idempotency_key is present only when the originating request sent a key. Send your own key before the first attempt to retry a write safely.

Flint trims surrounding whitespace from keys on every route. A key longer than 255 characters or containing null characters or line breaks returns 400 INVALID_IDEMPOTENCY_KEY with param: Idempotency-Key. Routes that require a key continue to return 400 IDEMPOTENCY_KEY_REQUIRED when it is missing.

Breaking: payment link resolution and report creation previously returned IDEMPOTENCY_KEY_TOO_LONG for oversized keys. Key validation errors now name the Idempotency-Key header in param. Responses no longer return a generated key when you omit one, and request.idempotency_key is optional in webhook envelopes.

  • Breaking change
  • API
  • Invoices
  • Payments

Invoice installments carry the tip first#

An invoice collection smaller than the invoice's balance, such as a scheduled installment, now carries the order's unpaid tip first, up to its amount, and puts the rest toward items and charges. A 250 deposit on an invoice with a 300 tip carries 250 of the tip in its payment intent's tip_money, and the next installment carries the other 50. A collection of the whole balance still carries the whole unpaid tip. Schedule entry amounts and the invoice total are unchanged. Partial payments of an order, outside an invoice, still pay for items and charges first.

Previously the first installment had to carry the whole tip, and collecting a deposit smaller than the tip failed with 400 TIP_ALLOCATION_CAPACITY_EXCEEDED.

A credit note allocation must now leave the order's unpaid tip outstanding on the invoice, because a credit lowers the balance but not the tip. Otherwise it returns 409 CREDIT_NOTE_ALLOCATION_EXCEEDS_BALANCE with the tip in required_money, what the allocation would leave in current_money, and the difference in gap_money. Allocate up to that limit, collect the remaining balance, which carries the tip, then refund the rest of the credit note once the invoice is paid.

TIP_ALLOCATION_CAPACITY_EXCEEDED is now a 409 conflict_error with retryable: false, declared on POST /v1/orders/{order_id}/pay and POST /v1/invoices/{invoice_id}/collect. It means the balance left to collect is smaller than the unpaid tip, which happens when a manual payment covered part of the tip without recording it. submitted_money is the unpaid tip and tip_capable_money is the balance left to collect. Contact support with the request ID.

Breaking: handlers that matched 400 TIP_ALLOCATION_CAPACITY_EXCEEDED should match 409, and credit allocations that leave less than the unpaid tip now fail.

  • Breaking change
  • API
  • Invoices

Invoice lists use cursor page tokens#

GET /v1/invoices, GET /v1/me/invoices, and GET /v1/invoices/{invoice_id}/delivery-attempts now return opaque cursor page tokens. Each token records the last row's sort value and ID, so inserting newer rows, changing the last row, or deleting it does not move the next page's starting position.

Breaking: numeric offset tokens no longer work. Invalid tokens, including tokens reused with different filters or ordering, return 400 INVALID_PAGE_TOKEN with param: "page_token". Restart from the first page without page_token, then pass each next_page_token back unchanged.

Invoices without an invoice number now sort last when sort_by=invoice_number&sort_direction=asc; they also sort last descending. Invoices without a due date continue to sort last in both directions. Invoice IDs break ties in the selected sort direction. Delivery attempts sort by created_at descending, then delivery attempt ID descending.

Rows whose sort values change during traversal may appear again on a later page or be missed. See Pagination for traversal and restart guidance.

  • API
  • Orders
  • Promotions

Order create resolves requested promotions together#

POST /v1/orders ranks all requested promotion codes together with the order's automatic promotions. The result no longer depends on the order of discounts[].

A code whose discountable balance another requested code fully consumed now returns a promotion_declined warning in meta.warnings[] on the 201 response instead of a 400 error.

A manual discount in the same request is no longer reduced by promotion codes. Manual discounts apply first, in request order. A code with no balance left after the request's manual discounts returns 400 with reason no_discountable_balance, wherever it appears in discounts[].

  • Breaking change
  • API
  • Orders

Orders report the requested tip only in tips#

Orders in API responses, including order payment results and checkout delivery selection results, no longer include requested_tip. Order webhooks never carried it. It repeated the entry in tips[] whose status is requested, which carries the same percent or amount_money, name, description, and metadata, plus its order_tip_id and resolved effective_amount_money.

Breaking: read the requested tip from the tips[] entry with status: "requested". requested_tip stays a request field on POST /v1/orders, PATCH /v1/orders/{order_id}, and invoice quick pay, and invoice snapshots keep their own requested_tip.

  • Breaking change
  • API
  • Orders

Order totals failures return 500 ORDER_TOTALS_UNAVAILABLE#

Order writes no longer return 400 INVALID_TOTALS or 400 INVALID_LINE_ITEMS with internal calculation text in message. A totals calculation failure indicates a Flint bug, not amounts you can change, so it returns 500 ORDER_TOTALS_UNAVAILABLE with retryable: false. Contact support with the request ID.

Removing the last taxable line item or charge from an external-tax order with an order-level flat fee, such as retail_delivery_fee, now succeeds and drops the fee. It previously failed with 400 INVALID_TOTALS when only gift card lines remained.

Breaking: these failures previously returned 400 validation_error. Update handlers that match INVALID_TOTALS or INVALID_LINE_ITEMS.

  • Breaking change
  • API
  • Orders
  • Delivery
  • Returns
  • Promotions

Order, delivery, and promotion state conflicts return 409#

Valid requests blocked by mutable order, delivery, or promotion state now return 409 conflict_error. Lifecycle conflicts include error.current_status with the public resource status. Retry guidance remains unchanged.

The following codes move from 400 to 409:

  • DEVICE_DELETED
  • ORDER_ALREADY_CLOSED
  • ORDER_HAS_NO_LINE_ITEMS
  • ORDER_INVENTORY_EXCEPTION_NOT_RESOLVABLE
  • ORDER_INVENTORY_EXCEPTION_UNRESOLVED
  • ORDER_PAYMENT_AUTHORIZATION_NOT_FOUND
  • ORDER_RECEIPT_NOT_AVAILABLE
  • ORDER_STATUS_NOT_CLOSABLE
  • NO_PAYMENTS_FOR_ORDER
  • SETUP_SOURCE_NON_ZERO_ORDER
  • LOCATION_UNAVAILABLE
  • PROMOTION_NO_ACTIVE_CODES
  • PROMOTION_NOT_REDEEMABLE
  • EXTERNAL_PAID_ORDER_ALLOWANCE_EXHAUSTED
  • GIFT_CARD_CHALLENGE_REQUIRED
  • GIFT_CARD_PURCHASE_LIMIT_EXCEEDED
  • NO_DISCOUNTABLE_BALANCE
  • ORDER_RECEIPT_RECIPIENT_LIMIT_REACHED
  • ORDER_TOTAL_BELOW_NET_COLLECTED
  • INVENTORY_RESERVATION_EXISTS
  • ORDER_CUSTOMER_ALREADY_SET
  • ORDER_CUSTOMER_NOT_UPDATABLE_AFTER_PAYMENT
  • ORDER_RECEIPT_EMAIL_ON_FILE
  • ORDER_TAX_NOT_UPDATABLE_AFTER_PAYMENT
  • PAID_LINE_ITEM_MODIFIER_CHANGE_FORBIDDEN
  • PAID_LINE_ITEM_PRICE_CHANGE_FORBIDDEN
  • PAID_LINE_ITEM_QUANTITY_CHANGE_FORBIDDEN
  • PAID_LINE_ITEM_QUANTITY_DECREASE_FORBIDDEN
  • PAID_LINE_ITEM_REMOVAL_FORBIDDEN
  • PAID_LINE_ITEM_TAX_CHANGE_FORBIDDEN
  • AUTOMATIC_PROMOTIONS_DISABLED
  • PROMOTION_CODES_DISABLED
  • AUTOMATIC_TAX_CONNECTION_REQUIRED
  • AUTOMATIC_TAX_ORIGIN_ADDRESS_REQUIRED
  • CATALOG_LINE_ITEM_CURRENCY_CHANGED
  • CATALOG_LINE_ITEM_SOURCE_CHANGED
  • DEVICE_LOCATION_MISMATCH
  • ORDER_RECEIPT_EMAIL_UNAVAILABLE
  • ORDER_RECEIPT_MERCHANT_MANAGED
  • FULFILLMENT_APPROVAL_REQUIRED
  • FULFILLMENT_COMPLETION_TIMESTAMP_REQUIRES_COMPLETE
  • FULFILLMENT_INACTIVE
  • FULFILLMENT_METHOD_UNAVAILABLE
  • FULFILLMENT_PROFILE_UNAVAILABLE
  • FULFILLMENT_SELECTION_REQUIRED
  • FULFILLMENT_SHIPMENT_NOT_MUTABLE
  • FULFILLMENT_SHIPMENT_NOT_VOIDABLE
  • FULFILLMENT_SHIPMENT_TERMINAL
  • FULFILLMENT_TERMINAL
  • ORDER_NOT_FULFILLABLE
  • SHIPMENT_PACKAGE_NOT_MUTABLE
  • SHIPMENT_PACKAGE_NOT_VOIDABLE
  • SHIPMENT_PACKAGE_TERMINAL
  • FULFILLMENT_QUANTITY_EXCEEDS_AVAILABLE
  • SHIPMENT_PACKAGE_ITEM_QUANTITY_EXCEEDS_FULFILLMENT
  • FULFILLMENT_ACTIVE_SHIPMENT_EXECUTION
  • FULFILLMENT_QUANTITY_EXCEEDS_LINE_ITEM
  • FULFILLMENT_COMPLETED
  • FULFILLMENT_COMPLETED_DETAILS_REPLACEMENT
  • FULFILLMENT_CONFIGURATION_ACTION_REQUIRED
  • FULFILLMENT_METHOD_ASSIGNMENT_UNSATISFIABLE
  • FULFILLMENT_PROFILE_REVISION_UNUSABLE

NO_DISCOUNTABLE_BALANCE stays 400 when creating an order because the balance comes from the request. Applying a discount to an existing order returns 409.

Return shipment requests now distinguish their failures:

  • Invalid line allocations return 400 RETURN_SHIPMENT_INVALID.
  • An undecided, canceled, or completed return returns 409 RETURN_ACTION_NOT_ALLOWED, with the return's current_status and param: return_id.
  • Exhausted shipment quantity returns the new 409 RETURN_SHIPMENT_QUANTITY_EXCEEDS_AVAILABLE.
  • A missing return returns 404 RETURN_NOT_FOUND.
  • An unexpected validation failure returns 500.

Requesting refund line items on an empty order now returns 400 LINE_ITEM_NOT_ON_ORDER. A missing delivery profile returns 404 RESOURCE_NOT_FOUND; an inactive profile or one without a current configuration returns 409 FULFILLMENT_PROFILE_UNAVAILABLE. Completion timestamps on fulfillment creation return 400 FULFILLMENT_COMPLETION_TIMESTAMP_NOT_ALLOWED; adding them to an incomplete fulfillment returns 409 FULFILLMENT_COMPLETION_TIMESTAMP_REQUIRES_COMPLETE.

Breaking: these conditions previously returned 400 with type: validation_error, or combined distinct causes under the same error code. Handle the statuses and codes listed above when recovering from a failed request.

  • Breaking change
  • API
  • Payments
  • Orders
  • Checkout
  • Errors

Payments in progress return 409 PAYMENT_IN_PROGRESS#

A request that a payment in flight blocks now returns 409 PAYMENT_IN_PROGRESS with retryable: true, whether a checkout session, an invoice, an order payment attempt, or a linked payment intent holds the order. error.blocking_resources names the blocking resource when Flint knows it and your credential may see it. Wait for it to finish, read the order or checkout session, then retry.

PAYMENT_IN_PROGRESS replaces these codes on every operation that returned them:

  • CHECKOUT_PAYMENT_RESOLVING
  • PAYMENT_ATTEMPT_IN_PROGRESS
  • PAYMENT_ATTEMPT_STILL_PROCESSING
  • PAYMENT_ATTEMPT_FROZEN

PAYMENT_ATTEMPT_REQUIRES_CAPTURE is unchanged and still means an authorized payment waits for you to capture or cancel it.

Breaking: update handlers that match any of the four replaced codes to match PAYMENT_IN_PROGRESS.

  • Breaking change
  • API
  • Payments

Creating a payment intent returns it flat in data#

POST /v1/payment-intents returns the payment intent flat in data, with data.payment_intent_id beside required data.payment_collection guidance. GET uses the same shape with optional collection guidance.

Breaking: standalone create previously nested the intent in data.payment_intent. Order-scoped create continues to return the order payment intent and collection guidance as separate resources.

If Flint can't verify a payment intent's fee, the response succeeds and omits processing_fee_money, add_on_fees, and merchant_net_money. Read the payment intent again later for verified fees.

Confirm and GET return 500 PAYMENT_CLIENT_ACTION_UNAVAILABLE if a payment needs buyer authentication but Flint cannot produce its client action. Contact support with the request ID. A same-key confirm retry returns the stored error again.

  • Breaking change
  • API
  • Payments

Payment intent tips are validated#

tip_money on a standalone payment intent reports the portion of amount_money that is a tip. It is included in the payment amount. Its amount must be at least 1 minor unit, its currency must exactly match amount_money.currency, and it must not exceed amount_money.amount. A tip equal to the payment amount is allowed. Omit tip_money when there is no tip. Order-linked payment intents do not accept it.

Breaking: creating or updating a payment intent with an invalid tip now returns 400 INVALID_TIP_AMOUNT for amounts below 1, 400 TIP_CURRENCY_MISMATCH for a different currency, or 400 TIP_EXCEEDS_AMOUNT for a tip larger than the payment amount. The error's param is tip_money.amount or tip_money.currency. When only amount_money changes, Flint rechecks the stored tip and sets param to amount_money.amount if the new amount is below that tip. When both fields change, Flint validates the new tip against the new amount.

  • API
  • Payments
  • Webhooks

payment_intent events carry the full payment intent#

payment_intent.canceled, payment_intent.payment_failed, payment_intent.requires_action, payment_intent.requires_capture, payment_intent.processing, and payment_intent.succeeded now carry the full payment intent in data.payment_intent, using the same public fields and values as the REST response. payment_intent.payment_failed includes last_payment_error. Status, amounts, and capture fields come from the transition; risk, fulfillment_hold, and last_payment_error are read once when Flint prepares the event, usually seconds later, and retries resend the same payload.

The snapshot omits current_payment_action, fee fields (processing_fee_money, add_on_fees, and merchant_net_money), and expanded customer, order, and invoice objects. Webhooks carry no client authority. Read fees from GET /v1/payment-intents/{payment_intent_id} or balance_transaction.created. cancellation_reason uses the REST cancellation reason values.

payment_intent.fulfillment_hold.updated keeps its existing payload shape.

  • Breaking change
  • API
  • Payments
  • Refunds
  • Invoices

Payment, refund, and invoice state conflicts return 409#

Requests that fail because of mutable payment or refund state, remaining capacity, or invoice configuration now return 409 conflict_error. Lifecycle conflicts include error.current_status with the blocking resource's public status. Error messages use public names.

The following codes now use 409 on every operation that returns them:

  • PAYMENT_ALREADY_SUCCEEDED
  • PAYMENT_CANCELED
  • PAYMENT_EXPIRED
  • PAYMENT_INTENT_CANNOT_BE_UPDATED
  • INVALID_STATUS_FOR_CAPTURE
  • CANNOT_CANCEL_SUCCEEDED_PAYMENT
  • PAYMENT_ALREADY_CANCELED
  • CANNOT_CANCEL_EXPIRED_PAYMENT
  • ORDER_NOT_PAYABLE
  • CONFIRMATION_TOKEN_EXPIRED
  • PAYMENT_AUTHORIZATION_NOT_CAPTURABLE
  • PAYMENT_AUTHORIZATION_EXPIRED
  • CONFIRMATION_TOKEN_ALREADY_USED
  • CAPTURE_AMOUNT_EXCEEDS_CAPTURABLE
  • PAYMENT_INTENT_LIMIT_REACHED
  • PAYMENT_NOT_REQUIRED
  • PAYMENT_LEG_SELECTION_REQUIRED
  • PAYMENT_SOURCE_NOT_UPDATABLE
  • CUSTOMER_NOT_UPDATABLE
  • RECEIPT_EMAIL_NOT_UPDATABLE
  • AMOUNT_NOT_UPDATABLE
  • ORDER_PAYMENT_CUSTOMER_MISMATCH
  • PAYMENT_OPTION_NOT_READY
  • PAYMENT_METHOD_REQUIRED
  • PROCESSOR_NOT_CONFIGURED
  • TEST_PROCESSOR_NOT_CONFIGURED
  • UNSUPPORTED_PROCESSOR
  • PROCESSING_FEE_PRICING_NOT_AUTHORIZED
  • RISK_ACTION_NOT_SUPPORTED_FOR_PAYMENT_OPTION
  • RISK_ACTION_NOT_SUPPORTED_FOR_PAYMENT_FLOW
  • SAVE_PAYMENT_METHOD_NOT_OFFERED
  • SAVE_PAYMENT_METHOD_VERIFICATION_REQUIRED
  • ACH_TRANSACTION_PURPOSE_UNRESOLVED
  • PAYMENT_INTENT_NOT_REFUNDABLE
  • PAYMENT_INTENT_DISPUTED
  • REFUND_SUBMISSION_DEADLINE_EXPIRED
  • REFUND_ADJUSTMENT_PAYMENT_INTENT_NOT_REFUNDABLE
  • ORDER_HAS_NO_REFUNDABLE_CHARGES
  • ORDER_HAS_NO_REFUNDABLE_LINE_ITEMS
  • AMOUNT_EXCEEDS_REFUNDABLE
  • CHARGE_REFUND_AMOUNT_EXCEEDS_REFUNDABLE
  • LINE_ITEM_REFUND_AMOUNT_EXCEEDS_REFUNDABLE
  • LINE_ITEM_REFUND_QUANTITY_EXCEEDS_REFUNDABLE
  • NOTHING_TO_REFUND
  • NOTHING_TO_REFUND_FOR_CHARGE
  • NOTHING_TO_REFUND_FOR_LINE_ITEM
  • NOTHING_TO_REFUND_FOR_TAX_BREAKDOWN
  • TAX_BREAKDOWN_REFUND_EXCEEDS_REMAINING
  • REFUND_ADJUSTMENT_REFUND_EXCEEDS_REMAINING
  • REFUND_ADJUSTMENT_REFUND_EXCEEDS_PAYMENT_INTENT_REFUNDABLE
  • FLAT_TAX_PAYMENT_ATTRIBUTION_UNAVAILABLE
  • CUSTOMER_PAYMENT_TERM_UNAVAILABLE
  • INVOICE_PAYMENT_TERM_CURRENCY_MISMATCH
  • INVOICE_AUTOPAY_PAYMENT_METHOD_REQUIRED
  • PAYMENT_OPTION_LIMIT_EXCEEDED

Request-shape failures keep 400 under ORDER_LINKED_AMOUNT_NOT_UPDATABLE, PAYMENT_OPTION_SCOPE_UNSUPPORTED, SAVE_PAYMENT_METHOD_REQUIRES_FULL_PAYMENT, and DUPLICATE_PAYMENT_INTENT_ID. DUPLICATE_PAYMENT_INTENT_ID names the repeated payment_intents[i].payment_intent_id in error.param. An explicitly empty subscription payment_method_id uses 400 INVALID_ID. Paying without any payment legs or payment source uses 400 PAYMENT_SOURCE_REQUIRED. Saving a card without a checkout credential uses 403 CHECKOUT_CREDENTIAL_REQUIRED.

Corrupt stored refund lineage returns 500 FLAT_TAX_REFUND_STATE_INVALID with retryable: false. Contact support with the request ID so Flint can repair the order's refund records.

Breaking: the same state conditions previously returned 400 validation_error. Update handlers that match HTTP 400 or the replaced request-shape codes.

  • API

Promotions without eligibility rules omit `eligibility_rules`#

Promotions without eligibility rules now omit eligibility_rules instead of returning {}. This also applies to promotions created with "eligibility_rules": [] and to empty discounted_item_rules and qualifying_item_rules groups in application_method.

The omitted fields match the published rule-group schema and avoid INVALID_RULE_GROUP when sending a response's rule fields back in an update request.

  • Breaking change
  • API
  • Orders
  • Promotions
  • Errors

Promotion write and discount removal errors name the field you sent#

Breaking: param on these promotion and discount errors now gives the path in the request you sent. If you match on param, update these values.

  • POST /v1/orders names manual discount errors by the discount's position, such as discounts[0].manual.scope, discounts[0].manual.discount_class, or discounts[0].manual.order_charge_ids. Before, they used discounts.manual.scope or discount.manual.order_charge_ids.
  • POST /v1/orders/{order_id}/discounts/remove returns DISCOUNT_IDS_REQUIRED with param: order_discount_ids, and CHECKOUT_DISCOUNT_NOT_REMOVABLE with the refused ID's position, such as order_discount_ids[1]. Before, the first had no param and the second used applied_discount_ids.
  • Rule errors on promotion create and update follow the rule group you sent. A rule in an array is eligibility_rules[1].values[0], and a rule object inside all or any is eligibility_rules.all[1].operator. Before, they used eligibility_rules.rules[1].values[0]. INVALID_RULE_VALUE names the value, such as eligibility_rules[1].values[0], instead of values. UNKNOWN_CATEGORY_HANDLE names the line_item.categories value, instead of category_handles.
  • Errors for an entry in codes on POST /v1/promotions include its position, such as codes[1].max_uses, and DUPLICATE_CODE uses codes. POST /v1/promotions/{promotion_id}/codes uses expires_at and metadata instead of code.expires_at and code.metadata.
  • 404 RESOURCE_NOT_FOUND for a missing promotion or code names it, such as Promotion code pcode_01J... not found. Before, it was Promotion not found for both.
  • INVALID_EXPIRES_AT is listed on promotion create and on promotion code create and update.
  • Breaking change
  • API
  • Orders
  • Promotions

Promotion declines use 409 for promotion state, and displaced promotions are reported#

Breaking: applying promotions to orders changed in these ways.

  • PROMOTION_DECLINED returns 409 conflict_error when the promotion's or code's state blocks it: reason is expired, not_yet_started, exhausted, or disabled, and error.current_status gives the promotion's or code's status. automatic_disabled and codes_disabled stay 409, and no_discountable_balance stays 409 on an existing order and 400 on order creation. Before, the four lifecycle reasons returned 400 validation_error. Every other reason still returns 400.
  • Creating an order with more codes than max_promotions_per_order no longer returns 400 PROMOTION_DECLINED with reason: max_promotions_reached. The order is created with its most valuable promotions, and each code that didn't fit comes back in meta.warnings[] as a promotion_declined warning.
  • When the codes on a new order conflict, such as two codes in one exclusivity group, Flint keeps the winner and returns the loser as a promotion_declined warning. Before, both could stay on the order.
  • Two pending promotions in one exclusivity group, or more pending promotions than max_promotions_per_order, are now resolved whenever the order changes or is repriced, even when you have no active automatic promotions.
  • Applying a discount that removes promotions already on the order returns a promotion_removed warning in meta.warnings[] for each one, with the decline reason, resource_id, promotion_code when a code applied it, and would_have_applied_money for the amount it took off. The order's activity records the same reason. Before, the removal was recorded only in the activity, always as superseded by a better offer.
  • POST /v1/orders/{order_id}/discounts/remove returns 409 DISCOUNT_NOT_REMOVABLE for a discount from an automatic promotion, with param naming its position in order_discount_ids, and changes nothing on the order. Before, the request succeeded and the promotion applied again under a new ID. Make the promotion inactive to stop it applying. Manual and code discounts can still be removed.
  • A repeated ID in order_discount_ids returns 400 DUPLICATE_ORDER_DISCOUNT_ID. Before, it returned 404 DISCOUNT_NOT_FOUND.
  • POST /v1/discount-previews lists promotions already on the order that the previewed code would remove in skipped, with their decline reason.
  • POST /v1/discount-previews reports an archived promotion in skipped with disabled. Before, unknown_type.
  • API
  • Orders
  • Promotions

New promotion decline reason `superseded_by_higher_priority`#

Promotions that lose to a higher exclusivity.priority in a highest_priority group now report superseded_by_higher_priority in preview skipped[] and order warnings. Equal-priority ties and best_of groups continue to report superseded_by_better_offer.

  • Breaking change
  • API
  • Promotions

Promotion rules reject unknown enum values#

Promotion rules on charge.type and delivery_choice.type using eq, ne, or in now reject values outside the published lists with 400 validation_error and code UNKNOWN_RULE_VALUE. Values are case-sensitive. The error message lists all allowed values, also published in OpenAPI's x-flint-rule-attributes extension on PromotionRule.

Existing promotions keep working until those rule groups are replaced. Updating eligibility_rules validates the replacement eligibility rules. Updating application_method validates its replacement discounted_item_rules and qualifying_item_rules. Unrelated edits leave stored rule groups unchanged.

  • Breaking change
  • API
  • Orders
  • Promotions

Percent promotions apply to the remaining balance#

This behavior change affects promotion previews, order creation, discount application, and repricing. Percent promotions now take their percent from the amount left after earlier discounts on the same targets, including manual discounts. Line-item discounts are priced first, then order discounts, then service-charge discounts. The standalone amount_money and would_have_applied_money values continue to use the gross basis.

For example, an order with three items at 25 USD each, a buy-two-get-one-free promotion, and an order promotion for 25% off now receives 12.50 USD from the percent promotion. Previously it received 18.75 USD. The percent promotion's amount_money remains 18.75 USD. Fixed amount_off caps and buy_x_get_y reward pricing are unchanged.

A promotion with no matching targets on the order now reports promotion_decline_reason: not_eligible in preview skipped[], or reason: not_eligible in meta.warnings when explicitly applied with a 200 response. Previously it reported no_discountable_balance. A matching target that has already been fully discounted continues to report no_discountable_balance.

  • Breaking change
  • API
  • Promotions

Promotion updates keep omitted combines_with classes#

Breaking: three promotion writes that Flint used to accept now behave differently.

  • PATCH /v1/promotions/{promotion_id} changes only the combines_with classes in the request. Classes you leave out keep their current value. Before, they were reset to true. combines_with: null, combines_with: {}, and null for a class now return 400; before, they reset every class to true. Creating a promotion still defaults missing classes to true.
  • "status": "" on PATCH /v1/promotions/{promotion_id} and PATCH /v1/promotions/{promotion_id}/codes/{promotion_code_id} returns 400 INVALID_STATUS. Before, it set the status to active. Send active or inactive.
  • application_method.max_discounted_quantity on an amount_off or buy_x_get_y promotion returns 400 INVALID_MAX_DISCOUNTED_QUANTITY with param: application_method.max_discounted_quantity. Before, Flint ignored it. It applies only to percent_off.
  • Breaking change
  • API
  • Refunds

Refund amounts default to the sum of named targets#

POST /v1/refunds now refunds the sum of named line_items, charges, and top-level tax_breakdown_refunds when you omit both top-level amount_money and tender_allocations. The total includes resolved line item tax. Unnamed tips and flat taxes remain unrefunded. Mixed targets no longer require amount_money, and AMOUNT_REQUIRED_FOR_MIXED_REFUND_TARGETS is retired.

A fully discounted charge can reverse its discount alongside a positive line item refund. The combined named targets must total greater than zero.

An allocation whose share is fully withheld by a refund adjustment now counts as zero. A request that previously refunded that withheld share now returns REFUND_AMOUNT_NOT_ALLOCATED when its explicit amount_money cannot be fully allocated. Lower the amount to the refundable target total, or name more refundable targets.

Sending amount_money still sets the refund total. Mixed targets that exceed it return REFUND_TARGETS_EXCEED_REFUND_AMOUNT. When you send tender_allocations, their amounts set the refund total. Omitting the amount and all targets still refunds the full remaining balance.

A payment_intent_id refund of a flat tax split across payments returns FLAT_TAX_PAYMENT_ATTRIBUTION_UNAVAILABLE. Refund by order_id instead.

Breaking: remove handlers for AMOUNT_REQUIRED_FOR_MIXED_REFUND_TARGETS and review explicit refund amounts that include shares withheld by adjustments. See Refunds and Sales tax.

  • Breaking change
  • API

Repeated query parameters are validated#

Breaking: sending a query parameter that accepts one value more than once now returns 400 REPEATED_QUERY_PARAM, with param naming the repeated key. Send the parameter once. Documented multi-value filters and expand continue to accept repeated keys and comma-separated values.

  • Breaking change
  • API
  • Errors

Request body field validation#

Breaking: request bodies reject duplicate and miscased fields, and null on fields that can't be cleared.

  • Repeating a member name in the same JSON object returns 400 DUPLICATE_FIELD, with param naming the repeated field. This applies at every depth, including inside metadata.
  • Field names are case-sensitive. A wrong-case field returns 400 UNKNOWN_FIELD, with param naming the field as sent and the message giving the correct spelling. Map keys such as metadata keys stay free-form.
  • Payment intent updates, payout settings updates, risk rule updates, and POST /v1/onboarding/advance return 400 NULL_NOT_ALLOWED when a field that cannot be cleared is sent as null. Omit a field to leave its value unchanged. Payout settings still accept null to clear delay_days_override or an individual currency in minimum_balance_by_currency. Sending the whole minimum-balance map as null still returns 400 INVALID_MINIMUM_BALANCE_BY_CURRENCY. Metadata patches still accept null to clear all metadata or delete a key.

When correcting a write, use a new idempotency key. See Error handling.

  • Breaking change
  • API
  • Refunds
  • Gift-cards

Refunds, wallet cards, and gift card refund allocations use consistent response fields#

POST /v1/refunds returns replacement card codes in data.gift_card_codes. Codes remain recoverable for 24 hours by replaying the original request. Refund reads, lists, expansions, and events omit them.

GET /v1/me/flint-wallet/payment-methods returns the full collection in one page and omits has_more.

Gift card purchase refund value_allocations use amount_money, including the parent refund's currency, instead of a bare amount.

Refund requests that require an Idempotency-Key trim surrounding whitespace. Successful replays carry both Idempotency-Replayed: true and meta.idempotency_replayed: true. A refund of a disputed payment retried with the same key returns the recorded error.

  • Subscriptions
  • Refunds

Subscription payments completed after cancellation are refunded#

If a subscription is canceled while its setup fee or recurring payment is processing, Flint automatically refunds payments completed at or after cancellation to the original payment method. Payments completed before cancellation stay owed. An order with a fulfillment that has started and has not been canceled is excluded from the automatic refund.

When a renewal is processing at period-end cancellation, Flint resolves the renewal first. A successful payment keeps the subscription active through the renewed period before cancellation. A failed payment ends it immediately without payment retries. Older subscriptions may instead cancel immediately and receive the automatic refund when the payment completes.

Automatic refunds use the standard refund.created, refund.updated, and refund.failed events and buyer refund receipt. They carry reason: "other" and metadata source: "subscription_canceled_before_payment" and subscription_id.

See Payments still processing.

  • API

Timestamps carry whole seconds#

Timestamps carry whole seconds, and request timestamps with fractional seconds are truncated. Responses use RFC 3339 UTC timestamps with Z, including delivery configuration timestamps, quote expirations, and customer receivables observations.

Report interval timestamps and gift card recipient send_at now accept fractional seconds. For example, 2026-09-01T00:00:00.123Z is stored and returned as 2026-09-01T00:00:00Z. Request timestamps with other RFC 3339 offsets are normalized to UTC.

  • Breaking change
  • API
  • Orders
  • Payments

Partly paid orders keep their unpaid tip payable#

A partial payment of an order pays for its items and charges before its tip; invoice installments, changed separately, carry the tip first. After a partial payment, removing an item, lowering a quantity, adding a discount, or another change could leave the unpaid tip larger than the balance left to pay. No payment could then complete the order, and POST /v1/orders/{order_id}/pay failed with 400 TIP_ALLOCATION_CAPACITY_EXCEEDED.

Those changes now fail when they happen, with 409 ORDER_TOTAL_BELOW_NET_COLLECTED and retryable: false, and the order stays as it was. required_money is the money already collected, current_money is what the change would leave for items and charges, and gap_money is the difference. ORDER_TOTAL_BELOW_NET_COLLECTED also carries these amounts when a change would drop the whole order total below the money collected.

Breaking: a change that previously succeeded and left the order unpayable now returns 409. Use a refund or credit adjustment instead.

  • API
  • Delivery

Delivery rate callbacks receive the quote's expiry#

Every delivery rate callback request now includes delivery_quote_expires_at, the time the quote expires. Return an expires_at no later than it. A quote lasts up to 15 minutes, or just under 5 minutes for a cart with tracked inventory. The value is truncated to whole seconds.

When the quote expires at or before evaluated_at plus the method's minimum_option_lifetime_seconds, Flint no longer calls your endpoint. The method reports option_lifetime_too_short instead of callback_result_invalid.

See Live delivery rates with a callback.

  • Breaking change
  • API
  • Delivery

Delivery rate callback URL errors use INVALID_URL and INVALID_URL_HOST#

POST /v1/delivery-rate-callbacks and PATCH /v1/delivery-rate-callbacks/{delivery_rate_callback_id} validate configuration.url the same way. A malformed URL, such as one that isn't HTTPS or contains credentials or a fragment, returns 400 INVALID_URL. A URL whose host is private or internal returns 400 INVALID_URL_HOST. Both set param to configuration.url.

Breaking: a malformed URL on create or update used to return DELIVERY_REQUEST_INVALID. A private host on create set param to url, and updating a callback's URL didn't check its host until Flint first called it.

  • Breaking change
  • API
  • Checkout
  • Delivery

Checkout sessions reject an empty delivery method list#

POST /v1/checkout-sessions now rejects "delivery_method_ids": [] with 400 INVALID_REQUEST and "delivery_method_ids": null with 400 NULL_NOT_ALLOWED, both with param set to delivery_method_ids. Leave the field out to use settings.checkout.default_delivery_method_ids, or list 1 to 25 methods.

Breaking: until this release, [] skipped the default. For an order with nothing to deliver it did nothing, and for an order with items to deliver it failed with FULFILLMENT_METHOD_ASSIGNMENT_UNSATISFIABLE. Payment links are unchanged: [] on a link clears its own list so it uses the default.

  • API
  • Delivery

Delivery errors name the field to fix#

  • Previews with an unavailable method. POST /v1/delivery-previews returns 400 FULFILLMENT_METHOD_UNAVAILABLE with param set to delivery_method_ids and blocking_resources naming the method, zone, location set, or rate callback, the same as checkout creation. Until this release it returned 503 DELIVERY_PREVIEW_UNAVAILABLE, which said to retry. 503 DELIVERY_PREVIEW_UNAVAILABLE now means only that a service Flint depends on was unavailable.
  • Subscription previews of a method edit. POST /v1/subscription-previews with mode: "delivery_method_update" returns 400 FULFILLMENT_METHOD_UNAVAILABLE when the method, as saved or as edited, uses a zone or location set that is missing or not active. param is delivery_method.configuration when your edit adds it and delivery_method_id when the saved method already uses it, and blocking_resources names it. Until this release it returned 503 DELIVERY_SERVICE_UNAVAILABLE, which said to retry, or 404 DELIVERY_RESOURCE_NOT_FOUND for a missing zone.
  • Payment links with an unavailable method. Creating or updating a payment link with an unavailable method returns reason and blocking_resources with 400 FULFILLMENT_METHOD_UNAVAILABLE, the same as checkout creation. If Flint fails while checking the methods, creating, updating, or reading a public payment link returns 500 INTERNAL_ERROR, which you can retry, instead of 400 FULFILLMENT_METHOD_UNAVAILABLE or 409 PAYMENT_LINK_FULFILLMENT_UNAVAILABLE.
  • Configuration errors. Creating or updating a delivery profile, zone, location set, method, or rate callback returns param and a specific message for every value you can fix, such as a blank name, a missing minimum_option_lifetime_seconds, or "eligibility": null. A create request without configuration returns DELIVERY_FIELD_REQUIRED, and an unknown or mistyped key inside it returns UNKNOWN_FIELD or INVALID_FIELD_TYPE with param such as configuration.foo. List filters such as status and type name the query parameter the same way.
  • Internal field names. Delivery configuration accepts only documented field names. A request that uses an internal name, such as fulfillment_zone_id for delivery_zone_id or tax_category for charge_tax_category, returns 400 UNKNOWN_FIELD with param set to the path you sent. Until this release, delivery methods accepted several of these names as aliases. Requests pinned to API version 2026-02-01 can still send tax_category when creating a method.
  • Revocations. POST /v1/delivery-revocations names the field to fix. A missing reason or target ID returns DELIVERY_FIELD_REQUIRED, a field the target_type doesn't take returns DELIVERY_FIELD_NOT_SUPPORTED, and a missing expected_version returns DELIVERY_EXPECTED_VERSION_REQUIRED, each with param. A stale expected_version returns 409 DELIVERY_RESOURCE_VERSION_CONFLICT with current_version, and estimated_impact.current_selection_count now counts the selections the revocation affects. Until this release these returned DELIVERY_REQUEST_INVALID without param, an unknown target_type returned DELIVERY_CONFIGURATION_INVALID, and fields the target type doesn't take were ignored.
  • Validation messages for delivery fields end without a period.
  • Name and metadata limits count characters, not bytes.

See Delivery options.

  • Breaking change
  • API
  • Delivery

Delivery methods no longer return offer_windows#

Delivery method configuration no longer includes offer_windows, at the top level or inside estimate.schedule_window. A method offers windows when estimate.type is schedule_window.

Breaking: sending offer_windows in a delivery method create or update now returns 400 UNKNOWN_FIELD with param naming the key. Flint already ignored the value and derived it from the estimate, so remove it from your requests.

  • Breaking change
  • API
  • Delivery
  • Fulfillment

Dimensions use millimeter, centimeter, or inch#

unit on package dimensions, delivery profile configuration.dimensions, and line item fulfillment.dimensions is now millimeter, centimeter, or inch, and length, width, and height must be greater than 0.

Breaking: until this release these fields accepted any string, such as in or cm, and profiles and line items accepted lengths of 0 or less. Those requests now return 400 with param naming the field: INVALID_SHIPMENT_DIMENSIONS for packages, INVALID_FULFILLMENT_REQUIREMENT for line items, and DELIVERY_REQUEST_INVALID for profiles. Dimensions saved earlier with mm, cm, or in are returned with the full unit name.

  • API
  • Delivery

Location sets accept stores without inventory allocation#

POST /v1/delivery-location-sets and PATCH /v1/delivery-location-sets/{delivery_location_set_id} now accept any active Location with an address and time zone. Until this release, a set rejected a Location whose inventory.allocation_status wasn't active with 400 DELIVERY_REQUEST_INVALID, while a pickup method that listed the same Location in origin.location_ids accepted it.

Stock is still checked when a buyer chooses a store: a store that can't supply tracked items shows as unavailable for pickup. See Offer pickup at your shops.

  • Breaking change
  • API
  • Delivery

Pickup search distances use meter, kilometer, and mile#

POST /v1/delivery-previews with mode: "pickup_locations" accepts maximum_distance.unit values meter, kilometer, and mile, the units zone radiuses and distance pricing already use.

Breaking: meters, kilometers, and miles now return 400 DELIVERY_PICKUP_AVAILABILITY_INVALID with param set to maximum_distance.unit. Send the singular value instead.

  • API
  • Delivery

State conditions recognize military addresses and can't be passed with an unknown state#

  • Military addresses. A state condition accepts AA, AE, and AP, stored as US-AA, US-AE, and US-AP, and a buyer address with one of these states matches them. To leave military addresses out of a contiguous US zone, list them with Alaska and Hawaii under not.
  • Unrecognized states. When a condition lists states in the buyer's country and the buyer's state names none of that country's subdivisions, the condition can no longer be decided. The method or rate is left out as cannot_calculate with invalid_buyer_input. Until this release, the condition counted as not matching, so a zone defined as "US, not Alaska or Hawaii" matched an address with an unrecognized state.
  • Other countries. A condition that lists no subdivision of the address's country doesn't match it, even when the address has no state. A rate for US states now falls through for a Canadian address without a province instead of leaving the method out.

See Create zones.

  • API

New in the API: week of October 5, 2026#

427 additions across Subscriptions, Customer account, and 34 more areas. These are additions, so nothing you already use needs to change.

Subscriptions
65 additions
Customer account
26 additions
Subscription offers
7 additions
Orders
63 additions
Subscription plans
22 additions
Customers
6 additions
Inventory
4 additions
Merchants
4 additions
  • Endpoint

    Get merchant

  • Endpoint

    Update merchant

  • Request field
    iconon PATCH /v1/merchant

    A square image, at least 128 by 128 pixels, shown where the store needs a small mark: browser tabs and compact headers.

  • Field

    A square image, at least 128 by 128 pixels, shown where the store needs a small mark: browser tabs and compact headers.

Invoices
18 additions
Settings
18 additions
Gift cards
11 additions
  • Endpoint

    Create a gift card funding disposition

  • Query parameter
    created_afteron GET /v1/gift-card-loads

    Inclusive lower bound on created_at.

  • Query parameter
    created_beforeon GET /v1/gift-card-loads

    Exclusive upper bound on created_at.

  • Query parameter
    created_afteron GET /v1/gift-card-notifications

    Inclusive lower bound on created_at.

  • Query parameter
    created_beforeon GET /v1/gift-card-notifications

    Exclusive upper bound on created_at.

  • Query parameter
    created_afteron GET /v1/gift-card-redemptions

    Inclusive lower bound on created_at.

  • Query parameter
    created_beforeon GET /v1/gift-card-redemptions

    Exclusive upper bound on created_at.

  • Query parameter
    posted_afteron GET /v1/gift-card-transactions

    Inclusive lower bound on posted_at.

Promotions
5 additions
Returns
1 addition
Fulfillment records
41 additions
  • Request field
    courier_support_phoneon PATCH /v1/fulfillments/{fulfillment_id}

    Buyer-safe support number for the courier or merchant fleet.

  • Request field
    reason_messageon POST /v1/fulfillments/{fulfillment_id}/transitions

    Your note explaining this transition.

  • Request field
    reason_messageon POST /v1/fulfillments/{fulfillment_id}/transitions

    Your note explaining this transition.

  • Request field
    reason_messageon POST /v1/fulfillments/{fulfillment_id}/transitions

    Your note explaining this transition.

  • Request field
    reason_messageon POST /v1/fulfillments/{fulfillment_id}/transitions

    Your note explaining this transition.

  • Request field
    reason_messageon POST /v1/fulfillments/{fulfillment_id}/transitions

    Your note explaining this transition.

  • Request field
    reason_messageon POST /v1/fulfillments/{fulfillment_id}/transitions

    Your note explaining this transition.

  • Request field
    reason_messageon POST /v1/fulfillments/{fulfillment_id}/transitions

    Your note explaining this transition.

Checkout sessions
24 additions
  • Request field
    subscription_termson PATCH /v1/checkout-sessions/{checkout_session_id}

    Subscription plan sessions only.

  • Request field
    timezoneon PATCH /v1/checkout-sessions/{checkout_session_id}

    Checkout credentials only.

  • Request field
    page_originon POST /v1/checkout-sessions

    Origin of the page where you render this embedded checkout, such as https://shop.example.com.

  • Request field
    subscription_plan_idon POST /v1/checkout-sessions

    An active plan.

  • Request field
    subscription_termson POST /v1/checkout-sessions

    Subscription terms you set on a checkout session: billing interval, interval count, and quantity.

  • Request field
    reason_messageon POST /v1/checkout-sessions/{checkout_session_id}/close

    Your note explaining why you are closing the checkout session.

  • Field

    Recipient name saved with the customer's default shipping address.

  • Field

    Where the buyer completes a gift card challenge for this checkout.

Payment links
11 additions
Delivery configuration
9 additions
Onboarding
9 additions
Webhooks
9 additions
  • Request field
    reason_messageon POST /v1/webhook-deliveries/{webhook_delivery_id}/resend

    Your note explaining this action.

  • Field

    The note supplied when this action was requested.

  • Field

    The note supplied when this action was requested.

  • Values

    custom_domain.status_changedsubscription_delivery_migration.completedsubscription_payment_retry.createdsubscription_payment_retry.failedsubscription_payment_retry.succeededsubscription.cycle_skipped

  • Values

    custom_domain.status_changedsubscription_delivery_migration.completedsubscription_payment_retry.createdsubscription_payment_retry.failedsubscription_payment_retry.succeededsubscription.cycle_skipped

  • Values

    custom_domain.status_changedsubscription_delivery_migration.completedsubscription_payment_retry.createdsubscription_payment_retry.failedsubscription_payment_retry.succeededsubscription.cycle_skipped

  • Values

    custom_domain.status_changedsubscription_delivery_migration.completedsubscription_payment_retry.createdsubscription_payment_retry.failedsubscription_payment_retry.succeededsubscription.cycle_skipped

  • Values

    custom_domain.status_changedsubscription_delivery_migration.completedsubscription_payment_retry.createdsubscription_payment_retry.failedsubscription_payment_retry.succeededsubscription.cycle_skipped

Payments
8 additions
Refunds
7 additions
Organizations
6 additions
Partner apps
5 additions
Delivery previews
4 additions
Merchant account sessions
3 additions
Delivery selections
1 addition
Devices
1 addition
Disputes
1 addition
Modifiers
1 addition
Money movement
1 addition
Partner OAuth
1 addition
Payment methods
1 addition
Products
1 addition
Sandboxes
1 addition
Webhook events
19 additions
  • Field
    DeliveryCalculatedPricingStrategy.base_fee_currency_options

    Base delivery fee by ISO currency code.

  • Field
    DeliveryCalculatedPricingStrategy.maximum_fee_currency_options

    Maximum delivery fee by ISO currency code.

  • Field
    DeliveryCalculatedPricingStrategy.minimum_fee_currency_options

    Minimum delivery fee by ISO currency code.

  • Field
    DeliveryCalculatedPricingStrategy.per_item_handling_fee_currency_options

    Handling fee per item by ISO currency code.

  • Field
    DeliveryEligibilityExpression.customer_has_phone

    A delivery eligibility condition that tests a true or false fact about the customer against `value`.

  • Field
    DeliveryEligibilityExpression.subscription_purchase

    A delivery eligibility condition that tests a true or false fact about the customer against `value`.

  • Field
    DeliveryExternalPricingStrategy.maximum_fee_currency_options

    Maximum delivery fee by ISO currency code.

  • Field
    DeliveryExternalPricingStrategy.minimum_fee_currency_options

    Minimum delivery fee by ISO currency code.

Errors
13 additions
  • API

New in the API: week of September 28, 2026#

131 additions across Gift cards, Customer account, and 19 more areas. These are additions, so nothing you already use needs to change.

Related: Codes for saved details default to the auto channel, Delivery choice groups name their method types, Delivery quotes name required recipient fields before an address, Delivery recipient details are checked at payment, Quotes offer delivery and pickup windows only while they can be chosen, Exact ZIP codes match verified addresses, Pickup search takes the buyer's current choice and lists stores that track no inventory, Request bodies up to 1 MiB, State conditions match the state a buyer sends

Gift cards
27 additions
Customer account
20 additions
Orders
23 additions
  • Endpoint
  • Endpoint

    Apply a gift card to an order

  • Endpoint
  • Request field
    gift_card_recipienton PATCH /v1/orders/{order_id}/line-items/{order_line_item_id}

    Replaces the recipient of an unfunded gift card purchase.

  • Request field
    accepted_gift_card_allocationon POST /v1/orders/{order_id}/pay

    Accept the exact gift_card_estimate returned on the current order, including all selected cards and both totals.

  • Request field
    accepted_gift_card_allocationon POST /v1/orders/{order_id}/pay

    Accept the exact gift_card_estimate returned on the current order, including all selected cards and both totals.

  • Request field
    save_payment_method_phoneon POST /v1/orders/{order_id}/pay

    A US or Canadian mobile phone number in E.164 format, such as +14155552671, that confirms a card saved while the checkout acts for no customer, or for the customer whose email the buyer confirmed in it.

  • Request field
    save_payment_method_phoneon POST /v1/orders/{order_id}/pay

    A US or Canadian mobile phone number in E.164 format, such as +14155552671, that confirms a card saved while the checkout acts for no customer, or for the customer whose email the buyer confirmed in it.

Refunds
9 additions
Checkout sessions
8 additions
Products
7 additions
Subscriptions
6 additions
Webhooks
6 additions
  • Values

    gift_card_load.createdgift_card_load.updatedgift_card_notification.createdgift_card_notification.updatedgift_card_redemption.createdgift_card_redemption.updated

  • Values

    gift_card_load.createdgift_card_load.updatedgift_card_notification.createdgift_card_notification.updatedgift_card_redemption.createdgift_card_redemption.updated

  • Values

    gift_card_load.createdgift_card_load.updatedgift_card_notification.createdgift_card_notification.updatedgift_card_redemption.createdgift_card_redemption.updated

  • Values

    gift_card_load.createdgift_card_load.updatedgift_card_notification.createdgift_card_notification.updatedgift_card_redemption.createdgift_card_redemption.updated

  • Values

    gift_card_load.createdgift_card_load.updatedgift_card_notification.createdgift_card_notification.updatedgift_card_redemption.createdgift_card_redemption.updated

  • Values

    gift_card_load.createdgift_card_load.updatedgift_card_notification.createdgift_card_notification.updatedgift_card_redemption.createdgift_card_redemption.updated

Returns
3 additions
Inventory
2 additions
Reports
2 additions
Delivery previews
1 addition
Delivery selections
1 addition
Merchants
1 addition
Onboarding
1 addition
Payment links
1 addition
Return inspections
1 addition
Sandboxes
1 addition
  • Values

    commerce.gift_cards.adjustments.writecommerce.gift_cards.readcommerce.gift_cards.redemptions.writecommerce.gift_cards.secrets.writecommerce.gift_cards.write

Settings
1 addition
Webhook events
7 additions
Errors
3 additions
  • Breaking change
  • API
  • Checkout

Codes for saved details default to the auto channel#

POST /v1/checkout-sessions/{checkout_session_id}/customer-verifications with purpose set to use_saved_payment_methods and no channel now uses "channel": "auto", which hosted checkout sends as the buyer types their email. Flint sends the code the way the buyer saved their details:

  • Details saved with a mobile phone number: Flint texts the code to that number, and the response has "channel": "sms" and phone_last_digits.
  • Details saved only by email: Flint emails the code, and the response has "channel": "email".
  • No saved details, or a cap reached: Flint sends nothing and returns 409 with CUSTOMER_VERIFICATION_NOT_SENT.

Breaking: until this release, a request without a channel emailed the code and answered the same whether or not the email had saved details. To keep that answer, send "channel": "email". A channel sent as an empty string or null now returns 400; leave the field out to use the default.

save_payment_method still defaults to email, and confirm_saved_payment_method still requires a channel. See Recognize a returning buyer.

  • API
  • Delivery
  • Checkout

Delivery choice groups name their method types#

A buyer-authenticated delivery quote showed how a choice group could be delivered only through its options, and a group has no options until the buyer gives what its methods need, such as an address. A checkout that offers only local delivery could not tell that from a shipping checkout until then.

  • Choice groups list method_types. The values are shipment, pickup, and local_delivery, each listed once, in the order the methods display. The field is on quotes, on the session's fulfillment, and in merchant-authenticated responses.
  • A type stays listed while its methods need input or cannot serve the current input, so a page can name what does not reach the buyer. options lists what the buyer can choose now.
  • A method whose type the group's items do not allow is left out. The list is [] when no method can serve the group.

See Build your own checkout.

  • API
  • Delivery
  • Checkout

Delivery quotes name required recipient fields before an address#

A quote listed a method's required recipient fields in input_requirements only once the method had an option. Before the buyer gave an address, a local delivery that needs the recipient's phone listed only the address, although the checkout session named recipient.phone before any quote existed. An Apple Pay or Google Pay sheet opened from that quote did not ask for a phone, so payment then failed with DELIVERY_RECIPIENT_REQUIRED.

  • A method waiting on the buyer's address or store lists the recipient fields it requires, with purpose selection, next to the quote requirement for the address. The same applies while the buyer must correct an address Flint could not use.
  • Those requirements name the method's choice group and have no delivery_option_ids. A quote that prices the method lists the requirement for its option instead, as before.
  • A method that cannot serve a usable address lists none. A choice group's own input_requirements still lists only what the group needs before it can be priced.

The change applies to delivery quotes, the checkout session's fulfillment, and delivery previews. See Delivery quotes.

  • API
  • Delivery
  • Checkout

Delivery recipient details are checked at payment#

Selecting a delivery option no longer fails when the recipient is missing a field the option requires, such as a phone for local delivery. Apple Pay and Google Pay reveal the buyer's name and phone only after the buyer approves the payment sheet, so a wallet flow can now select delivery from the partial address first and add the recipient afterwards.

  • Delivery selections carry input_requirements. Each entry is a required recipient field the selection is still missing, with the same delivery_input_requirement_id the quote gave that option. It is [] when nothing is missing.
  • Paying the order returns 400 DELIVERY_RECIPIENT_REQUIRED while a required field is missing, before any payment is attempted. param names the first field, such as delivery_selection.recipient.phone, and remediation.missing_or_invalid_fields lists all of them.
  • To add the fields, select the same choices from the same quote with the full recipient, and send the current selection's ID as expected_delivery_selection_id. The price does not change.

A malformed email or phone you do send is still rejected when you select, with DELIVERY_REQUEST_INVALID. An integration that sends the required fields when it selects sees no difference. If yours counted on the selection rejecting a missing field, read input_requirements after selecting, or handle DELIVERY_RECIPIENT_REQUIRED from POST /v1/orders/{order_id}/pay. See Delivery selections.

  • API
  • Delivery
  • Checkout

Quotes offer delivery and pickup windows only while they can be chosen#

A schedule_window method offers its windows in each option's offered_windows. Until this release a quote also offered the part of a window already under way, starting and expiring at the moment of the quote, so choosing it failed with DELIVERY_WINDOW_EXPIRED. A window could also be chosen until it started, whatever the method's preparation_lead_time_seconds.

  • Every offered window can be chosen. A window's expires_at is preparation_lead_time_seconds before it starts, or the same-day cutoff when that is earlier, and a quote offers only windows whose expires_at is still ahead. With no lead time, a window can be chosen until it starts.
  • A window already under way is not offered. The first window is the next one the lead time allows, so start_at and end_at are always the times you configured.
  • A chosen window ends the selection at its expires_at. Pay before then, or quote and select again.

A method with one long window a day, such as store hours, now offers the next day's window once the day's window has started. To keep taking orders through the day, split it into shorter windows. See Delivery options.

  • API
  • Delivery

Exact ZIP codes match verified addresses#

Address verification returns a full US address with its ZIP+4, such as 07030-5788. Until this release a postal_code condition with an exact value such as 07030 compared the whole code, so it never matched a verified address: a method limited to exact ZIP codes read as outside the delivery area everywhere, while the same codes as prefix values worked. That applied to zones, method eligibility, rate-table when conditions, and pickup searches by buyer_location.

  • A five-digit ZIP code matches the ZIP+4 codes within it. 07030 matches 07030, 07030-5788, and 070305788. This applies to ZIP codes in the US and in US territories, such as Puerto Rico.
  • A ZIP+4 value matches only that ZIP+4. 07030-5788 does not match 07030 or 07030-5789.
  • Prefixes and other countries are unchanged. A prefix value still matches every code that starts with it, and codes outside the US compare whole, in uppercase without spaces or hyphens.

Check any method that excludes ZIP codes with not. An address in an excluded five-digit ZIP was not excluded before when verification returned its ZIP+4. It is now. See Delivery configuration.

  • API
  • Delivery
  • Checkout

Pickup search takes the buyer's current choice and lists stores that track no inventory#

POST /v1/checkout-sessions/{checkout_session_id}/query-pickup-availability had two problems that left buyers without stores to choose from.

  • The search takes the current delivery selection. A search that sent the checkout's current expected_delivery_selection_id returned 409 DELIVERY_PICKUP_AVAILABILITY_CHANGED, and only a search that named no selection answered, so a buyer who had chosen a store could not look for another. The search now compares the ID with the selection GET .../delivery-selections/current returns, like quote creation does, and does not change the choice. Send null only when the checkout has no selection: naming none while one is current now returns the same 409.
  • Stores without inventory allocation are listed. The search left out every store whose inventory.allocation_status was not active, although quote creation priced pickup at those stores for items without tracked inventory. A store now needs allocation only to supply items with tracked inventory. For those items, a store without it is listed with an unavailable outcome and inventory_insufficient, and merchant-authenticated searches name it in merchant_diagnostics with PICKUP_LOCATION_INVENTORY_UNAVAILABLE.

See Delivery configuration.

  • API
  • Errors

Request bodies up to 1 MiB#

Until this release a request body over 8 KB was refused before it reached the API. The response was an HTML 403 Forbidden page with no request_id, so an order with a few dozen line items or a delivery method with a week of hourly windows could not be created.

  • Bodies up to 1 MiB (1,048,576 bytes) are accepted. This is the limit on every endpoint unless its reference names a lower one.
  • A larger body returns 413 in the standard error envelope, with type set to invalid_request_error, code set to REQUEST_BODY_TOO_LARGE, and a message that states the limit. REQUEST_BODY_TOO_LARGE replaces RESOURCE_LIMIT_EXCEEDED as the code for an oversized body.
  • Split a larger write into several requests. For example, create an order with its first line items and add the rest with POST /v1/orders/{order_id}/line-items.

See Error handling.

  • API
  • Delivery

State conditions match the state a buyer sends#

state conditions store ISO 3166-2 codes such as US-NY, while buyers, address forms, Apple Pay, Google Pay, and address validation send the state as NY. Until this release Flint compared the two as written, so a state condition matched only a state sent as US-NY, and address validation rewrites that to NY for a full address. That applied to zones, to method eligibility, to rate-table when conditions, and to pickup searches by buyer_location.

  • Matching reads the address's state as a subdivision of its country. NY, ny, US-NY, and New York in a US address all match US-NY. Outside the US and Canada, send the subdivision code, with or without the country prefix. A state that names no subdivision of the address's country matches no state condition.
  • Conditions accept two-letter US and Canadian codes. Send NY or ON and Flint stores US-NY or CA-ON, so reads return the ISO 3166-2 code. NY and US-NY in the same list are rejected as duplicates. Other countries still need the full code, such as DE-BY.

Check any method that excludes states with not. A zone such as "the US but not US-AK or US-HI" excluded no one before, so its methods were offered in Alaska and Hawaii. They no longer are. See Delivery configuration.

  • Breaking change
  • API
  • Checkout

Checkout sessions always collect an order#

Every checkout session now collects an order, and pays through it. POST /v1/checkout-sessions takes exactly one of order_id, quick_pay_item, or subscription_plan_id.

Breaking: payment_intent_id is no longer a create field, and a request that sends it returns 400 with UNKNOWN_FIELD. Checkout sessions no longer carry a payment_intent_id field; payment_intent_ids still lists the order's PaymentIntents. A checkout session credential no longer reads or confirms a standalone PaymentIntent, and the error codes for PaymentIntent checkouts, such as PAYMENT_INTENT_CHECKOUT_ALREADY_EXISTS, are retired.

To charge an amount on a Flint-hosted page, create the session with quick_pay_item, and Flint creates a one-line order for it. To collect a PaymentIntent you create on its own, use Stripe Elements on your page and confirm through Flint, as Server-confirmed payments describes.

A Return resolution offers a checkout session only for the balance of its replacement Order. A correction that would leave the buyer owing money is rejected, as Return resolutions describes.

  • API

New in the API: week of September 21, 2026#

45 additions across Checkout sessions, Refunds, and 10 more areas. These are additions, so nothing you already use needs to change.

Related: Checkout sessions always collect an order

Checkout sessions
11 additions
Refunds
7 additions
Orders
6 additions
Fulfillment records
4 additions
Money movement
4 additions
Customer account
3 additions
  • Query parameter
    usageon GET /v1/me/payment-methods

    Filter by usage: off_session payment methods can be charged without the buyer; on_session ones were saved by buyers for faster checkout.

  • Request field
    typeon POST /v1/me/payment-methods

    string

  • Error code

    A subscription that is not canceled still uses the payment method.

Payment methods
2 additions
  • Query parameter
    usageon GET /v1/payment-methods

    Filter by usage: off_session payment methods can be charged without the buyer; on_session ones were saved by buyers for faster checkout.

  • Field

    When Flint may charge the payment method. off_session payment methods can pay subscriptions, automatic invoices, and other charges without the buyer, and can be a customer's default. on_session payment methods were saved by the buyer in checkout for faster checkout at this merchant; Flint charges them only in checkouts the buyer completes and refuses them for any charge without the buyer and as a default.

Settings
2 additions
Invoices
1 addition
Subscriptions
1 addition
Webhook events
1 addition
  • Values
    Payout.initiated_by

    manualplatformscheduled

Errors
3 additions
  • API
  • Invoices
  • Refunds

Refund a credit note on a paid invoice#

A credit note can now return money. Until this release a note could only be allocated against an outstanding balance, so a correction to a paid invoice meant a credit note for the paperwork and a separate refund against the order. Now the note creates the refund, and the refund stays linked to it.

To issue a draft and refund it in one call, add refund to POST /v1/credit-notes/{credit_note_id}/issue. The response keeps the credit note at data and adds the refund at data.refund. For a note that is already issued, call POST /v1/credit-notes/{credit_note_id}/refunds, which returns the Refund resource. Both accept amount_money, reason, and reason_message. Leave amount_money out to refund all available credit. The money goes back to the original payment methods; payments you recorded manually are not eligible.

Both calls require a caller-chosen Idempotency-Key, and both need commerce.credit_notes.write and commerce.refunds.write. Flint keeps the key for the life of the refund. To recover a response you lost, call GET /v1/credit-notes/{credit_note_id}/refunds?idempotency_key=..., which needs both read scopes.

Credit notes carry two new fields. pending_refund_money reserves credit while a refund is unresolved, and refunded_money records what succeeded. Both reduce unallocated_money, and a failed or canceled refund releases its amount. Refunds created this way carry credit_note_id and invoice_id, and they fire the existing refund.* events. The invoice stays paid, with refund_status and refunded_money tracking the refund as before.

A credit note cannot be voided while a refund on it is pending or after one succeeds; the call returns CREDIT_NOTE_HAS_ALLOCATIONS. A refund larger than the available credit returns CREDIT_NOTE_REFUND_EXCEEDS_BALANCE, and an ineligible invoice or note returns CREDIT_NOTE_REFUND_INVALID.

These changes are additive. See Refund a paid invoice for the walkthrough.

  • API
  • Invoices
  • Tax

Invoice late fees and automatic tax on invoices#

Late fees are now part of the invoice balance. invoice.late_fee_due still fires after the grace period on the invoice's payment term, and it remains a notice. To charge it, call POST /v1/invoices/{invoice_id}/late-fees, which assesses the frozen policy against the current unpaid principal, once per invoice or per overdue schedule entry. Send invoice_schedule_entry_id for a scheduled invoice and {} for an unscheduled one.

POST /v1/invoices/{invoice_id}/late-fees/{invoice_late_fee_id}/waive waives the unpaid remainder and requires a reason, which is visible only to you. A waiver leaves collected money in place; refund it through the Refunds API.

Invoice responses carry late_fees, with each fee's calculation inputs, assessed amount, paid amount, waived amount, and outstanding amount. An assessed fee raises the outstanding balance and appears as a dated addendum on the hosted invoice and the PDF. The issued line items, tax, and original total do not change. Subscribe to invoice.late_fee_assessed and invoice.late_fee_waived to track the balance, and late_fee_assessed and late_fee_waived join the invoice event timeline.

This release supports fixed and percentage policies, without recurring fees, compounding, or tax on the fee.

Quaderno-connected automatic tax also works on invoices now. Issue freezes the calculation, and the invoice takes one payment for its full balance: a schedule for less than the balance fails with AUTOMATIC_TAX_INVOICE_INSTALLMENTS_UNSUPPORTED, and a partial payment fails with AUTOMATIC_TAX_PARTIAL_PAYMENT_UNSUPPORTED. Editing the backing order after issue fails collection with INVOICE_TAX_SNAPSHOT_STALE; void the invoice and issue it again. Use external tax for a deposit, installments, or a balance collected in pieces.

  • Breaking change
  • API

API version 2026-09-07#

Balance transactions report fees as positive charges that match the payment's processing_fee_money, name their timestamp occurred_at, and identify the related resource by its own ID field. Tax categories split into line_item_tax_category for items and charge_tax_category for charges, so the field name tells you which set of values applies.

  • Your integrationKeeps working as it does today. Nothing moves to 2026-09-07 until you upgrade.
  • Affects130 endpoints and 82 webhook events. See the ones you use
  • Upgrade byNo deadline. 2026-02-01 is supported until at least September 7, 2028.
  • Roll backFor 72 hours after you switch, the Versions screen can restore your previous version.

What changes

  1. Balance transaction fee charges are positive and fee credits are negative.

    Response and request shape
    Before (2026-02-01)
    {
      "amount_money": {
        "amount": 5000,
        "currency": "USD"
      },
      "fee_money": {
        "amount": -175,
        "currency": "USD"
      },
      "net_money": {
        "amount": 4825,
        "currency": "USD"
      }
    }
    After (2026-09-07)
    {
      "amount_money": {
        "amount": 5000,
        "currency": "USD"
      },
      "fee_money": {
        "amount": 175,
        "currency": "USD"
      },
      "net_money": {
        "amount": 4825,
        "currency": "USD"
      }
    }
    What to do
    Read a positive fee_money as a fee charged and a negative one as a fee returned. net_money is unchanged.
  2. Balance transactions use occurred_at for the transaction timestamp.

    Response and request shape
    Before (2026-02-01)
    {
      "balance_transaction_id": "btxn_test",
      "created_at": "2026-09-06T12:34:56Z"
    }
    After (2026-09-07)
    {
      "balance_transaction_id": "btxn_test",
      "occurred_at": "2026-09-06T12:34:56Z"
    }
    What to do
    Use occurred_at instead of created_at.
  3. Tax categories use line_item_tax_category for items and charge_tax_category for charges.

    Response and request shape
    Before (2026-02-01)
    {
      "tax_category": "prepared_food"
    }
    After (2026-09-07)
    {
      "line_item_tax_category": "prepared_food"
    }
    What to do
    Replace tax_category with line_item_tax_category on product variants, modifiers, bundles, and line item taxes, and with charge_tax_category on delivery methods and charge taxes.
    Affects

    128 endpoints across 16 resources, most in Orders (21), Subscriptions (21), Invoices (15), Customer account (13).

    Webhook events:bundle.archivedbundle.createdbundle.updatedcheckout_session.closedcheckout_session.completedcheckout_session.expiredcheckout_session.invalidateddelivery_method.activateddelivery_method.archiveddelivery_method.createddelivery_method.deactivateddelivery_method.updatedinvoice.collection_block_resolvedinvoice.collection_blockedinvoice.createdinvoice.creditedinvoice.delivery_failedinvoice.delivery_succeededinvoice.issue_failedinvoice.issuedinvoice.late_fee_assessedinvoice.late_fee_dueinvoice.late_fee_waivedinvoice.manual_payment_recordedinvoice.manual_payment_reversedinvoice.marked_uncollectibleinvoice.overdueinvoice.paidinvoice.partially_paidinvoice.partially_refundedinvoice.refundedinvoice.reminder_dueinvoice.reopenedinvoice.sentinvoice.updatedinvoice.voidedmodifier_group.archivedmodifier_group.createdmodifier_group.updatedmodifier_set.archivedmodifier_set.createdmodifier_set.updatedorder.closedorder.createdorder.inventory_exception_createdorder.inventory_exception_resolvedorder.paidorder.partially_paidorder.payment_authorization_canceledorder.payment_authorization_expiredorder.payment_authorizedorder.payment_capturedorder.refundedorder.updatedpayment_link.createdpayment_link.updatedproduct.archivedproduct.createdproduct.updatedproduct_variant.archivedproduct_variant.createdproduct_variant.updatedsubscription.activatedsubscription.canceledsubscription.cancellation_scheduledsubscription.createdsubscription.cycle_skippedsubscription.delivery_action_requiredsubscription.delivery_pause_upcomingsubscription.delivery_updatedsubscription.dunning_exhaustedsubscription.past_duesubscription.pausedsubscription.payment_failedsubscription.payment_succeededsubscription.reactivatedsubscription.renewal_upcomingsubscription.resumedsubscription.trial_endingsubscription.updated

How to upgrade

  1. Read your impact report on the Versions screen. It lists the routes and webhook events you use that change.
  2. Test in test mode: send Flint-Version: 2026-09-07, or install the SDK release built for it.
  3. Move your merchant default on the Versions screen.
  4. Move webhook endpoints and partner apps that pin an older version.

Read the upgrade guide

  • Breaking change
  • API
  • Inventory

Shared money inputs and ad hoc inventory tracking#

Money inputs use Money: an integer amount in minor units and a required currency. Delivery pricing maps now require each amount's currency to match its map key, for example "USD": {"amount": 900, "currency": "USD"}. Requested tips and flat order taxes still require an amount of at least 1 minor unit.

Breaking: ad hoc order lines no longer accept inventory_requirement. A line with inventory_demands is tracked; a line without them is not. Order line snapshots expose the resolved setting at inventory_snapshot.inventory_tracking.

For tracked ad hoc lines, each inventory_demands entry accepts only inventory_item_id and quantity_per_line_item_unit. Bundle component and component snapshot IDs remain available on responses. The catalog inventory relationship revision is no longer part of the public response.

  • Breaking change
  • API
  • Inventory

Multi-location inventory, allocation, and reservations#

Inventory has its own API. Stock is tracked per location, demand is routed across locations, and checkout holds real claims instead of decrementing a counter.

  • Inventory API: inventory items and per-location levels, adjustments, safety stock, policies, transfers, counts, and merchant-owned reservations. Read levels to inspect stock; create a reservation when your integration needs to hold it.
  • Locations API: create and manage locations, publish their geography, and enable them for inventory. Identity, geography, and the inventory block are versioned independently so concurrent writes cannot silently overwrite each other.
  • Allocation policies: rank locations into priority groups to decide which one serves demand. Single-location merchants can skip policies entirely and route to a fixed location.
  • New webhook events use the inventory. prefix, alongside order.inventory_exception.created and order.inventory_exception.resolved. See the event catalog.

Breaking: product variants no longer carry their own stock count. quantity_available and enforce_inventory_limit have been removed from ProductVariant. A variant now declares inventory_tracking (not_tracked or tracked) and points inventory_item_id at an inventory item whose quantities live on per-location levels. Tracking is never inferred from a product being physical, so a variant you do not explicitly mark tracked sells without a stock check.

Idempotency keys are required on quantity-changing commands. Adjustments and reservation transitions reject a request with no Idempotency-Key. Flint will not generate one, because a generated key cannot protect the case that matters: the request succeeded and you never received the response. Reusing a key with identical input returns the original result and its original level projections.

  • Breaking change
  • Payments
  • API

One all-in processing fee on every payment#

Payments now report a single Flint processing fee, and the fields around it are provider-neutral.

  • processing_fee_money on payment intents replaces platform_fee_money. It is Flint's complete all-in price to process the payment, not a provider cost with a markup on top, so your net is captured_money minus this one field. The fee is final at card capture or ACH success; canceled payments and failures before success have none, and refunds do not return or revise it.
  • Balance transaction fee_money is documented as Flint's own fee on the movement. Refund, dispute, and return entries carry no Flint fee; an instant payout carries its own.
  • processor_fee and flint_fee balance transaction types are no longer part of the public ledger. What Flint pays its providers is Flint's cost and never appears as a deduction on your payments.

New Processing fees and Reconciliation guides cover the fee lifecycle and matching deposits back to the ledger.

  • Docs
  • API

New developer docs with a generated API reference#

The developer docs have been rebuilt:

  • Generated API reference: every endpoint is now documented from the same contract the server runs, with request and response schemas, realistic examples, and cURL plus Node SDK samples generated from that contract, so they do not drift.
  • Webhook event catalog at /docs/webhooks/events and an error code catalog, both generated from server code.
  • Agent-ready docs: append .md to any page URL for raw Markdown, fetch /llms.txt or /llms-full.txt, or connect your editor to the docs MCP server at https://developers.withflintpay.com/mcp.
  • Dark mode and a refreshed design that matches the Flint dashboard.