Skip to content

Versions and write fences

A resource you can change carries an integer version that increases with each change to its own fields. Send the version you read as expected_version, and Flint applies your write only if nothing changed since.

Read the version#

Most changeable resources carry version: orders, payment intents, refunds, returns, fulfillments, shipments, packages, invoices, credit notes, subscriptions, checkout sessions, customers, products, and more. A resource's reference lists the field when it has one. A dispute or a payout itself has no version.

A version covers the resource's own fields, not records expanded inside it. A change to an expanded order doesn't move the version of the return that embeds it.

Send expected_version#

Put expected_version in the body of a PATCH or an action POST, and in the query string of a DELETE, where 39 routes accept it. Flint compares it before changing anything, so a stale value changes nothing.

cURL
curl -X PATCH https://api.withflintpay.com/v1/customers/cus_1kmn0aExample \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: customer-note-cus_1kmn0aExample-v4" \
  -H "Content-Type: application/json" \
  -d '{
    "expected_version": 4,
    "internal_note": "Prefers pickup on Saturdays."
  }'
cURL
curl -X DELETE "https://api.withflintpay.com/v1/bundles/bun_1kmn0aExample?expected_version=3" \
  -H "Authorization: Bearer YOUR_API_KEY"

It is optional unless the route's reference says it is required. A few routes require it, and some require it only for certain changes, such as a customer's tax_identity. Send it whenever you read before you write, even where it is optional.

Other fences#

Some writes assert a different value than the resource's own version:

  • A parent's version. A create that hangs off a parent sends expected_{parent}_version, never expected_version. A return resolution sends expected_return_version, the version of the return it resolves.
  • A balance. Paying an order sends expected_outstanding_money, the balance you reviewed.
  • An API version. Updating a webhook endpoint or a partner app sends expected_api_version with api_version, to apply the change only if the current API version still matches.
  • Physical stock. Inventory count and adjustment lines send expected_physical_version, the physical version of the inventory level you read before counting.
  • A delivery selection. Delivery quotes and selections send expected_delivery_selection_id, and so does the DELETE that clears the current selection.

Each field's reference says whether it is required and what a mismatch returns.

When the version moved#

A stale assertion versus a lost race. VERSION_CONFLICT (after_refresh) means the expected_version, or another expected_* field you sent, no longer matches. param names the field, and details[0] carries the current value, such as current_version. Read the resource, rebuild the change on its current state, and send it with the new value and a new key. Without param, Flint found the resource changed during a write that had no assertion; read current state before you retry, and check what applied if the write had started. CONCURRENT_MODIFICATION (same_request) means another write won a race while your basis was still current. Nothing changed, so resend the same request with the same key.

Here is the rebuild for a customer note, where the conflict means another writer changed the note first:

JavaScript
async function appendCustomerNote(customerId, line) {
  for (let attempt = 1; attempt <= 3; attempt++) {
    const { data: customer } = await flintFetch(`/v1/customers/${customerId}`);
    try {
      return await flintFetch(`/v1/customers/${customerId}`, {
        method: "PATCH",
        headers: { "Idempotency-Key": crypto.randomUUID() },
        body: JSON.stringify({
          expected_version: customer.version,
          internal_note: [customer.internal_note, line].filter(Boolean).join("\n"),
        }),
      });
    } catch (err) {
      // Another writer changed the customer: read it again and rebuild.
      const stale = err instanceof FlintApiError && err.retry === "after_refresh";
      if (stale && attempt < 3) continue;
      throw err;
    }
  }
}

Error handling lists the other 409 codes.

Versions in webhooks#

An event's data.object is a snapshot, and delivery is at least once and unordered. Compare versions, not arrival order: skip a snapshot whose version is lower than one you already applied. A dispute or a payout itself has no version, so read the current state before an irreversible step such as shipping, refunding, or granting access.

Next steps#

Error handlingEvery error code, remediation.retry, and which 409 needs which response.IdempotencySend a key on every write and reuse it when you retry.Statuses and lifecyclesWhat each status means and which actions are allowed next.WebhooksDelivery, retries, and deduplication.

Was this helpful?