Conventions
Every endpoint of the fepli API follows the same rules for identifiers, dates, money, filters and writes. They're explained once here, so the resource reference can stay short.
Requests and responses
The API speaks JSON only. Responses are application/json; errors are application/problem+json (see Errors).
Requests with a body (POST, PATCH) must send Content-Type: application/json. For PATCH, application/merge-patch+json works too. A request body without a JSON content type is refused with 415 Unsupported Media Type.
A write request
curl -X PATCH https://ferienpass-musterstadt.de/api/offers/0192a4f1-6e3b-7c85-b1d2-8f4e6a9c3b03 \
-H "Authorization: Bearer $FEPLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"maxParticipants": 15}'
Field names are English. Content such as offer names and descriptions is returned as it was entered, usually in German.
Identifiers
Every record is identified by a UUID (RFC 4122), returned as uuid:
0192a4f1-6e3b-7c85-b1d2-8f4e6a9c3b03
The admin's URLs contain the same UUID in a shorter base58 form (22 characters, for example 1C8wV6pQk2Tz9RfXh4MbNd). The API accepts both forms wherever it reads an identifier, in paths, filters and request bodies, so you can paste whichever you have at hand. Responses always use the RFC 4122 form.
Some records can also be named by something more memorable, in filters and in request bodies:
| Record | Also accepted |
|---|---|
| Edition | its alias, e.g. herbstferien-2026 |
| Organiser | its alias, e.g. stadtjugendring-musterstadt |
| Category | its alias, e.g. sport-bewegung |
| Account | its e-mail address |
Paths take a UUID (in either form), never an alias: /editions/herbstferien-2026 does not work, ?edition=herbstferien-2026 does.
References
When a record points to another, it embeds a small reference with enough to display it and to fetch it:
A reference
{
"uuid": "01913e5c-2b4a-7d10-9f3e-6a1c2e7b4d01",
"name": "Herbstferien 2026",
"alias": "herbstferien-2026"
}
- Name
uuid- Type
- string
- Description
The identifier to fetch the full record with.
- Name
name- Type
- string
- Description
A display name: a person's full name, an offer's title, a receipt's number. Left out when the person's data is masked.
- Name
alias- Type
- string
- Description
The URL-safe alias, for records that have one (editions, organisers, offers, categories).
Some lists of related records, such as receipts on an application, contain bare UUID strings instead.
Empty fields are left out
Fields whose value is null are omitted from responses. An offer without a meeting point simply has no meetingPoint key. Treat a missing field as null.
Lists that apply to you are always present, and empty ones are returned as []. Fields you may not see, such as the internal fields of an offer without a token, are left out entirely. Booleans and numbers that have a default are always present.
The exception are a few nested lists of plain values, such as an edition's tasks, a receipt's items and a ledger's bookEntries: there, empty values appear as null.
Dates and times
Timestamps are ISO 8601 with an offset, in the municipality's local time (Europe/Berlin):
2026-10-14T09:00:00+02:00
When you send a timestamp, include the offset. Date-only values (such as a participant's dateOfBirth) are accepted as 2015-04-12.
Date filters such as from, until or modifiedSince accept a date (2026-10-01) or a full timestamp. An unreadable date is a 400.
Money
Amounts are integers in cents. fee: 750 is €7.50. Negative amounts appear on reversals and in ledgers: see Receipts and Debtors.
Filters
Collection endpoints take their filters as query parameters. Each resource lists its own.
curl "https://ferienpass-musterstadt.de/api/attendances?edition=herbstferien-2026&status=confirmed,waitlisted&paid=false" \
-H "Authorization: Bearer $FEPLI_TOKEN"
- Several filters are combined: a record must match all of them.
- Several values of one filter, where the filter allows it, are comma-separated and combined with OR:
status=confirmed,waitlisted. - Booleans are
trueorfalse(1/0andyes/nowork as well). - Free-text search (
q) matches any part of the fields each resource names, ignoring case. - A malformed value, such as a date that can't be read, a
truethat isn't a boolean or an unknownsort, is a400 Bad Requestwhosedetailnames the parameter. - A reference that matches nothing, such as
?edition=with an alias that doesn't exist, is not an error. It simply returns an empty list. (Theofferfilter of applications is the exception: an unknown offer is a404.) - Unknown parameters are ignored.
Writes
| Method | Used for | Success |
|---|---|---|
POST /things | create a record | 201 Created with the new record |
PATCH /things/{uuid} | change a record | 200 OK with the changed record |
DELETE /things/{uuid} | delete a record | 204 No Content |
POST /things/{uuid}/action | an action: a transition, a reversal, a revocation | 200 OK with the resulting record |
POST /things/{uuid}/action | an action that makes a new record: a copy, a variant, a move to another offer | 201 Created with the new record |
POST /things/{uuid}/action | an action done in the background: refilling a waiting list, committing decisions | 202 Accepted with the record as it is now |
How PATCH works
PATCH applies only the keys you send:
- A key you leave out is left alone.
- A key you send as
nullclears the field. - A list you send (for example an offer's
dates,hostsorcategories) replaces the whole list. - An empty body is a
422: there is nothing to change. (Editions are the exception and return unchanged.)
Change the fee, clear the meeting point
curl -X PATCH https://ferienpass-musterstadt.de/api/offers/0192a4f1-6e3b-7c85-b1d2-8f4e6a9c3b03 \
-H "Authorization: Bearer $FEPLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"fee": 500, "meetingPoint": null}'
Fields that must always have a value, such as a name or an offer's edition, ignore null. Each resource lists them.
Validation
Writes are validated with the same rules as the admin's forms. Invalid input is a 422 Unprocessable Entity. Its detail always holds one line per problem, each starting with the field:
422 Unprocessable Entity
{
"type": "/validation_errors/04b91c99-a946-4221-afc5-e65ebac401eb",
"title": "An error occurred",
"status": 422,
"detail": "maxAge: This value should be between 0 and 99.",
"violations": [
{
"propertyPath": "maxAge",
"message": "This value should be between 0 and 99.",
"code": "04b91c99-a946-4221-afc5-e65ebac401eb"
}
]
}
When a value is malformed or out of range, as above, the response also lists each problem in violations. When a request is well-formed but doesn't make sense for the record, such as an edition that doesn't exist or an amount above the fee, you get the detail only. Read detail and you're covered either way.
Workflows
Offers and applications move through states, just like in the admin. You don't set their state directly. Instead you apply a transition with POST …/{uuid}/transition:
curl -X POST https://ferienpass-musterstadt.de/api/attendances/0192d0e8-3a4b-7c5d-9e6f-7a8b9c0d1e07/transition \
-H "Authorization: Bearer $FEPLI_TOKEN" \
-H "Content-Type: application/json" \
-d '{"transition": "confirm", "notify": true}'
Every offer and application lists its possible transitions in transitions. For offers the list already takes your permissions into account; for applications it only reflects the state. If you try one that isn't possible, the answer is a 409 Conflict that names the possible ones:
409 Conflict
{
"type": "/errors/409",
"title": "An error occurred",
"status": 409,
"detail": "The transition \"noshow\" is not possible from \"waitlisted\". Possible: confirm, withdraw, reset, reject, unfulfill, participated."
}
The transitions trigger the same side effects as in the admin: e-mails to families (where you ask for them), reordered participant lists, updated payment records.
Caching
Responses to authenticated requests are marked Cache-Control: private, no-store. They are one person's view and must never be stored by a shared cache. Anonymous reads of the public offers stay cacheable.