Download OpenAPI specification:
Operations the partner's end-user app performs on behalf of a single employee: submitting, viewing, deleting and disputing receipts.
User JWT via the token-exchange grant: the partner backend trades its own
employee identifier for a Hrmony token scoped to that one employee. Tokens are
not issued by this gateway but by its own subdomain,
auth.<partner>.hrmony.rest.
| Endpoint | Purpose |
|---|---|
POST /oauth/token |
Token exchange (step 1) and refresh (step 2) |
POST /oauth/revoke |
End a session (step 3) |
GET /.well-known/jwks.json |
Public key that issued tokens can be verified against |
All three live on the auth subdomain rather than on this gateway, so they do
not appear in the endpoint list below — the examples here are their complete
description.
curl --request POST \
--url https://auth.<partner>.hrmony.rest/oauth/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=urn:ietf:params:oauth:grant-type:token-exchange \
--data client_id=<CLIENT_ID> \
--data client_secret=<CLIENT_SECRET> \
--data subject_token=ext-emp-0001
| Parameter | Meaning |
|---|---|
subject_token |
the externalEmployeeId of the employee the token is for |
<partner> |
the subdomain assigned to the partner |
client_id, client_secret |
provided by Hrmony during onboarding |
The response carries access_token, token_type: Bearer, expires_in and
refresh_token. Send the access token as
Authorization: Bearer <access_token>. Because the exchange needs the client
secret, it belongs in the partner backend, not in the app — the app only ever
receives the finished access token.
When the access token expires, the partner backend obtains a new one without exchanging the employee identifier again:
curl --request POST \
--url https://auth.<partner>.hrmony.rest/oauth/token \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data grant_type=refresh_token \
--data client_id=<CLIENT_ID> \
--data client_secret=<CLIENT_SECRET> \
--data refresh_token=<REFRESH_TOKEN>
A refresh token is valid for 30 days after its last use, but never longer than
90 days after the original token exchange. An expired or revoked token is
answered with 400 and error: invalid_grant.
Three rules an integration otherwise gets wrong:
refresh_token; the old one is spent. Always store the new one.When an employee signs out, the partner backend revokes the session:
curl --request POST \
--url https://auth.<partner>.hrmony.rest/oauth/revoke \
--header 'Content-Type: application/x-www-form-urlencoded' \
--data client_id=<CLIENT_ID> \
--data client_secret=<CLIENT_SECRET> \
--data token=<REFRESH_TOKEN>
The response is always 200 with no body — for unknown tokens too. Access
tokens already issued stay valid until they expire; that is why they are
short-lived.
Hrmony issues one client_id/client_secret pair per partner installation.
That pair is deliberately valid on all three tiers — it is not an access right
in itself, only the permission to have tokens issued. What a call may do is
decided by the issued access token alone:
401.client:read client:write. The token
exchange takes no scope parameter; what limits a client token is the
employee it was issued for, not its scope. Read-only tokens exist on the two
client_credentials tiers (admin, partner).x-required-scope.On request Hrmony issues several client_id/client_secret pairs for the same
installation — one per connecting system, say, so that one can be replaced
without touching the others. A credential cannot be restricted to particular
tiers or scopes; the separation happens on the token.
Backend operations use a backend token (client_credentials) instead; that
flow is documented on the admin and partner tiers.
The token's sub claim is the externalEmployeeId — the id the partner knows
the employee by. Hrmony resolves employeeId and organizationId from it
when issuing the token and carries them inside it, so they do not belong in
requests on this tier. One token addresses exactly one employee, and only that
employee's own data is reachable through this tier. This tier carries no
X-Hrmony-Tenant-ID header.
Write endpoints carry a verb in the path: POST …/employees/create rather
than POST …/employees. This is deliberate.
The style is hybrid by design:
GET …/{id}, parameters in the URL, no body.Behind every write endpoint is a command-based model, not a CRUD one. Enabling
the meal subsidy or fetching payroll data are not updates to a field on a
resource; expressing them as PUT or PATCH would force an unnatural
mapping. Many actions are also scheduled rather than immediate: a benefit is
activated for an upcoming service month, not right now, and a PATCH on the
current state cannot say that.
Reads stay on GET because GET buys something real: responses are
cacheable in CDNs and proxies, retries are safe, and a plain lookup needs no
request body.
The result is RPC for actions, REST for data.
All errors follow RFC 9457 Problem Details.
Only the type URI is stable and worth branching on — title and detail
are for humans and may change.
They follow the request language, with one exception: for /errors/unauthorized, /errors/forbidden, /errors/not-found, /errors/throttled, /errors/client-error and /errors/server-error they are always English, for /errors/internal-server-error they can be English.
Every error carries a requestId; quote it when raising something with Hrmony.
403 or 404. 403 means the token lacks the required scope, or the
current business state forbids the action — a benefit that is not enabled for
the organization, for instance.
404 means the resource addressed
does not exist or is not yours:
another employee's receipt, another partner's organization. We answer both the
same way, deliberately indistinguishable — otherwise the status codes would let
you probe which ids exist.
A 404 therefore does not tell you that the id is unknown.
One error is listed under no single endpoint: /errors/not-found (404)
for a path this API does not know, or an HTTP method an endpoint does not offer.
Framework errors and domain errors alike carry type URIs under
/errors/….
Responses the API gateway produces itself, and that are none of the cases above,
carry the catch-all types /errors/client-error (4xx) and
/errors/server-error (5xx). Neither carries a status field: they
span several status codes, and the gateway cannot read the actual code inside its
template. Branch on the HTTP status code of the response instead.
There are no application-level rate limits: no quota per client, per token, per
endpoint or per time window, and therefore no RateLimit-* headers either.
The 429 /errors/throttled documented on every operation occurs only when
the environment's capacity limit is reached. That limit sits high enough that
regular operation never touches it, and it responds to total load across all
endpoints — not to a single endpoint or a particular access pattern.
A 429 carries no Retry-After. Treat it as transient and retry with
exponentially growing delays plus jitter, not at a fixed interval. The usual
retry rules apply: GET is safe to repeat, write endpoints only with the same
Idempotency-Key as the first attempt — otherwise you create a second
operation.
A 429 says nothing about permissions and does not indicate a blocked
account: it means only that this request was not processed and that the same
request may succeed later.
For your partner account, the API serves the English variant only: English
paths, English field names, English error text. Accept-Language is optional; when
sent it must be en. This variant is supported permanently.
The benefit product names are translated like every other term: mealSubsidy
and mobility.
Compact list of benefits available to the calling employee, with current status (active, suspended, inactive). Retrieve detailed configuration and budget usage per benefit via GET /benefits/{benefit}/configuration.
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": [
- {
- "benefitType": "meal_subsidy",
- "status": "active"
}
]
}Returns only the currently active meal subsidy configuration for the calling employee. If no active configuration exists, the benefit is not active and the endpoint responds with 404.
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": {
- "budget": 100,
- "dailyRate": 7.5
}
}Detail view of the meal subsidy reimbursement for the specified service month: benefit status, configured budget, total amount, number of contributing receipts, and reimbursement days with their respective daily totals. When status: inactive or suspended, reimbursementDays is empty and total is 0. Full daily view (incl. receipts) via GET /benefits/mealSubsidy/dailyReimbursements/{date}. The two 404 cases are distinguishable: /errors/meal-subsidy-not-enabled reports a missing benefit enablement, /errors/monthly-reimbursement-not-found a period without reimbursements.
| serviceMonth required | string^\d{4}-(0[1-9]|1[012])$ Examples: 2026-09 Service month in |
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": {
- "serviceMonth": "2026-09",
- "status": "active",
- "budget": 108,
- "total": 82.5,
- "receiptCount": 11,
- "reimbursementDays": [
- {
- "date": "2026-05-04",
- "total": 6.43
}
]
}
}Meal subsidy reimbursement for the calling employee on the specified service day: daily total and list of contributing receipts with ID and direct URL — no separate receipt call required. The two 404 cases are distinguishable: /errors/meal-subsidy-not-enabled reports a missing benefit enablement, /errors/daily-reimbursement-not-found a day without a reimbursement.
| date required | string <date> Service day in |
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": {
- "date": "2026-05-04",
- "total": 6.43,
- "receipts": [
- {
- "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7",
}
]
}
}Returns the status of the calling employee regarding the meal subsidy in the specified service month: active, inactive, or suspended. Allows frontends to show retroactively whether the benefit was effective in a month, regardless of whether any reimbursements occurred. suspended means the benefit is configured but was not effective in that month and continues afterwards without being configured again — unlike inactive, where there is no entitlement. Such months arise when a benefit end is set and the benefit is activated again later.
| serviceMonth required | string^\d{4}-(0[1-9]|1[012])$ Examples: 2026-09 Service month in |
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": {
- "serviceMonth": "2026-09",
- "status": "active"
}
}Returns only the currently active mobility configuration for the calling employee. If no active configuration exists, the benefit is not active and the endpoint responds with 404.
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": {
- "budget": 100
}
}Detail view of the mobility reimbursement for the specified service month: benefit status, configured budget, total amount, and number of contributing receipts. Mobility has no daily cap — only the monthly budget applies, and as long as it is not exhausted, any number of receipts on a single day is reimbursed. The response therefore carries no reimbursementDays; only the meal subsidy sets that field, because it caps per day as well. When status: inactive or suspended, total is 0. To list the receipts of a single day, use GET /benefits/mobility/dailyReimbursements/{date} — it breaks down the monthly reimbursement, it is not a cap of its own. The two 404 cases are distinguishable: /errors/mobility-not-enabled reports a missing benefit enablement, /errors/monthly-reimbursement-not-found a period without reimbursements.
| serviceMonth required | string^\d{4}-(0[1-9]|1[012])$ Examples: 2026-09 Service month in |
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": {
- "serviceMonth": "2026-09",
- "status": "active",
- "budget": 108,
- "total": 82.5,
- "receiptCount": 11,
- "reimbursementDays": [
- {
- "date": "2026-05-04",
- "total": 6.43
}
]
}
}Mobility reimbursement for the calling employee on the specified service day: daily total and list of contributing receipts with ID and direct URL — no separate receipt call required. The daily total is the share of the monthly reimbursement that falls on this day, not a cap of its own: mobility caps per month only. The two 404 cases are distinguishable: /errors/mobility-not-enabled reports a missing benefit enablement, /errors/daily-reimbursement-not-found a day without a reimbursement.
| date required | string <date> Service day in |
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": {
- "date": "2026-05-04",
- "total": 6.43,
- "receipts": [
- {
- "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7",
}
]
}
}Returns the status of the calling employee regarding mobility in the specified service month: active, inactive, or suspended. Allows frontends to show retroactively whether the benefit was effective in a month, regardless of whether any reimbursements occurred. suspended means the benefit is configured but was not effective in that month and continues afterwards without being configured again — unlike inactive, where there is no entitlement. Such months arise when a benefit end is set and the benefit is activated again later.
| serviceMonth required | string^\d{4}-(0[1-9]|1[012])$ Examples: 2026-09 Service month in |
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": {
- "serviceMonth": "2026-09",
- "status": "active"
}
}Submitting a receipt takes two steps.
POST /v1/receipts/submit.multipart/form-data straight to the uploadUrl you got
back, not through this API.The receiptId from step 1 identifies the receipt in every later call.
Upload
| Requirement | Details |
|---|---|
| Request | POST to uploadUrl, multipart/form-data, without Authorization and Accept-Language — the signature in the fields authorizes it |
| Form fields | all uploadFields, unchanged and in the order returned, before the file |
| File | last part, field name file, 1 byte to 6 MiB (6,291,456 bytes), one receipt per file |
| Validity | until validUntil, then repeat step 1 |
curl -X POST "$UPLOAD_URL" \
-F key=… # uploadFields first, unchanged
-F policy=… \
-F x-amz-signature=… \
-F "file=@receipt.jpg" # file last
The responses come from storage, not from this API: XML instead of Problem Details, described in the S3 documentation on presigned URLs.
| Status | Meaning |
|---|---|
204 |
file stored, empty body |
400 EntityTooSmall |
file is 0 bytes |
400 EntityTooLarge |
file over 6 MiB |
403 |
a field altered, omitted or sent after the file; Content-Type differs from fileType; URL expired |
fileType
Accepted are image/jpeg, image/jpg, image/png, image/heic,
image/heif, image/gif, image/bmp and application/pdf. Any other value
makes step 1 answer 400 /errors/invalid-request-body.
The value is part of the signed policy and appears as Content-Type in
uploadFields; the upload has to send it unchanged. Only the declared value is
checked, never the file's content — a file declared image/jpeg that holds no
image is stored, but the image pipeline cannot read it and the receipt is left
in review without an outcome.
Status
The receipt is registered asynchronously after the upload. Until that has
happened, GET /v1/receipts/{receiptId}
answers 404 /errors/receipt-not-found — that is not a failed upload.
| Status | Meaning | serviceMonth |
receiptUrl |
Reimbursement |
|---|---|---|---|---|
in_review |
the review is running | absent | absent | none |
recorded |
the review accepted the receipt | set | set | reimbursementAmount on the receipt |
rejected |
the review rejected the receipt | mobility only | set | none |
Transitions and webhooks
After the upload the receipt is in_review; receipt.in_review says so,
exactly once. Every review outcome arrives as receipt.status_changed, the
arrow is the cause:
cause: initial_review — the first review: recorded with
reimbursementAmount, or rejected with rejection.reason.cause: dispute_upheld — the re-review of a disputed receipt changed the
outcome; the body carries the new values.cause: dispute_dismissed — the re-review confirmed the outcome; the status
stays and the body carries neither reimbursementAmount nor rejection.Rules
recorded and rejected are not final states. A receipt's status always
reflects its latest review, so both recorded → rejected and
rejected → recorded are possible.POST /v1/receipts/dispute) has no status of its own: the receipt
keeps the outcome it has until the new one is decided.in_review produces no receipt.status_changed. That the review
has started is what receipt.in_review says, exactly once per receipt —
how many review steps it takes stays internal.reimbursementAmount in a webhook holds as of the delivery time and is
never back-filled. It is a share of an allocation period — the day for the
meal subsidy, the month for mobility — and changes with the other receipts
in that period:GET /v1/receipts/{receiptId} gives you the current value.X-Hrmony-Event-Id. Within one receipt, occurredAt orders the events:
discard a delivery whose occurredAt is older than the state you have
already applied for that receipt. There is no sequence number; occurredAt
does the same job.POST /v1/receipts/delete) has no status any more, and
the deletion produces no webhook.Webhook endpoints are registered on the partner tier; the events and their bodies are documented there under Webhook Events.
Collecting the outcome
Either through the receipt.status_changed webhook or by polling
GET /v1/receipts/{receiptId} — spaced
out rather than in a tight loop. A receipt with status: recorded carries its
reimbursementAmount — the amount currently credited to it. The daily and
monthly totals remain in the reimbursement endpoints under
Benefits · Meal Subsidy and
Benefits · Mobility.
When a service month is done
409 /errors/employee-month-closed is a fixed calendar boundary: from the
first day of the following month the employee can neither delete nor dispute a
receipt of that service month.
The service month itself is not finished at that point.
It is done once every receipt the organization submitted in time has been reviewed.
When that is depends on the organization: for one it can be the first of the
month, for another the second. Receipts submitted in time are reviewed past the
turn of the month, and status, reimbursement amount and monthly total can still
change afterwards. Treat a monthly total as final only once no receipt of that
month is on in_review any more.
Meal subsidy and mobility
benefitType
tells them apart.POST /v1/receipts/delete answers
422.wrong_date is valid only for the meal subsidy — mobility
receipts record no receiptDate
(POST /v1/receipts/dispute).Receipts are submitted in two steps. This endpoint returns a presigned upload URL, the associated uploadFields, and a permanent receiptId. In the second step, the client posts multipart/form-data directly to the upload URL. An upload is not a write through this API: the receipt is registered asynchronously, is therefore not visible in any read endpoint right after the upload, and must not be submitted again — see Receipts. The receipt date determines the service month: a receipt dated before the current calendar month (Europe/Berlin time zone) is rejected during review — there is no grace window past the turn of the month.
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
| Idempotency-Key | string Optional idempotency key for mutating requests. |
| benefitType required | string Enum: "meal_subsidy" "mobility" Benefit type. |
| fileType required | string Enum: "image/jpeg" "image/jpg" "image/png" "image/heic" "image/heif" "image/gif" "image/bmp" "application/pdf" Media type of the file you are about to upload. It is baked into the presigned upload policy — the upload has to send exactly this type. |
| reference | string <= 200 characters Optional partner reference. |
{- "benefitType": "meal_subsidy",
- "fileType": "image/jpeg"
}{- "data": {
- "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7",
- "uploadFields": {
- "key": "receipts/9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7",
- "Content-Type": "image/jpeg",
- "x-amz-algorithm": "AWS4-HMAC-SHA256",
- "x-amz-credential": "AKIAIOSFODNN7EXAMPLE/20260512/eu-central-1/s3/aws4_request",
- "x-amz-date": "20260512T093000Z",
- "policy": "eyJleHBpcmF0aW9uIjoiMjAyNi0wNS0xMlQxMjozMDowMFoiLCJjb25kaXRpb25zIjpbXX0=",
- "x-amz-signature": "3f1d9c0b5a7e2d48c6b91f0a4e8d2c7b6a5f4e3d2c1b0a9f8e7d6c5b4a39281f"
}, - "validUntil": "2026-05-12T12:30:00Z"
}
}Returns the receipt including a download URL of the receipt image (receiptUrl). receiptUrl is only set once the receipt is recorded or rejected — receipts still under review do not carry the field. The response is a union over benefitType; both variants carry the same fields, only the benefitType literal tells them apart. What actually distinguishes the meal subsidy from mobility receipts is described under Receipts. While the receipt is not registered yet — that is, right after the upload — this endpoint answers 404 /errors/receipt-not-found; that is not a failed upload and no reason to submit again. The end of that window is exactly what the receipt.in_review webhook reports.
| receiptId required | string <uuid> UUID of the receipt. |
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": {
- "benefitType": "meal_subsidy",
- "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7",
- "employeeId": "4c8e1b60-3a29-4f5d-9b71-8d2e6a0c53f4",
- "status": "recorded",
- "serviceMonth": "2026-05",
- "submittedAt": "2026-05-12T09:14:00Z",
- "receiptDate": "2026-05-04",
- "reimbursementAmount": 6.5
}
}Returns receipts of all benefit types for the employee, sorted by submittedAt descending. List items carry the same fields as GET /receipts/{receiptId}, including receiptUrl. A receipt that was just uploaded is missing from this list until it is registered (see Receipts).
Filterable fields
A comma in a value is always an enumeration separator; literal commas are not allowed.
An enumeration takes at most 100 values; more are rejected with 400 /errors/invalid-filter.
An empty value is not allowed, not even as an element of an enumeration.
| Field | Operators | Values | Examples |
|---|---|---|---|
benefitType |
=, , (IN) |
meal_subsidy, mobility |
?benefitType=meal_subsidy?benefitType=meal_subsidy,mobility |
status |
=, , (IN) |
in_review, recorded, rejected |
?status=in_review?status=in_review,recorded |
serviceMonth |
=, , (IN), .gt, .gte, .lt, .lte |
string | ?serviceMonth=<value>?serviceMonth=<value>,<value2>?serviceMonth.gt=<value>?serviceMonth.gte=<value>?serviceMonth.lt=<value>?serviceMonth.lte=<value> |
submittedAt |
=, .gt, .gte, .lt, .lte |
date-time | ?submittedAt=<date-time>?submittedAt.gt=<date-time>?submittedAt.gte=<date-time>?submittedAt.lt=<date-time>?submittedAt.lte=<date-time> |
Sortable fields: status, serviceMonth, submittedAt (default -submittedAt)
| limit | integer >= 1 Default: 50 Page size: default 50, at most 1000. A larger value is rejected with |
| cursor | string Opaque cursor from |
| benefitType | string Allowed values: meal_subsidy, mobility. Comma-separated for multiple values. A comma always separates values and is never part of a value. |
| status | string Allowed values: in_review, recorded, rejected. Comma-separated for multiple values. A comma always separates values and is never part of a value. |
| serviceMonth | string Comma-separated for multiple values. A comma always separates values and is never part of a value. |
| serviceMonth.gt | string^\d{4}-(0[1-9]|1[012])$ Examples: serviceMonth.gt=2026-09 Greater than. |
| serviceMonth.gte | string^\d{4}-(0[1-9]|1[012])$ Examples: serviceMonth.gte=2026-09 Greater than or equal. |
| serviceMonth.lt | string^\d{4}-(0[1-9]|1[012])$ Examples: serviceMonth.lt=2026-09 Less than. |
| serviceMonth.lte | string^\d{4}-(0[1-9]|1[012])$ Examples: serviceMonth.lte=2026-09 Less than or equal. |
| submittedAt | string <date-time> |
| submittedAt.gt | string <date-time> Greater than. |
| submittedAt.gte | string <date-time> Greater than or equal. |
| submittedAt.lt | string <date-time> Less than. |
| submittedAt.lte | string <date-time> Less than or equal. |
| sort | string Sortable fields: status, serviceMonth, submittedAt. A leading "-" sorts descending, comma-separated for multiple fields. Default: -submittedAt. Entries with equal values always come in the same order. Without writes between pages, paging returns each entry exactly once. |
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
{- "data": [
- {
- "benefitType": "meal_subsidy",
- "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7",
- "employeeId": "4c8e1b60-3a29-4f5d-9b71-8d2e6a0c53f4",
- "status": "recorded",
- "serviceMonth": "2026-05",
- "submittedAt": "2026-05-12T09:14:00Z",
- "receiptDate": "2026-05-04",
}
], - "meta": {
- "pagination": {
- "nextCursor": null,
- "hasMore": false,
- "totalCount": 1
}
}
}Deletes a meal subsidy receipt. Deletion is only supported for
meal subsidy receipts; for other benefit types the endpoint answers 422.
The benefit type is derived server-side from the receiptId.
Until when. A receipt can be deleted while its serviceMonth is the
current calendar month. From the first day of the following month the endpoint
answers 409 /errors/employee-month-closed; the calendar month is the one in
the Europe/Berlin time zone. Two receipts stay deletable indefinitely: one
that was rejected, and one still under review.
Effect. The deletion is permanent: there is no undo, and the same receipt
cannot be submitted again. The receipt disappears from GET /v1/receipts, and
GET /v1/receipts/{receiptId} answers 404 — shortly after the 200, not
with it: a read immediately afterwards can still return the receipt. If the receipt had already been
recorded, the reimbursement for its day drops by that receipt's share and the
month total decreases accordingly; several receipts can contribute to one day,
so the day may still carry a reimbursement afterwards. A receipt still under
review carries no reimbursement, so nothing changes there.
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
| Idempotency-Key | string Optional idempotency key for mutating requests. |
| receiptId required | string <uuid> UUID of the receipt. |
{- "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7"
}{- "data": {
- "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7"
}
}Disputes the review of a receipt; the benefit type is
derived server-side from the receiptId. The reason determines the
correction (one reason per request): amount_too_high/amount_too_low with
reimbursementAmount (Euro) — for the meal subsidy the reimbursement you expect,
for mobility the receipt total —, wrongly_rejected with no further fields,
wrong_date with receiptDate — meal subsidy only, not valid for
mobility. The disputed review is resolved server-side (always the latest one).
Both review outcomes can be disputed: a receipt on recorded (wrong amount or
receipt date) and a receipt on rejected (wrongly_rejected). A receipt still
on in_review has no review outcome to dispute; the endpoint then answers
422 /errors/receipt-still-in-review. For a deleted receipt the endpoint
answers 404 /errors/receipt-not-found.
Until when. A review can be disputed while the receipt's serviceMonth is
the current calendar month; from the first day of the following month the
endpoint answers 409 /errors/employee-month-closed; the calendar month is the
one in the Europe/Berlin time zone. Unlike deletion, this
also applies to a receipt that was rejected in a past month — dispute it with
wrongly_rejected within its own month.
| Accept-Language | string Value: "en" Optional. This deployment serves the English language surface only; the only accepted value is |
| X-Request-Id | string Correlation id; echoed on the response. |
| traceparent | string W3C trace context — echoed back when the request supplied it. The API never generates one of its own. |
| tracestate | string W3C trace state — echoed back when the request supplied it. |
| Idempotency-Key | string Optional idempotency key for mutating requests. |
| receiptId required | string <uuid> UUID of the disputed receipt. Determines the benefit type server-side (meal subsidy or mobility). |
| reason required | string Enum: "amount_too_high" "amount_too_low" "wrong_date" "wrongly_rejected" Reason for the dispute. |
| reimbursementAmount | number decimal places <= 2 >= 0 Monetary amount in Euro with two decimal places. |
| receiptDate | string <date> Desired receipt date (YYYY-MM-DD). Required when |
| comment | string <= 2000 characters Optional comment on the dispute. |
{- "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7",
- "reason": "amount_too_high",
- "reimbursementAmount": 6.85,
- "comment": "My share of the receipt was only €6.85."
}{- "data": {
- "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7"
}
}