Hrmony Embedded API — Client (EN) (1.0.0)

Download OpenAPI specification:

License: LicenseRef-Hrmony-Proprietary

Operations the partner's end-user app performs on behalf of a single employee: submitting, viewing, deleting and disputing receipts.

Authentication — user token

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.

Authorization server endpoints

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.

Step 1 — token exchange (sign-in)

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.

Step 2 — renewing the 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:

  • Every refresh token is valid exactly once. The response carries a new refresh_token; the old one is spent. Always store the new one.
  • Never send a spent token again. If one is sent anyway, Hrmony treats it as theft and revokes every refresh token of that session — including the currently valid one. Access tokens already issued stay valid until they expire. The employee then has to be signed in again via token exchange.
  • After a timeout, do not retry — exchange again. If the response to a refresh never arrives, the new token is lost and the old one is spent; a retry triggers the lock-out above.

Step 3 — signing out

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.

Credentials and scopes

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:

  • A token belongs to exactly one tier. The admin and partner gateways reject a client token with 401.
  • Client tokens always carry 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).
  • The required scope is stated on every operation in this specification as 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.

Employee context

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.

API style

Write endpoints carry a verb in the path: POST …/employees/create rather than POST …/employees. This is deliberate.

The style is hybrid by design:

  • Writes are RPC. The path names the action, the body carries the data.
  • Reads are REST. 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.

Errors

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.

Throttling (429)

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.

Language

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.

Benefits

Benefits available to the calling employee.

List available benefits for the calling employee

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.

Authorizations:
userJwt
header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
{
  • "data": [
    ]
}

Benefits · Meal Subsidy

Configuration, reimbursements and employee status for the meal subsidy.

Active Meal Subsidy configuration for the employee

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.

Authorizations:
userJwt
header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Meal Subsidy monthly reimbursement for a service month

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.

Authorizations:
userJwt
path Parameters
serviceMonth
required
string^\d{4}-(0[1-9]|1[012])$
Examples: 2026-09

Service month in YYYY-MM format.

header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Meal Subsidy daily reimbursement for a service day

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.

Authorizations:
userJwt
path Parameters
date
required
string <date>

Service day in YYYY-MM-DD format.

header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
{}

Employee Meal Subsidy status for a service month

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.

Authorizations:
userJwt
path Parameters
serviceMonth
required
string^\d{4}-(0[1-9]|1[012])$
Examples: 2026-09

Service month in YYYY-MM format.

header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Benefits · Mobility

Configuration, reimbursements and employee status for mobility.

Active Mobility configuration for the employee

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.

Authorizations:
userJwt
header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Mobility monthly reimbursement for a service month

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.

Authorizations:
userJwt
path Parameters
serviceMonth
required
string^\d{4}-(0[1-9]|1[012])$
Examples: 2026-09

Service month in YYYY-MM format.

header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Mobility daily reimbursement for a service day

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.

Authorizations:
userJwt
path Parameters
date
required
string <date>

Service day in YYYY-MM-DD format.

header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
{}

Employee Mobility status for a service month

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.

Authorizations:
userJwt
path Parameters
serviceMonth
required
string^\d{4}-(0[1-9]|1[012])$
Examples: 2026-09

Service month in YYYY-MM format.

header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
{
  • "data": {
    }
}

Receipts

Submitting a receipt takes two steps.

  1. Create an upload URL: POST /v1/receipts/submit.
  2. Send the file as 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

State diagram of a receipt

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.
  • A dispute (POST /v1/receipts/dispute) has no status of its own: the receipt keeps the outcome it has until the new one is decided.
  • Entering 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.
  • Every re-review produces another event for the same receipt id, even when the outcome is unchanged.
  • A 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:
    • When a further receipt for the same period arrives, the amount on the receipts already recorded might go down.
    • When one falls away — deleted, or rejected by a re-review — the amount on the remaining ones might go up.
    • No webhook follows for the receipts changed that way; only the receipt that was itself re-reviewed triggers one, and there are no correction events.
    • Only GET /v1/receipts/{receiptId} gives you the current value.
  • The order in which deliveries arrive is not guaranteed; deduplicate on 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.
  • A deleted receipt (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

  • A receipt's fields are identical for both benefit types; only benefitType tells them apart.
  • Deletion exists only for the meal subsidy; for mobility POST /v1/receipts/delete answers 422.
  • The dispute reason wrong_date is valid only for the meal subsidy — mobility receipts record no receiptDate (POST /v1/receipts/dispute).

Submit receipt

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.

Authorizations:
userJwt
header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Request Body schema: application/json
required
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.

Responses

Request samples

Content type
application/json
Example
{
  • "benefitType": "meal_subsidy",
  • "fileType": "image/jpeg"
}

Response samples

Content type
application/json
Example
{
  • "data": {
    }
}

Retrieve a single receipt

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.

Authorizations:
userJwt
path Parameters
receiptId
required
string <uuid>

UUID of the receipt.

header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
Example
{}

List receipts for the calling employee

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)

Authorizations:
userJwt
query Parameters
limit
integer >= 1
Default: 50

Page size: default 50, at most 1000. A larger value is rejected with 400 /errors/limit-exceeded.

cursor
string

Opaque cursor from meta.pagination.nextCursor.

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.

header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Responses

Response samples

Content type
application/json
Example
{}

Delete a receipt

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.

Authorizations:
userJwt
header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Request Body schema: application/json
required
receiptId
required
string <uuid>

UUID of the receipt.

Responses

Request samples

Content type
application/json
{
  • "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7"
}

Response samples

Content type
application/json
{
  • "data": {
    }
}

Dispute a review outcome

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.

Authorizations:
userJwt
header Parameters
Accept-Language
string
Value: "en"

Optional. This deployment serves the English language surface only; the only accepted value is en.

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.

Request Body schema: application/json
required
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. amount_too_high/amount_too_low require reimbursementAmount, wrongly_rejected requires no further fields, wrong_date requires receiptDate (meal subsidy only — not valid for mobility).

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 reason = wrong_date.

comment
string <= 2000 characters

Optional comment on the dispute.

Responses

Request samples

Content type
application/json
Example
{
  • "receiptId": "9f3c1a52-7d4e-4b8a-9c11-2f6b8d0a41e7",
  • "reason": "amount_too_high",
  • "reimbursementAmount": 6.85,
  • "comment": "My share of the receipt was only €6.85."
}

Response samples

Content type
application/json
Example
{
  • "data": {
    }
}