Skip to content

File a claim

POST
/expenses/claims
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.

Idempotency-Key
required
string
>= 1 characters <= 255 characters

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.

Media typeapplication/json
object
group_id
string format: uuid
nullable
payout_channel_id
string format: uuid
nullable
title
required
string
>= 1 characters <= 200 characters

Example generated

{
"group_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"payout_channel_id": "2489E9AD-2EE2-8E00-8EC9-32D5F69181C0",
"title": "example"
}

The claim as filed

Media typeapplication/json

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
available_transitions
required

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).

Array<object>
object
action_key
required
string
allowed
required
boolean
gate
required
string
Allowed values: reporter target source advance
labels
required

Locale to text. “en” is required; keys are supported UI locales.

object
key
additional properties
string
<= 200 characters
refusal
object
code
required

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).

string
detail
required
string
gaps
Array<object>

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
analytic_account_id
string format: uuid
axis_id
string format: uuid
grant_id
required
string format: uuid
member_id
string format: uuid
position_id
string format: uuid
reason
required
string
requires_reason
required
boolean
signs

The shares this edge lets the actor sign, on a sign-off step.

Array<object>
object
account_code
required
string
account_name
required
string
amount
required

A decimal amount as a string. Never a JSON number.

string
analytic_account_id
required
string format: uuid
axis_id
required
string format: uuid
sole_owner_exception

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.

boolean
to
required
string
claim
required
object
category_ids
required

The distinct categories of the active lines, in order of each category’s first line. [] for a claim with no active line.

Array<string>
claimant_member_id
required

Who filed it. Nobody files for anybody else (Ruling R20).

string format: uuid
claimant_unit_name
required

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.

string
nullable
created_at
required

When the claim was filed. The list’s order and its month filter are on this.

string format: date-time
currency
required

Always the organization’s base currency (Ruling R5).

string
>= 3 characters <= 3 characters
group_id
required
string format: uuid
nullable
id
required
string format: uuid
merchant
required

The supplier of the first active line that names one; null when none does.

string
nullable
multiple_currencies
required

The active lines are in more than one currency, so there is no single original total.

boolean
original_currency
required

Set when every active line is in one currency other than the claim’s base currency; null otherwise.

string
nullable >= 3 characters <= 3 characters
original_total
required

The active lines’ amounts in original_currency, summed by the server. null with it.

string
payout_channel_id
required

The claimant’s preferred channel; the payer may override it.

string format: uuid
nullable
project_names
required

The distinct accounts on the project analytic axis that the active lines’ shares name, in order of first line, then name. [] when none.

Array<string>
reference
required

EC-YYYY-NNNN, allocated from the claim sequence (Ruling R19).

string
revision
required

The optimistic lock. It is also the ETag, and every write against this claim must send it back in If-Match (Ruling R29).

integer
semantic
required

The closed set of workflow state semantics the ledger reacts to.

string
Allowed values: draft pending accrual payout acknowledged dispute rejected cancelled custom committed effect
state
required

The workflow instance’s state key. It changes only through a transition.

string
submitted_at
required

When it first left draft: the first decision out of a draft-semantic state, from the decision log. null until then.

string format: date-time
nullable
title
required
string
<= 200 characters
total
required

The active lines’ base amounts. Kept by the platform, never sent in.

string
your_share

On scope=to_act only: the shares of this claim the caller may sign now.

string
decisions
required

The decision log, oldest first.

Array<object>

One entry of the claim’s decision log, read as written and never re-resolved.

object
actor_member_id
required
string format: uuid
authority_member_id
required

Whose authority the actor decided on — themselves, or the member who delegated.

string format: uuid
decided_at
required
string format: date-time
from
required
string
reason
required
string
to
required
string
via
required
string
Allowed values: reporter grant delegation escalation sole_owner_exception
lines
required

Every line in line order, withdrawn ones included.

Array<object>
object
amount
required

What was spent, in currency.

string
band
required
string
nullable
base_amount
required

What the organization reimburses, converted once at the expense date.

string
category_id
required
string format: uuid
currency
required
string
>= 3 characters <= 3 characters
description
required
string
document_basis
required
string
Allowed values: einvoice_company einvoice_employee foreign_supplier relaxation_receipt
documents
required

The documents ACTIVELY supporting this line, oldest attachment first.

Array<string>
duplicate_claim_references
required

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.

Array<string>
einvoice_uuid
required
string
nullable
einvoice_validated_at
required
string format: date-time
nullable
expense_date
required
string format: date
fx_rate
required

