SpendRock Docs

Idempotency

Transaction IDs are generated by the client and double as idempotency keys, so retries never duplicate.

Networks fail. If a request to add a transaction times out, you don't know whether it was saved. SpendRock makes retrying safe: the client picks the transaction's ID, and that ID is the idempotency key.

How it works

When you create a transaction with POST /transactions, you send an id: a new ULID you generate.

You sendYou get
A new id201 Created with the transaction and the recomputed month(s).
The same id and the same body again200 OK with the stored result. Nothing is created twice.
The same id with a different body409 Conflict with the code id_conflict.

So the rule for clients is simple: generate the ID once, before the first attempt, and reuse it for every retry of that same transaction. Never generate a new ID for a retry.

import { ulid } from 'ulid';

const txn = {
  id: ulid(), // once, up front
  type: 'expense',
  amount: 5420,
  date: '2026-12-03',
  merchant: 'Corner Grocery',
  budget_month: '2026-12',
  splits: [{ item_id: groceriesId, amount: 5420 }],
};

// Safe to call as many times as needed: at most one transaction is created.
async function save() {
  const res = await fetch(`${SR_API}/transactions`, {
    method: 'POST',
    headers: { Authorization: `Bearer ${SR_TOKEN}`, 'Content-Type': 'application/json' },
    body: JSON.stringify(txn),
  });
  if (res.status === 201 || res.status === 200) return res.json();
  throw new Error(`${res.status}: ${(await res.json()).code}`);
}

This is exactly how the mobile app saves expenses you add while offline: they're stored on the device with their ID and uploaded when the connection returns, and nothing is lost or duplicated.

Other requests

  • Updates (PATCH) set fields to the values you send, so repeating one is harmless.
  • Deleting a transaction is a soft delete: nothing is lost, and POST /transactions/{id}/restore brings it back.
  • Other creates (groups, items, households, invites) use server-generated IDs and are not idempotent. If one times out, list or read before retrying. Names of groups and items are unique within a month, so a duplicate create usually fails with name_taken rather than creating a copy.

On this page