File a claim
const url = 'https://example.com/v1/expenses/claims';const options = { method: 'POST', headers: { 'Idempotency-Key': 'example', Authorization: 'Bearer <token>', 'Content-Type': 'application/json' }, body: '{"group_id":"2489E9AD-2EE2-8E00-8EC9-32D5F69181C0","payout_channel_id":"2489E9AD-2EE2-8E00-8EC9-32D5F69181C0","title":"example"}'};
try { const response = await fetch(url, options); const data = await response.json(); console.log(data);} catch (error) { console.error(error);}curl --request POST \ --url https://example.com/v1/expenses/claims \ --header 'Authorization: Bearer <token>' \ --header 'Content-Type: application/json' \ --header 'Idempotency-Key: example' \ --data '{ "group_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "payout_channel_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "title": "example" }'Nobody files for anybody else (Ruling R20), so there is no claimant field: it is the caller, so a claim made with an API key is filed by the key’s owner. Submitting, approving and paying are people’s decisions and are not part of the public API. Idempotency-Key is required when sent with an API key. There is no currency either — a claim is reimbursed in the organization’s base currency only (Ruling R5) — and no total, which is its lines’ sum. The response carries ETag: "1", the revision every later write against this claim must send back in If-Match. Requires expenses.claim.create.
Authorizations
Section titled “Authorizations”Parameters
Section titled “Parameters”Header Parameters
Section titled “Header Parameters”Names this attempt. Required for every write made with an API key: reuse it to retry (the first attempt’s answer is replayed, with Idempotent-Replayed: true, and nothing runs twice); change it for a different request. The same key over a different request is 409 idempotency_key_reused. Keys are kept for 24 hours and are scoped to the API key that sent them.
Request Bodyrequired
Section titled “Request Bodyrequired”object
Example generated
{ "group_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "payout_channel_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0", "title": "example"}Responses
Section titled “Responses”The claim as filed
The answer to a claim read AND to every claim mutation: a client that has just added a line needs the new total, the new revision and the new set of buttons, and a second GET to find them is a round trip and a race.
object
Every exit of the current state, whether THIS caller may take it, and why not. A client renders buttons from this and never evaluates a gate itself (spec 13.4).
object
Locale to text. “en” is required; keys are supported UI locales.
object
object
Not_reporter, no_band_covers_amount, not_in_band, self_approval, dispute_raiser or already_signed (a sign-off step, when the actor has already signed everything they may).
Reason is one of member_inactive, position_retired, vacant, no_primary_position, no_unit_head, above_root, only_the_claimant, no_band_covers_amount, unallocated (part of the amount carries no account on the step’s axis) or no_account_approver (the account, and every ancestor, has no approver).
object
The shares this edge lets the actor sign, on a sign-off step.
object
A decimal amount as a string. Never a JSON number.
Allowed only as the sole-owner exception (Ruling O-3a): the organization’s one active member deciding their own sales document. requires_reason is then true, and the decision is recorded with authority_via sole_owner_exception. The reason is English or Bahasa Malaysia, at most 500 characters (422 field=reason otherwise); a blank one is refused with 422 sole_owner_reason_required.
object
The distinct categories of the active lines, in order of each category’s first line. [] for a claim with no active line.
Who filed it. Nobody files for anybody else (Ruling R20).
The org unit of the claimant’s primary position on the day of the claim’s latest submission (its most recent move out of a draft state, in the organization’s timezone), or today while the claim is in draft, a returned claim included. null when the claimant held no primary position then.
When the claim was filed. The list’s order and its month filter are on this.
Always the organization’s base currency (Ruling R5).
The supplier of the first active line that names one; null when none does.
The active lines are in more than one currency, so there is no single original total.
Set when every active line is in one currency other than the claim’s base currency; null otherwise.
The active lines’ amounts in original_currency, summed by the server. null with it.
The claimant’s preferred channel; the payer may override it.
The distinct accounts on the project analytic axis that the active lines’ shares name, in order of first line, then name. [] when none.
EC-YYYY-NNNN, allocated from the claim sequence (Ruling R19).
The optimistic lock. It is also the ETag, and every write against this claim must send it back in If-Match (Ruling R29).
The closed set of workflow state semantics the ledger reacts to.
The workflow instance’s state key. It changes only through a transition.
When it first left draft: the first decision out of a draft-semantic state, from the decision log. null until then.
The active lines’ base amounts. Kept by the platform, never sent in.
On scope=to_act only: the shares of this claim the caller may sign now.
The decision log, oldest first.
One entry of the claim’s decision log, read as written and never re-resolved.
object
Whose authority the actor decided on — themselves, or the member who delegated.
Every line in line order, withdrawn ones included.
object
What was spent, in currency.
What the organization reimburses, converted once at the expense date.
The documents ACTIVELY supporting this line, oldest attachment first.
The references of the matching claims that the CALLER may read themselves (Ruling R21), in the order they were filed. A match the caller may not read is left out, so this can be empty while possible_duplicate is true. Never null.
A decimal rate or percentage as a string, up to 10 places. Never a JSON number.
Where the rate came from: identity, reference, override, or bank_statement (derived from a bank-charged amount). Frozen with the line (Ruling R5).
True when a document supporting this line has the same content (SHA-256) as a document supporting an active line of ANOTHER claim that has left draft and is not rejected or cancelled — for example the same e-Invoice PDF uploaded by two members. Computed on every read, so it also appears on the earlier claim once a later one is submitted. A signal for the approver, never a refusal: a bill honestly split between colleagues looks the same. Shown to anybody who may read the claim.
Kilometres for mileage, days for per-diem.
How a receipt line becomes the base amount the organization reimburses. expense_date_rate converts at the rate in force on the expense date (the only basis for a line in the base currency); bank_charged reimburses exactly the figure the claimant’s bank charged.
object
A decimal rate or percentage as a string, up to 10 places. Never a JSON number.
SST borne, backed out of the gross (Ruling R13). Recorded, never recovered.
The four kinds of line a claim can carry (Ruling R11).
True when the line has been taken out of the claim. The row stays — a withdrawn line is the claim’s history (Ruling R16) — and it no longer counts towards the total.
The claim’s payouts, newest first. Masked (Ruling R44).
One payout, frozen (spec 9.4), as the API renders it. This is the ONLY shape a settlement leaves the server in: masked displays, no value, no ciphertext (Ruling R44). additionalProperties is false on purpose — a snapshot, value or sealed member added here later would be a leak, and the schema refuses it rather than describing it.
object
The last few digits of the account paid, lifted from the snapshot’s plaintext.
A decimal amount as a string. Never a JSON number.
One frozen payout field as a reader of the claim sees it: what it is called, and the MASKED rendering. There is deliberately no member here that could carry the value (Ruling R44).
object
The masked form, e.g. “****6789”.
One string per locale. en and ms are required; zh is optional.
object
Every sign-off required to leave the current state, signed or not. Empty on any state that is not a sign-off step.
object
A decimal amount as a string. Never a JSON number.
Reason is one of member_inactive, position_retired, vacant, no_primary_position, no_unit_head, above_root, only_the_claimant, no_band_covers_amount, unallocated (part of the amount carries no account on the step’s axis) or no_account_approver (the account, and every ancestor, has no approver).
object
object
A decimal amount as a string. Never a JSON number.
The project’s assignment the authority came through; null under a default grant or the claimant’s superior.
The step being signed.
Example
{ "available_transitions": [ { "gate": "reporter", "signs": [ { "amount": "1234.5600" } ] } ], "claim": { "original_total": "1234.5600", "semantic": "draft", "total": "1234.5600", "your_share": "1234.5600" }, "decisions": [ { "via": "reporter" } ], "lines": [ { "amount": "1234.5600", "base_amount": "1234.5600", "document_basis": "einvoice_company", "fx_rate": "4.2000000000", "quantity": "4.2000000000", "reimbursement_basis": "expense_date_rate", "shares": [ { "percentage": "4.2000000000" } ], "tax_amount": "1234.5600", "type": "receipt" } ], "settlements": [ { "amount": "1234.5600" } ], "signoffs": [ { "amount": "1234.5600", "signed": { "amount": "1234.5600", "via": "grant" } } ]}Headers
Section titled “Headers”The claim’s revision, quoted. Send it back in If-Match on the next write against this claim.
The body is not a single JSON object of this endpoint’s shape: it is missing, malformed, carries a field of the wrong type, or carries a field this endpoint does not accept. Unknown fields are refused, not ignored; the detail names the offending field where there is one.
object
A stable refusal code the client branches on, where the status alone is not enough: owner_role_manage_required and self_lockout on the role editor’s 409s; revision_mismatch (a stale If-Match) and document_bound_elsewhere (the document backs another record) on an expense claim’s 409s.
Example generated
{ "code": "example", "detail": "example", "instance": "example", "status": 1, "title": "example", "type": "example"}Not authenticated, or the session is no longer valid.
object
A stable refusal code the client branches on, where the status alone is not enough: owner_role_manage_required and self_lockout on the role editor’s 409s; revision_mismatch (a stale If-Match) and document_bound_elsewhere (the document backs another record) on an expense claim’s 409s.
Example generated
{ "code": "example", "detail": "example", "instance": "example", "status": 1, "title": "example", "type": "example"}The caller does not hold the permission this operation requires.
object
A stable refusal code the client branches on, where the status alone is not enough: owner_role_manage_required and self_lockout on the role editor’s 409s; revision_mismatch (a stale If-Match) and document_bound_elsewhere (the document backs another record) on an expense claim’s 409s.
Example generated
{ "code": "example", "detail": "example", "instance": "example", "status": 1, "title": "example", "type": "example"}The Idempotency-Key was already used for a different request (idempotency_key_reused), or the same request is still being processed (idempotency_in_progress; retry after Retry-After), or the records are in another state (conflict).
object
A stable refusal code the client branches on, where the status alone is not enough: owner_role_manage_required and self_lockout on the role editor’s 409s; revision_mismatch (a stale If-Match) and document_bound_elsewhere (the document backs another record) on an expense claim’s 409s.
Example generated
{ "code": "example", "detail": "example", "instance": "example", "status": 1, "title": "example", "type": "example"}Title is blank, over-long or in a script that is not a record language; or the group named is unknown or retired (validation); or the Idempotency-Key header is missing or too long (idempotency_key_required).
object
A stable refusal code the client branches on, where the status alone is not enough: owner_role_manage_required and self_lockout on the role editor’s 409s; revision_mismatch (a stale If-Match) and document_bound_elsewhere (the document backs another record) on an expense claim’s 409s.
Example generated
{ "code": "example", "detail": "example", "instance": "example", "status": 1, "title": "example", "type": "example"}A rate limit was reached: per API key, per organization across its keys, or per source address for repeated failed key authentication. Retry after the number of seconds in Retry-After. Every answer on a key carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset.
object
A stable refusal code the client branches on, where the status alone is not enough: owner_role_manage_required and self_lockout on the role editor’s 409s; revision_mismatch (a stale If-Match) and document_bound_elsewhere (the document backs another record) on an expense claim’s 409s.
Example generated
{ "code": "example", "detail": "example", "instance": "example", "status": 1, "title": "example", "type": "example"}Headers
Section titled “Headers”Seconds until a retry may succeed.