A decimal rate or percentage as a string, up to 10 places. Never a JSON number.

string
fx_rate_source
required

Where the rate came from: identity, reference, override, or bank_statement (derived from a bank-charged amount). Frozen with the line (Ruling R5).

string
id
required
string format: uuid
line_no
required
integer
possible_duplicate
required

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.

boolean
quantity
required

Kilometres for mileage, days for per-diem.

string
rate_id
required
string format: uuid
nullable
reimbursement_basis
required

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.

string
Allowed values: expense_date_rate bank_charged
relaxation_reason
required
string
nullable
shares
required
Array<object>
object
analytic_account_id
required
string format: uuid
percentage
required

A decimal rate or percentage as a string, up to 10 places. Never a JSON number.

string
supplier_name
required
string
nullable
supplier_tin
required
string
nullable
tax_amount
required

SST borne, backed out of the gross (Ruling R13). Recorded, never recovered.

string
tax_code_id
required
string format: uuid
nullable
type
required

The four kinds of line a claim can carry (Ruling R11).

string
Allowed values: receipt mileage per_diem advance_offset
withdrawn
required

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.

boolean
settlements
required

The claim’s payouts, newest first. Masked (Ruling R44).

Array<object>

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
account_tail
required

The last few digits of the account paid, lifted from the snapshot’s plaintext.

string
nullable
amount
required

A decimal amount as a string. Never a JSON number.

string
channel_id
required
string format: uuid
channel_key
required
string
channel_name
required
string
currency
required
string
>= 3 characters <= 3 characters
fields
required
Array<object>

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
display
required

The masked form, e.g. “****6789”.

string
key
required
string
label
required

One string per locale. en and ms are required; zh is optional.

object
en
required
string
ms
required
string
zh
string
id
required
string format: uuid
proof_document_id
required
string format: uuid
nullable
settled_at
required
string format: date-time
settled_by
required
string format: uuid
signoffs
required

Every sign-off required to leave the current state, signed or not. Empty on any state that is not a sign-off step.

Array<object>
object
account_code
required
string
account_name
required
string
amount
required

A decimal amount as a string. Never a JSON number.

string
analytic_account_id
required
string format: uuid
axis_id
required
string format: uuid
currency
required
string
eligible
required
Array<string>
gap

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
analytic_account_id
string format: uuid
axis_id
string format: uuid
grant_id
required
string format: uuid
member_id
string format: uuid
position_id
string format: uuid
reason
required
string
signed
object
actor_member_id
required
string format: uuid
amount
required

A decimal amount as a string. Never a JSON number.

string
analytic_account_id
required
string format: uuid
approver_assignment_id
required

The project’s assignment the authority came through; null under a default grant or the claimant’s superior.

string format: uuid
nullable
authority_member_id
required
string format: uuid
axis_id
required
string format: uuid
decided_at
required
string format: date-time
id
required
string format: uuid
state
required
string
via
required
string
Allowed values: grant delegation escalation
state
required

The step being signed.

string

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"
}
}
]
}
ETag
string

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.

Media typeapplication/problem+json
object
code

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.

string
detail
string
instance
string
status
required
integer
title
required
string
type
required
string

Example generated

{
"code": "example",
"detail": "example",
"instance": "example",
"status": 1,
"title": "example",
"type": "example"
}

Not authenticated, or the session is no longer valid.

Media typeapplication/problem+json
object
code

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.

string
detail
string
instance
string
status
required
integer
title
required
string
type
required
string

Example generated

{
"code": "example",
"detail": "example",
"instance": "example",
"status": 1,
"title": "example",
"type": "example"
}

The caller does not hold the permission this operation requires.

Media typeapplication/problem+json
object
code

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.

string
detail
string
instance
string
status
required
integer
title
required
string
type
required
string

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).

Media typeapplication/problem+json
object
code

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.

string
detail
string
instance
string
status
required
integer
title
required
string
type
required
string

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).

Media typeapplication/problem+json
object
code

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.

string
detail
string
instance
string
status
required
integer
title
required
string
type
required
string

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.

Media typeapplication/problem+json
object
code

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.

string
detail
string
instance
string
status
required
integer
title
required
string
type
required
string

Example generated

{
"code": "example",
"detail": "example",
"instance": "example",
"status": 1,
"title": "example",
"type": "example"
}
Retry-After
integer

Seconds until a retry may succeed.