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 -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 -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, neverexpected_version. A return resolution sendsexpected_return_version, theversionof 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_versionwithapi_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 theDELETEthat 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:
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.
