Introduction
festiware API v2 — project-scoped REST API for People, Applications, Ticket Types, and workflow-step transitions. Authentication is OAuth 2.0 Authorization Code grant via Laravel Passport.
Looking for legacy v1 reference? It's at /v1.
Connecting your integration (e.g. n8n)
Complete this once per integration. After Step 4 the credential auto-renews — no further manual work.
Prerequisites
- A festiware instance with backend admin access
- An OAuth 2.0 client (n8n, custom integration, postman, etc.)
Step 1 — Create an API User
In the festiware backend: API Users → Create API User
- Enter a descriptive name (e.g.
N8N — Bucht der Träumer) - Assign the permissions the integration needs (e.g.
all_persons_and_applications_administrate,ticketcode_administrate) - Copy the password shown — it is displayed once. Hand the email + password to whoever will complete Step 4.
Step 2 — Issue an OAuth Client
In the backend: open the API user → Actions → Create OAuth Client
| Field | Value |
|---|---|
| Client Name | e.g. N8N Production — Bucht der Träumer |
| Redirect URI | https://{n8n-instance}/rest/oauth2-credential/callback |
Click Run Action. Copy the client_id and client_secret shown — they are displayed once.
Step 3 — Configure the n8n credential
In n8n: Credentials → New → OAuth2 API
| Field | Value |
|---|---|
| Grant Type | Authorization Code |
| Authorization URL | https://{instance}.festiwa.re/oauth/authorize |
| Access Token URL | https://{instance}.festiwa.re/oauth/token |
| Client ID | from Step 2 |
| Client Secret | from Step 2 |
| Scope | (leave empty) |
Click Connect. A browser window opens.
Step 4 — Complete browser authorization (one-time)
The user from Step 1 logs in:
- Enter email + password → Login
- The consent screen shows the client name → Authorize
- Browser closes → n8n shows Connected
This step runs once. n8n stores the refresh token and auto-renews from this point.
Step 5 — Token lifecycle
- Access tokens expire after 1 hour
- n8n uses the refresh token to mint a new access token automatically — no manual action
- Refresh tokens are valid for 1 year
- If the client secret is compromised: revoke the client in the backend (API user → Actions → Revoke Client), issue a new one, and reconnect in n8n
Step 6 — First API call
In an n8n HTTP Request node:
Method: GET
URL: https://{instance}.festiwa.re/api/v2/projects/{project_id}/people
Auth: Predefined Credential Type → OAuth2 (the credential from Step 3)
Expected: HTTP 200 with a paginated People list.
Connecting without n8n (Postman, scripts, custom code)
The credentials from Steps 1–2 are a standard OAuth 2.0 Authorization Code client — nothing about them is n8n-specific. Any tool or library that supports this grant works: Postman, Insomnia, or your own code. Steps 3–6 above are simply the n8n flavour of the same flow.
Understanding the Redirect URI
This is the single most common point of confusion, so to be clear:
- The redirect URI does not have to point to your tool, your server, or anything reachable at all. It is only the address the browser is sent to after a successful login — tools like Postman intercept that redirect locally and never actually load the URL.
- The only hard requirement: the redirect/callback URI configured in your tool must exactly match the one registered on the OAuth client (Step 2) — same scheme, host, path, no extra trailing slash.
- On a mismatch the login fails with a redirect-URI error. Fix it on either side: change the callback URL in your tool, or update the client's redirect URI in the backend under the API user.
Postman setup
- Import or create a request/collection and open the Authorization tab → Type OAuth 2.0.
- Configure:
| Field | Value |
|---|---|
| Grant Type | Authorization Code |
| Callback URL | the Redirect URI registered on the client (Step 2) |
| Auth URL | https://{instance}.festiwa.re/oauth/authorize |
| Access Token URL | https://{instance}.festiwa.re/oauth/token |
| Client ID / Client Secret | from Step 2 |
| Scope | (leave empty) |
| Client Authentication | Send client credentials in body |
- Leave "Authorize using browser" switched OFF. When it is on, Postman replaces your callback URL with its own relay (
https://oauth.pstmn.io/v1/callback), which won't match the registered redirect URI — the login then always fails. With the switch off, Postman opens its own login popup and intercepts the redirect regardless of where it points. - Click Get New Access Token, log in with the API-user email + password from Step 1, and approve the consent screen. Postman stores the token and attaches it as a Bearer header.
Token lifecycle without auto-refresh
Unlike n8n, Postman and plain scripts do not renew tokens by themselves:
- The access token expires after 1 hour. In Postman, simply click Get New Access Token again.
- In your own code, store the
refresh_tokenfrom the token response and exchange it before expiry:
curl -X POST https://{instance}.festiwa.re/oauth/token \
-d grant_type=refresh_token \
-d refresh_token={refresh_token} \
-d client_id={client_id} \
-d client_secret={client_secret}
- Refresh tokens are valid for 1 year; after that, run the browser authorization once more.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Login form says "not an API user" | The customer signed in with their own festiware account | Use the API-user credentials from Step 1 |
| n8n shows "redirect URI mismatch" | The redirect URI in the backend doesn't match n8n's callback URL | Update the client in the backend with n8n's exact callback URL |
| Postman: login fails / redirect mismatch although the callback URL looks right | "Authorize using browser" is ON — Postman silently forces its oauth.pstmn.io relay as callback |
Switch "Authorize using browser" off and get the token again |
| API calls return 403 | The API user lacks the required permission | Add the permission in the backend → API User → Edit |
| n8n shows "disconnected" after weeks/months | Refresh token expired (1 year) or the client was revoked | Issue a new client and reconnect |
Authenticating requests
To authenticate requests, include an Authorization header with the value "Bearer {ACCESS_TOKEN}".
All authenticated endpoints are marked with a requires authentication badge in the documentation below.
Obtain a token via OAuth 2.0 Authorization Code grant. See the connection guide in the introduction for the full backend → OAuth client → n8n setup. The returned access_token must be sent as a Bearer token in the Authorization header.
People
List person types.
requires authentication
Returns every person type defined on the instance, paginated. Global reference data; not project-scoped.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/person-types?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/person-types"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/person-types';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (500):
Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
{
"message": "An error occurred and was reported automatically.<br>(Event ID: 1e9595cf08c84d00a6656157a35f6c3f)<br>We usually resolve errors within a few hours.<br>Please try again later!"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Paginated list of Persons in a project.
requires authentication
Required permission: all_persons_and_applications_see
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/people?email=anna%40example.com&updated_since=2026-04-30T00%3A00%3A00%2B00%3A00&person_type=volunteer&step=acc_buildup&workflow_id=209&withJourney=&per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 16,
\"email\": \"zbailey@example.net\",
\"updated_since\": \"2026-08-19T22:44:41+02:00\",
\"person_type\": \"architecto\",
\"step\": \"architecto\",
\"workflow_id\": 16,
\"withJourney\": false
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/people"
);
const params = {
"email": "anna@example.com",
"updated_since": "2026-04-30T00:00:00+00:00",
"person_type": "volunteer",
"step": "acc_buildup",
"workflow_id": "209",
"withJourney": "0",
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 16,
"email": "zbailey@example.net",
"updated_since": "2026-08-19T22:44:41+02:00",
"person_type": "architecto",
"step": "architecto",
"workflow_id": 16,
"withJourney": false
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/people';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'email' => 'anna@example.com',
'updated_since' => '2026-04-30T00:00:00+00:00',
'person_type' => 'volunteer',
'step' => 'acc_buildup',
'workflow_id' => '209',
'withJourney' => '0',
'per_page' => '25',
],
'json' => [
'per_page' => 16,
'email' => 'zbailey@example.net',
'updated_since' => '2026-08-19T22:44:41+02:00',
'person_type' => 'architecto',
'step' => 'architecto',
'workflow_id' => 16,
'withJourney' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"project_id": 1,
"data": {
"firstname": "Anna",
"email": "anna@example.com"
},
"calculated_data": {},
"person_types": [
"volunteer"
],
"created_at": "2026-04-30T11:24:53+00:00",
"updated_at": "2026-04-30T11:24:53+00:00",
"related": {
"teams": [
405,
426
],
"team_roles": [
12
],
"groups_owned": [],
"groups_member": [
3310
],
"events": [
881
],
"tickets": [],
"orders": [],
"person_access_types": [
4
],
"coupons": [],
"nfc_tags": [],
"contracts": [
3021
]
}
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (200, With journey (?withJourney=1)):
{
"data": [
{
"id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"project_id": 1,
"data": {
"firstname": "Anna"
},
"calculated_data": {},
"person_types": [
"volunteer"
],
"created_at": "2026-04-30T11:24:53+00:00",
"updated_at": "2026-04-30T11:24:53+00:00",
"current_steps": [
{
"workflow_id": 209,
"step": {
"id": 1904,
"shortname": "acc_buildup",
"name": "2a) Akkreditierung Aufbau"
},
"entered_at": "2026-05-01T09:00:00+00:00"
}
],
"journey": [
{
"step": {
"id": 1904,
"shortname": "acc_buildup"
},
"registered_at": "2026-05-01T09:00:00+00:00",
"user_id": null
}
]
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (422, Invalid query parameter):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"per_page": [
"The per page must not be greater than 100."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a new Person in a project.
requires authentication
Required permission: all_persons_and_applications_administrate
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/people" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"id\": \"architecto\",
\"person_types\": [
\"volunteer\"
],
\"act_as_backend_creation\": false,
\"data\": {
\"firstname\": \"Anna\",
\"email\": \"anna@example.com\"
}
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/people"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"id": "architecto",
"person_types": [
"volunteer"
],
"act_as_backend_creation": false,
"data": {
"firstname": "Anna",
"email": "anna@example.com"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/people';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'id' => 'architecto',
'person_types' => [
'volunteer',
],
'act_as_backend_creation' => false,
'data' => [
'firstname' => 'Anna',
'email' => 'anna@example.com',
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Created):
{
"data": {
"id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"project_id": 1,
"data": {
"firstname": "Anna",
"email": "anna@example.com"
},
"calculated_data": {},
"person_types": [
"volunteer"
],
"created_at": "2026-04-30T11:24:53+00:00",
"updated_at": "2026-04-30T11:24:53+00:00"
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (409, DuplicateId):
{
"error": {
"code": "duplicate_id",
"message": "A Person with this id already exists."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"data": [
"The data field is required."
],
"person_types": [
"The person_types field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show a single Person by UUID.
requires authentication
Required permission: all_persons_and_applications_see
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/people/architecto" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/people/architecto"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/people/architecto';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": {
"id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"project_id": 1,
"data": {
"firstname": "Anna",
"email": "anna@example.com"
},
"calculated_data": {},
"person_types": [
"volunteer"
],
"created_at": "2026-04-30T11:24:53+00:00",
"updated_at": "2026-04-30T11:24:53+00:00"
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Person not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update a Person.
requires authentication
Both PUT and PATCH route here. The behavioural switch is replaceMissing:
- PUT → replaceMissing=true (full replace; absent
datakeys cleared) - PATCH → replaceMissing=false (sparse merge; absent keys preserved)
Both verbs require an updated_at body field (ISO 8601) matching the Person's
current updated_at; mismatch returns 409 with error.errors.current.updated_at.
Required permission: all_persons_and_applications_manage
Example request:
curl --request PUT \
"https://{instance}.festiwa.re/api/v2/projects/564/people/architecto" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"person_types\": [
\"architecto\"
],
\"act_as_backend_creation\": false,
\"data\": [],
\"updated_at\": \"2026-04-30T11:24:53+00:00\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/people/architecto"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"person_types": [
"architecto"
],
"act_as_backend_creation": false,
"data": [],
"updated_at": "2026-04-30T11:24:53+00:00"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/people/architecto';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'person_types' => [
'architecto',
],
'act_as_backend_creation' => false,
'data' => [],
'updated_at' => '2026-04-30T11:24:53+00:00',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": {
"id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"project_id": 1,
"data": {
"firstname": "Anna",
"email": "anna@example.com"
},
"calculated_data": {},
"person_types": [
"volunteer"
],
"created_at": "2026-04-30T11:24:53+00:00",
"updated_at": "2026-04-30T12:00:00+00:00"
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Person not found."
}
}
Example response (409, OptimisticLockFailed):
{
"error": {
"code": "optimistic_lock_failed",
"message": "The Person was modified after your last read.",
"errors": {
"current": {
"updated_at": "2026-04-30T11:24:53+00:00"
}
}
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"updated_at": [
"The updated_at field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Soft-delete a Person.
requires authentication
Requires explicit confirm: true in the body. Missing or false confirmation
returns 422 and the Person remains intact. Soft-deleted Persons return 404
from all subsequent v2 endpoints.
Required permission: all_persons_and_applications_administrate
Example request:
curl --request DELETE \
"https://{instance}.festiwa.re/api/v2/projects/564/people/architecto" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"confirm\": true
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/people/architecto"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"confirm": true
};
fetch(url, {
method: "DELETE",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/people/architecto';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'confirm' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204, SoftDeleted):
Empty response
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Person not found."
}
}
Example response (409, PersonHasContracts):
{
"error": {
"code": "person_has_contracts",
"message": "Person has signed contracts and cannot be deleted."
}
}
Example response (409, PersonHasInvoices):
{
"error": {
"code": "person_has_invoices",
"message": "Person has linked invoices and cannot be deleted."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"confirm": [
"The confirm field must be accepted."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Advance many Persons in one request.
requires authentication
force=false(default): every id is pre-validated; if ANY would fail, the response is 400bulk_validation_failedwith afailed_idsarray and no Person is transitioned.force=true: every id is processed; failures are reported per-entity inside a 200 response. Sync/async dispatch still follows ChangeWorkflowStepJob::$maxModelsForAsync (default 25); above that threshold the response is 202dispatched.
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/people/transitions/bulk" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"ids\": [
\"uuid-1\",
\"uuid-2\"
],
\"to_step_id\": 42,
\"to_step_name\": \"APPROVED\",
\"person_type\": \"volunteer\",
\"workflow_id\": 5,
\"note\": \"Approved.\",
\"act_as_backend_creation\": false,
\"dry_run\": false,
\"force\": false
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/people/transitions/bulk"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"ids": [
"uuid-1",
"uuid-2"
],
"to_step_id": 42,
"to_step_name": "APPROVED",
"person_type": "volunteer",
"workflow_id": 5,
"note": "Approved.",
"act_as_backend_creation": false,
"dry_run": false,
"force": false
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/people/transitions/bulk';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'ids' => [
'uuid-1',
'uuid-2',
],
'to_step_id' => 42,
'to_step_name' => 'APPROVED',
'person_type' => 'volunteer',
'workflow_id' => 5,
'note' => 'Approved.',
'act_as_backend_creation' => false,
'dry_run' => false,
'force' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, SyncBulk):
{
"data": {
"01a1e3a6-efbf-4413-9db1-f36e60489254": {
"status": "success",
"previous_step_id": 41,
"current_step_id": 42,
"triggers_fired": 3
},
"0298f01b-aa11-4d3c-bb22-cc33dd44ee55": {
"status": "skipped",
"reason": "already_in_step",
"previous_step_id": 42
}
}
}
Example response (202, AsyncDispatched):
{
"status": "dispatched",
"count": 50
}
Example response (400, BulkValidationFailed):
{
"error": {
"code": "bulk_validation_failed",
"message": "One or more transitions are not possible. Use force=true to process the eligible entries.",
"failed_ids": [
{
"id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"reason": "already_in_step",
"previous_step_id": 42
},
{
"id": "0298f01b-aa11-4d3c-bb22-cc33dd44ee55",
"reason": "person_not_found",
"previous_step_id": null
}
]
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "scope_missing",
"message": "Token does not carry all_persons_and_applications_manage."
}
}
Example response (422, InvalidPayload):
{
"error": {
"code": "invalid_payload",
"message": "The given data was invalid.",
"errors": {
"ids": [
"The ids field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Advance a single Person to a new workflow step.
requires authentication
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/people/architecto/transitions" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"to_step_id\": 42,
\"to_step_name\": \"APPROVED\",
\"person_type\": \"volunteer\",
\"workflow_id\": 5,
\"note\": \"Approved via n8n.\",
\"act_as_backend_creation\": false,
\"dry_run\": false
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/people/architecto/transitions"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"to_step_id": 42,
"to_step_name": "APPROVED",
"person_type": "volunteer",
"workflow_id": 5,
"note": "Approved via n8n.",
"act_as_backend_creation": false,
"dry_run": false
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/people/architecto/transitions';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'to_step_id' => 42,
'to_step_name' => 'APPROVED',
'person_type' => 'volunteer',
'workflow_id' => 5,
'note' => 'Approved via n8n.',
'act_as_backend_creation' => false,
'dry_run' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Transitioned):
{
"data": {
"person_id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"previous_workflow_step": {
"id": 41,
"shortname": "PENDING",
"name": "Pending"
},
"current_workflow_step": {
"id": 42,
"shortname": "APPROVED",
"name": "Approved",
"entered_at": "2026-05-13T10:00:00+00:00"
},
"triggers_fired": 3,
"note_saved": true
}
}
Example response (200, DryRun):
{
"data": {
"person_id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"previous_workflow_step": {
"id": 41,
"shortname": "PENDING",
"name": "Pending"
},
"current_workflow_step": {
"id": 42,
"shortname": "APPROVED",
"name": "Approved",
"entered_at": "2026-05-13T10:00:00+00:00"
},
"triggers_fired": 0,
"note_saved": false,
"dry_run": true
}
}
Example response (400, AlreadyInStep):
{
"error": {
"code": "already_in_step",
"message": "The target is already in workflow step APPROVED."
}
}
Example response (400, StepNotInWorkflow):
{
"error": {
"code": "step_not_in_workflow",
"message": "The target step does not belong to the identified workflow."
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "scope_missing",
"message": "Token does not carry all_persons_and_applications_manage."
}
}
Example response (404, PersonNotFound):
{
"error": {
"code": "person_not_found",
"message": "Person not found in this project."
}
}
Example response (404, WorkflowNotFound):
{
"error": {
"code": "workflow_not_found",
"message": "Workflow could not be resolved from the supplied disambiguation."
}
}
Example response (422, InvalidPayload):
{
"error": {
"code": "invalid_payload",
"message": "One of `to_step_id` or `to_step_name` is required.",
"errors": {
"to_step": [
"One of `to_step_id` or `to_step_name` is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Applications
List application types.
requires authentication
Returns every application type defined on the instance, paginated. Global reference data; not project-scoped.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/application-types?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/application-types"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/application-types';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (500):
Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
{
"message": "An error occurred and was reported automatically.<br>(Event ID: 92296ff13fbc46419102a3ca9ed34cb6)<br>We usually resolve errors within a few hours.<br>Please try again later!"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Paginated list of Applications in a project.
requires authentication
Required permission: all_persons_and_applications_see
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/applications?application_type=volunteer&person_id=01a1e3a6-efbf-4413-9db1-f36e60489254&step=applied&updated_since=2026-05-07T00%3A00%3A00%2B00%3A00&per_page=25&withOwner=&withMembersCollection=&withSubordinatedApplications=&withJourney=&withEvents=&withEventAttendees=&withTickets=&withTranslatedValues=de%0A%0AEvery+Application+also+carries+an+always-on+%60related%60+block+%28no+flag+needed%29%3A+ID-only%0Aarrays+of+linked+resources+%E2%80%94+events%2C+teams%2C+ticket_types%2C+subordinated_applications%2C%0Acontracts+%E2%80%94+for+follow-up+requests.+%60owner%60+and+%60members%60+are+omitted+from+%60related%60%0Aas+they+are+already+top-level+%28person_id%2C+members%5B%5D%29." \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"per_page\": 16,
\"application_type\": \"architecto\",
\"person_id\": \"a4855dc5-0acb-33c3-b921-f4291f719ca0\",
\"step\": \"architecto\",
\"updated_since\": \"2026-08-19T22:44:41+02:00\",
\"withOwner\": true,
\"withMembersCollection\": true,
\"withSubordinatedApplications\": true,
\"withJourney\": true,
\"withEvents\": false,
\"withEventAttendees\": true,
\"withTickets\": true,
\"withTranslatedValues\": \"architecto\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/applications"
);
const params = {
"application_type": "volunteer",
"person_id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"step": "applied",
"updated_since": "2026-05-07T00:00:00+00:00",
"per_page": "25",
"withOwner": "0",
"withMembersCollection": "0",
"withSubordinatedApplications": "0",
"withJourney": "0",
"withEvents": "0",
"withEventAttendees": "0",
"withTickets": "0",
"withTranslatedValues": "de
Every Application also carries an always-on `related` block (no flag needed): ID-only
arrays of linked resources — events, teams, ticket_types, subordinated_applications,
contracts — for follow-up requests. `owner` and `members` are omitted from `related`
as they are already top-level (person_id, members[]).",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"per_page": 16,
"application_type": "architecto",
"person_id": "a4855dc5-0acb-33c3-b921-f4291f719ca0",
"step": "architecto",
"updated_since": "2026-08-19T22:44:41+02:00",
"withOwner": true,
"withMembersCollection": true,
"withSubordinatedApplications": true,
"withJourney": true,
"withEvents": false,
"withEventAttendees": true,
"withTickets": true,
"withTranslatedValues": "architecto"
};
fetch(url, {
method: "GET",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/applications';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'application_type' => 'volunteer',
'person_id' => '01a1e3a6-efbf-4413-9db1-f36e60489254',
'step' => 'applied',
'updated_since' => '2026-05-07T00:00:00+00:00',
'per_page' => '25',
'withOwner' => '0',
'withMembersCollection' => '0',
'withSubordinatedApplications' => '0',
'withJourney' => '0',
'withEvents' => '0',
'withEventAttendees' => '0',
'withTickets' => '0',
'withTranslatedValues' => 'de
Every Application also carries an always-on `related` block (no flag needed): ID-only
arrays of linked resources — events, teams, ticket_types, subordinated_applications,
contracts — for follow-up requests. `owner` and `members` are omitted from `related`
as they are already top-level (person_id, members[]).',
],
'json' => [
'per_page' => 16,
'application_type' => 'architecto',
'person_id' => 'a4855dc5-0acb-33c3-b921-f4291f719ca0',
'step' => 'architecto',
'updated_since' => '2026-08-19T22:44:41+02:00',
'withOwner' => true,
'withMembersCollection' => true,
'withSubordinatedApplications' => true,
'withJourney' => true,
'withEvents' => false,
'withEventAttendees' => true,
'withTickets' => true,
'withTranslatedValues' => 'architecto',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"project_id": 1,
"application_type_id": 3,
"application_type": {
"id": 3,
"shortname": "volunteer"
},
"person_id": null,
"members": [],
"current_step": {
"id": 42,
"shortname": "APPROVED"
},
"data": {
"shirt_size": "M"
},
"created_at": "2026-05-07T11:24:53+00:00",
"updated_at": "2026-05-07T11:24:53+00:00",
"anonymized_at": null,
"deactivated_at": null,
"deleted_at": null,
"related": {
"events": [
881
],
"teams": [
405
],
"ticket_types": [],
"subordinated_applications": [],
"contracts": []
}
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (422, Invalid query parameter):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"per_page": [
"The per page must not be greater than 100."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a new Application in a project.
requires authentication
Required permission: all_persons_and_applications_administrate
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/applications" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"application_type\": \"volunteer\",
\"person_id\": \"01a1e3a6-efbf-4413-9db1-f36e60489254\",
\"data\": {
\"shirt_size\": \"M\",
\"dietary_pref\": \"vegan\"
}
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/applications"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"application_type": "volunteer",
"person_id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"data": {
"shirt_size": "M",
"dietary_pref": "vegan"
}
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/applications';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'application_type' => 'volunteer',
'person_id' => '01a1e3a6-efbf-4413-9db1-f36e60489254',
'data' => [
'shirt_size' => 'M',
'dietary_pref' => 'vegan',
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Created):
{
"data": {
"id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"project_id": 1,
"application_type_id": 3,
"application_type": {
"id": 3,
"shortname": "volunteer"
},
"person_id": null,
"members": [],
"current_step": {
"id": 40,
"shortname": "PENDING"
},
"data": {
"shirt_size": "M",
"dietary_pref": "vegan"
},
"calculated_data": {},
"created_at": "2026-05-07T11:24:53+00:00",
"updated_at": "2026-05-07T11:24:53+00:00",
"anonymized_at": null,
"deactivated_at": null,
"deleted_at": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (409, DuplicateId):
{
"error": {
"code": "duplicate",
"message": "An Application with this id already exists."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"application_type": [
"The application_type field is required."
],
"data": [
"The data field must be an array."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show a single Application by UUID.
requires authentication
Same sideload flags (withOwner, withMembersCollection, withJourney,
withEvents, withEventAttendees, withTickets, withSubordinatedApplications,
withTranslatedValues) are supported here as on the list endpoint.
Required permission: all_persons_and_applications_see
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/applications/16?withOwner=&withMembersCollection=&withJourney=&withEvents=&withEventAttendees=&withTickets=&withSubordinatedApplications=&withTranslatedValues=de" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/applications/16"
);
const params = {
"withOwner": "0",
"withMembersCollection": "0",
"withJourney": "0",
"withEvents": "0",
"withEventAttendees": "0",
"withTickets": "0",
"withSubordinatedApplications": "0",
"withTranslatedValues": "de",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/applications/16';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'withOwner' => '0',
'withMembersCollection' => '0',
'withJourney' => '0',
'withEvents' => '0',
'withEventAttendees' => '0',
'withTickets' => '0',
'withSubordinatedApplications' => '0',
'withTranslatedValues' => 'de',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": {
"id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"project_id": 1,
"application_type_id": 3,
"application_type": {
"id": 3,
"shortname": "volunteer"
},
"person_id": null,
"members": [],
"current_step": {
"id": 42,
"shortname": "APPROVED"
},
"data": {
"shirt_size": "M"
},
"calculated_data": {},
"created_at": "2026-05-07T11:24:53+00:00",
"updated_at": "2026-05-07T11:24:53+00:00",
"anonymized_at": null,
"deactivated_at": null,
"deleted_at": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update an Application (PUT = full replace, PATCH = sparse merge).
requires authentication
Both PUT and PATCH route here; replaceMissing is derived from the HTTP method.
applicationType (immutable) and step_id / workflow_step_id (transitions
belong to a separate endpoint) are rejected if present in the body.
members[] writes return 422 — manage members in the festiware backend or via v1.
Required permission: all_persons_and_applications_manage
Example request:
curl --request PUT \
"https://{instance}.festiwa.re/api/v2/projects/564/applications/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"data\": [],
\"updated_at\": \"2026-05-07T11:24:53+00:00\",
\"person_id\": \"architecto\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/applications/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"data": [],
"updated_at": "2026-05-07T11:24:53+00:00",
"person_id": "architecto"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/applications/16';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'data' => [],
'updated_at' => '2026-05-07T11:24:53+00:00',
'person_id' => 'architecto',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": {
"id": "01a1e3a6-efbf-4413-9db1-f36e60489254",
"project_id": 1,
"application_type_id": 3,
"application_type": {
"id": 3,
"shortname": "volunteer"
},
"person_id": null,
"members": [],
"current_step": {
"id": 42,
"shortname": "APPROVED"
},
"data": {
"shirt_size": "L"
},
"calculated_data": {},
"created_at": "2026-05-07T11:24:53+00:00",
"updated_at": "2026-05-07T12:00:00+00:00",
"anonymized_at": null,
"deactivated_at": null,
"deleted_at": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Not found."
}
}
Example response (409, OptimisticLockFailed):
{
"error": {
"code": "optimistic_lock_failed",
"message": "The Application was modified by someone else; reload and retry.",
"errors": {
"current": {
"updated_at": "2026-05-07T11:24:53+00:00"
}
}
}
}
Example response (422, ApplicationTypeImmutable):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"application_type": [
"The application_type cannot be changed once set."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Soft-delete an Application.
requires authentication
Requires explicit confirm: true. Missing/false confirmation returns 422 and
the Application remains intact. Soft-deleted Applications return 404 from
all subsequent v2 endpoints.
Required permission: all_persons_and_applications_administrate
Example request:
curl --request DELETE \
"https://{instance}.festiwa.re/api/v2/projects/564/applications/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"confirm\": true
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/applications/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"confirm": true
};
fetch(url, {
method: "DELETE",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/applications/16';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'confirm' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204, SoftDeleted):
Empty response
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Not found."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"confirm": [
"The confirm field must be accepted."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Advance many Applications in one request.
requires authentication
force=false(default): every id is pre-validated; if ANY would fail, the response is 400bulk_validation_failedwith afailed_idsarray and no Application is transitioned.force=true: every id is processed; failures are reported per-entity inside a 200 response. Sync/async dispatch still follows ChangeWorkflowStepJob::$maxModelsForAsync (default 25); above that threshold the response is 202dispatched.
Bulk dry_run ABOVE the sync threshold returns 400
dry_run_async_unsupported — the preview cannot be produced asynchronously.
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/applications/transitions/bulk" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"ids\": [
4711,
4712
],
\"to_step_id\": 42,
\"to_step_name\": \"APPROVED\",
\"application_type\": \"vendor\",
\"workflow_id\": 5,
\"note\": \"Approved.\",
\"act_as_backend_creation\": false,
\"dry_run\": false,
\"force\": false
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/applications/transitions/bulk"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"ids": [
4711,
4712
],
"to_step_id": 42,
"to_step_name": "APPROVED",
"application_type": "vendor",
"workflow_id": 5,
"note": "Approved.",
"act_as_backend_creation": false,
"dry_run": false,
"force": false
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/applications/transitions/bulk';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'ids' => [
4711,
4712,
],
'to_step_id' => 42,
'to_step_name' => 'APPROVED',
'application_type' => 'vendor',
'workflow_id' => 5,
'note' => 'Approved.',
'act_as_backend_creation' => false,
'dry_run' => false,
'force' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, SyncBulk):
{
"data": {
"4711": {
"status": "success",
"previous_step_id": 41,
"current_step_id": 42,
"triggers_fired": 3
},
"4712": {
"status": "skipped",
"reason": "already_in_step",
"previous_step_id": 42
}
}
}
Example response (202, AsyncDispatched):
{
"status": "dispatched",
"count": 50
}
Example response (400, BulkValidationFailed):
{
"error": {
"code": "bulk_validation_failed",
"message": "One or more transitions are not possible. Use force=true to process the eligible entries.",
"failed_ids": [
{
"id": 4712,
"reason": "already_in_step",
"previous_step_id": 42
},
{
"id": 4713,
"reason": "application_not_found",
"previous_step_id": null
}
]
}
}
Example response (400, DryRunAsyncUnsupported):
{
"error": {
"code": "dry_run_async_unsupported",
"message": "Bulk dry_run is not supported for batches above the sync threshold."
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You do not have permission to perform this action."
}
}
Example response (422, InvalidPayload):
{
"error": {
"code": "invalid_payload",
"message": "The given data was invalid.",
"errors": {
"ids": [
"The ids field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Advance a single Application to a new workflow step.
requires authentication
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/applications/16/transitions" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"to_step_id\": 42,
\"to_step_name\": \"APPROVED\",
\"application_type\": \"vendor\",
\"workflow_id\": 5,
\"note\": \"Approved via n8n.\",
\"act_as_backend_creation\": false,
\"dry_run\": false
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/applications/16/transitions"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"to_step_id": 42,
"to_step_name": "APPROVED",
"application_type": "vendor",
"workflow_id": 5,
"note": "Approved via n8n.",
"act_as_backend_creation": false,
"dry_run": false
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/applications/16/transitions';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'to_step_id' => 42,
'to_step_name' => 'APPROVED',
'application_type' => 'vendor',
'workflow_id' => 5,
'note' => 'Approved via n8n.',
'act_as_backend_creation' => false,
'dry_run' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Transitioned):
{
"data": {
"application_id": 4711,
"previous_workflow_step": {
"id": 41,
"shortname": "PENDING",
"name": "Pending"
},
"current_workflow_step": {
"id": 42,
"shortname": "APPROVED",
"name": "Approved",
"entered_at": "2026-05-25T10:00:00+00:00"
},
"triggers_fired": 3,
"note_saved": true
}
}
Example response (200, DryRun):
{
"data": {
"application_id": 4711,
"previous_workflow_step": {
"id": 41,
"shortname": "PENDING",
"name": "Pending"
},
"current_workflow_step": {
"id": 42,
"shortname": "APPROVED",
"name": "Approved",
"entered_at": "2026-05-25T10:00:00+00:00"
},
"triggers_fired": 0,
"note_saved": false,
"dry_run": true
}
}
Example response (400, AlreadyInStep):
{
"error": {
"code": "already_in_step",
"message": "The target is already in workflow step APPROVED."
}
}
Example response (400, StepNotInWorkflow):
{
"error": {
"code": "step_not_in_workflow",
"message": "The target step does not belong to the identified workflow."
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You do not have permission to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (404, WorkflowNotFound):
{
"error": {
"code": "workflow_not_found",
"message": "Workflow could not be resolved from the supplied disambiguation."
}
}
Example response (422, InvalidPayload):
{
"error": {
"code": "invalid_payload",
"message": "One of `to_step_id` or `to_step_name` is required.",
"errors": {
"to_step": [
"One of `to_step_id` or `to_step_name` is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Ticket Types
Paginated list of Ticket Types in a project.
requires authentication
Required permission: ticketcode_see
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/ticket-types?per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/ticket-types"
);
const params = {
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/ticket-types';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": 161,
"name": "Shuttle Berlin",
"shortname": "BFST",
"description": "Bus shuttle from Berlin Ostbahnhof",
"issue": 100,
"available_in_backend": true,
"ticket_category_id": 7,
"created_at": "2026-05-07T11:24:53+00:00",
"updated_at": "2026-05-07T11:24:53+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create a new Ticket Type in a project.
requires authentication
Required permission: ticketcode_administrate
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/ticket-types" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "name=Shuttle Berlin"\
--form "shortname=BFST"\
--form "description=Bus shuttle from Berlin Ostbahnhof to the festival site."\
--form "issue=100"\
--form "available_in_backend=1"\
--form "ticket_category_id=7"\
--form "additional_html=<p>Welcome!</p>"\
--form "ticket_background=@/tmp/phpbETLPN" const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/ticket-types"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('name', 'Shuttle Berlin');
body.append('shortname', 'BFST');
body.append('description', 'Bus shuttle from Berlin Ostbahnhof to the festival site.');
body.append('issue', '100');
body.append('available_in_backend', '1');
body.append('ticket_category_id', '7');
body.append('additional_html', '<p>Welcome!</p>');
body.append('ticket_background', document.querySelector('input[name="ticket_background"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/ticket-types';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'name',
'contents' => 'Shuttle Berlin'
],
[
'name' => 'shortname',
'contents' => 'BFST'
],
[
'name' => 'description',
'contents' => 'Bus shuttle from Berlin Ostbahnhof to the festival site.'
],
[
'name' => 'issue',
'contents' => '100'
],
[
'name' => 'available_in_backend',
'contents' => '1'
],
[
'name' => 'ticket_category_id',
'contents' => '7'
],
[
'name' => 'additional_html',
'contents' => '<p>Welcome!</p>'
],
[
'name' => 'ticket_background',
'contents' => fopen('/tmp/phpbETLPN', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Created):
{
"id": 161,
"name": "Shuttle Berlin",
"shortname": "BFST",
"description": null,
"issue": 100,
"available_in_backend": true,
"ticket_category_id": null,
"created_at": "2026-05-07T11:24:53+00:00",
"updated_at": "2026-05-07T11:24:53+00:00"
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"name": [
"The name field is required."
],
"shortname": [
"The shortname must be 2-4 alphanumeric characters."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show a single Ticket Type by id.
requires authentication
Required permission: ticketcode_see
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/ticket-types/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/ticket-types/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/ticket-types/16';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"id": 161,
"name": "Shuttle Berlin",
"shortname": "BFST",
"description": "Bus shuttle from Berlin Ostbahnhof",
"issue": 100,
"available_in_backend": true,
"ticket_category_id": 7,
"created_at": "2026-05-07T11:24:53+00:00",
"updated_at": "2026-05-07T11:24:53+00:00"
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Ticket Type not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update a Ticket Type (PATCH = sparse merge; PUT = full replace).
requires authentication
Required permission: ticketcode_administrate
Example request:
curl --request PUT \
"https://{instance}.festiwa.re/api/v2/projects/564/ticket-types/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "name=Shuttle Berlin"\
--form "shortname=BFST"\
--form "description=Bus shuttle from Berlin Ostbahnhof to the festival site."\
--form "issue=100"\
--form "available_in_backend=1"\
--form "ticket_category_id=7"\
--form "additional_html=<p>Welcome!</p>"\
--form "ticket_background=@/tmp/phpTqbynj" const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/ticket-types/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('name', 'Shuttle Berlin');
body.append('shortname', 'BFST');
body.append('description', 'Bus shuttle from Berlin Ostbahnhof to the festival site.');
body.append('issue', '100');
body.append('available_in_backend', '1');
body.append('ticket_category_id', '7');
body.append('additional_html', '<p>Welcome!</p>');
body.append('ticket_background', document.querySelector('input[name="ticket_background"]').files[0]);
fetch(url, {
method: "PUT",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/ticket-types/16';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'name',
'contents' => 'Shuttle Berlin'
],
[
'name' => 'shortname',
'contents' => 'BFST'
],
[
'name' => 'description',
'contents' => 'Bus shuttle from Berlin Ostbahnhof to the festival site.'
],
[
'name' => 'issue',
'contents' => '100'
],
[
'name' => 'available_in_backend',
'contents' => '1'
],
[
'name' => 'ticket_category_id',
'contents' => '7'
],
[
'name' => 'additional_html',
'contents' => '<p>Welcome!</p>'
],
[
'name' => 'ticket_background',
'contents' => fopen('/tmp/phpTqbynj', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"id": 161,
"name": "Shuttle Berlin (updated)",
"shortname": "BFST",
"description": "Updated description.",
"issue": 150,
"available_in_backend": true,
"ticket_category_id": 7,
"created_at": "2026-05-07T11:24:53+00:00",
"updated_at": "2026-05-07T12:00:00+00:00"
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Ticket Type not found."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"shortname": [
"The shortname must be 2-4 alphanumeric characters."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Delete a Ticket Type. Refuses with 409 if the type still has issued ticket codes or offers attached.
requires authentication
Required permission: ticketcode_administrate
Example request:
curl --request DELETE \
"https://{instance}.festiwa.re/api/v2/projects/564/ticket-types/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/ticket-types/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "DELETE",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/ticket-types/16';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204, Deleted):
Empty response
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Ticket Type not found."
}
}
Example response (409, HasTicketCodes):
{
"error": {
"code": "ticket_type_has_codes",
"message": "This ticket type has issued ticket codes and cannot be deleted."
}
}
Example response (409, HasOffers):
{
"error": {
"code": "ticket_type_has_offers",
"message": "This ticket type has offers attached and cannot be deleted."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Ticket Codes
Paginated list of Ticket Codes in a project.
requires authentication
Each item nests its ticket_type (id, shortname) and its owner
(id, firstname, lastname, email); owner is null when the code
is unowned.
Required permission: ticketcode_see
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/ticket-codes?per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/ticket-codes"
);
const params = {
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/ticket-codes';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": "9b1c4d2e-0000-4000-8000-000000000001",
"code": "ABC123",
"ticket_type": {
"id": 42,
"shortname": "VIP"
},
"owner": {
"id": "7a2f4d2e-0000-4000-8000-000000000002",
"firstname": "Anna",
"lastname": "Meyer",
"email": "anna@example.com"
},
"created_at": "2026-06-01T10:00:00+00:00",
"updated_at": "2026-06-01T10:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Orders
Paginated list of Orders in a project.
requires authentication
Required permission: order_see
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/orders?per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/orders"
);
const params = {
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/orders';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": "3f8a4d2e-0000-4000-8000-000000000001",
"order_number": "2026-000123",
"current_status": "paid",
"firstname": "Anna",
"lastname": "Meyer",
"email": "anna@example.com",
"created_at": "2026-06-01T10:00:00+00:00",
"updated_at": "2026-06-01T10:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Offers
Money fields are JSON numbers: price is the gross amount in the project's
currency (the currency is a per-project setting, not part of the response),
and tax is a rate as a decimal (0.19 = 19%), not a computed amount.
Paginated list of OfferCategories in a project.
requires authentication
Sort: order ASC (admin-curated shop display order), id ASC tiebreaker.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/offer-categories?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offer-categories"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offer-categories';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": 42,
"project_id": 5,
"name": "Outbound",
"shortname": "outbound",
"description": "Bus shuttles leaving the festival",
"order": 1,
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T09:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create an OfferCategory in a project.
requires authentication
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/offer-categories" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Outbound\",
\"shortname\": \"outbound\",
\"description\": \"Bus shuttles leaving the festival.\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offer-categories"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Outbound",
"shortname": "outbound",
"description": "Bus shuttles leaving the festival."
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offer-categories';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Outbound',
'shortname' => 'outbound',
'description' => 'Bus shuttles leaving the festival.',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Created):
{
"id": 42,
"project_id": 5,
"name": "Outbound",
"shortname": "outbound",
"description": null,
"order": 0,
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T09:00:00+00:00"
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"name": [
"The name field is required."
],
"shortname": [
"The shortname field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show a single OfferCategory.
requires authentication
?withOffers=1 adds the legacy combined payload — each offer with
its offerable (TicketType) inlined, mirroring v1's
with('offers.offerable'). Unpaginated. The new
/offer-categories/{category}/offers sub-resource is the
best-practice path for new integrators.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16?withOffers=" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16"
);
const params = {
"withOffers": "0",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'withOffers' => '0',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"id": 42,
"project_id": 5,
"name": "Outbound",
"shortname": "outbound",
"description": "Bus shuttles leaving the festival",
"order": 1,
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T09:00:00+00:00"
}
Example response (200, OK (withOffers=1)):
{
"id": 42,
"project_id": 5,
"name": "Outbound",
"shortname": "outbound",
"description": "Bus shuttles leaving the festival",
"order": 1,
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T09:00:00+00:00",
"offers": [
{
"id": 300,
"shortname": "shuttle-bln",
"description": "Bus shuttle from Berlin",
"price": 2500,
"tax": 19,
"issue": 100,
"ticket_count": 1,
"available_from": "2026-06-01T00:00:00+00:00",
"available_to": "2026-06-20T00:00:00+00:00",
"published_from": null,
"published_to": null,
"sort_order": 1,
"offerable": {
"id": 161,
"name": "Shuttle Berlin",
"shortname": "BFST",
"description": "Bus shuttle from Berlin Ostbahnhof",
"issue": 100,
"available_in_backend": true,
"ticket_category_id": 7,
"created_at": "2026-05-07T11:24:53+00:00",
"updated_at": "2026-05-07T11:24:53+00:00"
}
}
]
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update an OfferCategory. Both PUT and PATCH route here.
requires authentication
Optimistic lock via updated_at. Mismatch → 409
optimistic_lock_failed. PUT requires every editable field;
PATCH treats omissions as preserve-existing.
Example request:
curl --request PUT \
"https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Outbound\",
\"shortname\": \"outbound\",
\"description\": \"Bus shuttles leaving the festival.\",
\"order\": 2,
\"updated_at\": \"2026-06-23T09:00:00+00:00\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Outbound",
"shortname": "outbound",
"description": "Bus shuttles leaving the festival.",
"order": 2,
"updated_at": "2026-06-23T09:00:00+00:00"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Outbound',
'shortname' => 'outbound',
'description' => 'Bus shuttles leaving the festival.',
'order' => 2,
'updated_at' => '2026-06-23T09:00:00+00:00',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"id": 42,
"project_id": 5,
"name": "Outbound (updated)",
"shortname": "outbound",
"description": "Bus shuttles leaving the festival",
"order": 2,
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T12:00:00+00:00"
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (409, OptimisticLockFailed):
{
"error": {
"code": "optimistic_lock_failed",
"message": "The resource was modified after your last read. Re-fetch and retry.",
"errors": {
"current": {
"updated_at": "2026-06-23T12:00:00+00:00"
}
}
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"updated_at": [
"The updated_at field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Soft-delete an OfferCategory.
requires authentication
Requires {"confirm": true} body. Returns 409
offer_category_has_offers (with count in the message) if the
category still has offers attached.
Example request:
curl --request DELETE \
"https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"confirm\": true
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"confirm": true
};
fetch(url, {
method: "DELETE",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'confirm' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204, Deleted):
Empty response
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (409, HasOffers):
{
"error": {
"code": "offer_category_has_offers",
"message": "This offer category cannot be deleted because offers are still attached. (3)"
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"confirm": [
"The confirm field must be accepted."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Paginated list of OfferableOptions in a project.
requires authentication
Optional filter by offerable_id + offerable_type. The polymorphic
relation has no project_id column on the join — scoping is enforced
by filtering against TicketTypes belonging to the URL's {project}.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/offerable-options?offerable_id=16&offerable_type=architecto&per_page=25" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offerable-options"
);
const params = {
"offerable_id": "16",
"offerable_type": "architecto",
"per_page": "25",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offerable-options';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'offerable_id' => '16',
'offerable_type' => 'architecto',
'per_page' => '25',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": 88,
"name": "Early bird seat",
"description": "Discounted seat for early buyers",
"issue": 50,
"overbook_allowance": 0,
"active": true,
"offerable_id": 161,
"offerable_type": "App\\TicketType",
"valid_from": "2026-06-01T00:00:00+00:00",
"valid_to": "2026-06-20T00:00:00+00:00",
"sort_order": 1,
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T09:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create an OfferableOption attached to a TicketType.
requires authentication
Resolves offerable_id × offerable_type, then verifies the
offerable belongs to the URL's {project} via
assertBelongsToProject() — cross-project returns 404 (same guard
used for URL-bound cross-tenant checks).
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/offerable-options" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"offerable_id\": 161,
\"offerable_type\": \"App\\\\TicketType\",
\"name\": \"Early bird seat\",
\"description\": \"Discounted seat for early buyers.\",
\"issue\": 50,
\"overbook_allowance\": 16,
\"active\": true,
\"valid_from\": \"2026-06-01\",
\"valid_to\": \"2026-06-20\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offerable-options"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"offerable_id": 161,
"offerable_type": "App\\TicketType",
"name": "Early bird seat",
"description": "Discounted seat for early buyers.",
"issue": 50,
"overbook_allowance": 16,
"active": true,
"valid_from": "2026-06-01",
"valid_to": "2026-06-20"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offerable-options';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'offerable_id' => 161,
'offerable_type' => 'App\\TicketType',
'name' => 'Early bird seat',
'description' => 'Discounted seat for early buyers.',
'issue' => 50,
'overbook_allowance' => 16,
'active' => true,
'valid_from' => '2026-06-01',
'valid_to' => '2026-06-20',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Created):
{
"id": 88,
"name": "Early bird seat",
"description": "Discounted seat for early buyers",
"issue": 50,
"overbook_allowance": 0,
"active": true,
"offerable_id": 161,
"offerable_type": "App\\TicketType",
"valid_from": "2026-06-01T00:00:00+00:00",
"valid_to": "2026-06-20T00:00:00+00:00",
"sort_order": 0,
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T09:00:00+00:00"
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"offerable_id": [
"The offerable_id field is required."
],
"name": [
"The name field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show a single OfferableOption.
requires authentication
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/offerable-options/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offerable-options/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offerable-options/16';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"id": 88,
"name": "Early bird seat",
"description": "Discounted seat for early buyers",
"issue": 50,
"overbook_allowance": 0,
"active": true,
"offerable_id": 161,
"offerable_type": "App\\TicketType",
"valid_from": "2026-06-01T00:00:00+00:00",
"valid_to": "2026-06-20T00:00:00+00:00",
"sort_order": 1,
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T09:00:00+00:00"
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update an OfferableOption. Both PUT and PATCH route here.
requires authentication
offerable_id and offerable_type are prohibited on update —
re-parenting is not supported. Optimistic lock via updated_at.
Example request:
curl --request PUT \
"https://{instance}.festiwa.re/api/v2/projects/564/offerable-options/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Early bird seat\",
\"description\": \"Discounted seat for early buyers.\",
\"issue\": 75,
\"overbook_allowance\": 16,
\"active\": true,
\"valid_from\": \"2026-06-01\",
\"valid_to\": \"2026-06-20\",
\"updated_at\": \"2026-06-23T09:00:00+00:00\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offerable-options/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Early bird seat",
"description": "Discounted seat for early buyers.",
"issue": 75,
"overbook_allowance": 16,
"active": true,
"valid_from": "2026-06-01",
"valid_to": "2026-06-20",
"updated_at": "2026-06-23T09:00:00+00:00"
};
fetch(url, {
method: "PUT",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offerable-options/16';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Early bird seat',
'description' => 'Discounted seat for early buyers.',
'issue' => 75,
'overbook_allowance' => 16,
'active' => true,
'valid_from' => '2026-06-01',
'valid_to' => '2026-06-20',
'updated_at' => '2026-06-23T09:00:00+00:00',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"id": 88,
"name": "Early bird seat (updated)",
"description": "Discounted seat for early buyers",
"issue": 75,
"overbook_allowance": 0,
"active": true,
"offerable_id": 161,
"offerable_type": "App\\TicketType",
"valid_from": "2026-06-01T00:00:00+00:00",
"valid_to": "2026-06-20T00:00:00+00:00",
"sort_order": 1,
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T12:00:00+00:00"
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (409, OptimisticLockFailed):
{
"error": {
"code": "optimistic_lock_failed",
"message": "The resource was modified after your last read. Re-fetch and retry.",
"errors": {
"current": {
"updated_at": "2026-06-23T12:00:00+00:00"
}
}
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"offerable_id": [
"The offerable_id field is prohibited."
],
"updated_at": [
"The updated_at field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Soft-delete an OfferableOption.
requires authentication
Requires {"confirm": true} body. Returns 409
offerable_option_has_order_positions (with count in the message)
if the option still has order_positions attached.
Example request:
curl --request DELETE \
"https://{instance}.festiwa.re/api/v2/projects/564/offerable-options/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"confirm\": true
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offerable-options/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"confirm": true
};
fetch(url, {
method: "DELETE",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offerable-options/16';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'confirm' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204, Deleted):
Empty response
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (409, HasOrderPositions):
{
"error": {
"code": "offerable_option_has_order_positions",
"message": "This option cannot be deleted because order positions still reference it. (5)"
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"confirm": [
"The confirm field must be accepted."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Paginated list of Offers in a given OfferCategory.
requires authentication
Best-practice sub-resource path. Each offer carries its offerable
(TicketType) inlined by default — no opt-in flag. Mirrors Tier 2's
/event-plans/{plan}/events pattern. The legacy
/offer-categories/{category}?withOffers=1 endpoint provides the
same data unpaginated for v1 callers.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16/offers?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16/offers"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offer-categories/16/offers';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": 300,
"shortname": "shuttle-bln",
"description": "Bus shuttle from Berlin",
"price": 2500,
"tax": 19,
"issue": 100,
"ticket_count": 1,
"available_from": "2026-06-01T00:00:00+00:00",
"available_to": "2026-06-20T00:00:00+00:00",
"published_from": null,
"published_to": null,
"sort_order": 1,
"offerable": {
"id": 161,
"name": "Shuttle Berlin",
"shortname": "BFST",
"description": "Bus shuttle from Berlin Ostbahnhof",
"issue": 100,
"available_in_backend": true,
"ticket_category_id": 7,
"created_at": "2026-05-07T11:24:53+00:00",
"updated_at": "2026-05-07T11:24:53+00:00"
}
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Paginated list of Offers in a project.
requires authentication
Admin surface: hidden and unpublished offers are included.
Each item carries its offerable (ticket type) and offer_category.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/offers?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offers"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offers';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": 311,
"shortname": "Camping Club Hamburg",
"name_translations": null,
"description": null,
"price": 0,
"tax": 0,
"issue": 0,
"ticket_count": 1,
"hidden_link": "club-hamburg",
"image_url": "https://instance.example/storage/api-uploads/offers/1753364000_logo.png",
"email_product": true,
"max_order_amount": 8,
"available_from": null,
"available_to": null,
"published_from": null,
"published_to": null,
"sort_order": 1,
"updated_at": "2026-07-24T09:00:00+00:00",
"offerable": {
"id": 208,
"name": "Camping Club Hamburg",
"shortname": "CCHH",
"description": null,
"capacity": 0,
"valid_from": null,
"valid_to": null,
"available_in_backend": true,
"category_id": null,
"ticket_background_url": null,
"additional_html": null,
"created_at": "2026-07-24T09:00:00+00:00",
"updated_at": "2026-07-24T09:00:00+00:00"
},
"offer_category": {
"id": 91,
"project_id": 5,
"name": "Camping",
"shortname": "camping",
"description": null,
"order": 0,
"created_at": "2026-07-24T09:00:00+00:00",
"updated_at": "2026-07-24T09:00:00+00:00"
}
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Create an Offer (composite).
requires authentication
Atomic: the ticket type is created from ticket_type[...] XOR
referenced via ticket_type_id; the category is referenced via
offer_category_id XOR found-or-created from offer_category[...]
(keyed on shortname + project). Any failure leaves zero rows and
zero stored files. Content type: multipart/form-data.
The 201 response carries hidden_link and image_url — consumers
(n8n) build the individualized shop link without a second request.
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/offers" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "shortname=Camping Club Hamburg"\
--form "name[de]=Camping Club Hamburg"\
--form "name[en]=Camping Club Hamburg"\
--form "description=Eius et animi quos velit et."\
--form "price=0"\
--form "tax=0"\
--form "issue=60"\
--form "hidden_link=club-hamburg"\
--form "hide_show_all_link=1"\
--form "email_product="\
--form "max_order_amount=16"\
--form "published_from=2026-08-19T22:44:41"\
--form "published_to=2052-09-11"\
--form "available_from=2026-08-19T22:44:41"\
--form "available_to=2052-09-11"\
--form "ticket_type_id=208"\
--form "ticket_type[name]=Camping Club Hamburg"\
--form "ticket_type[shortname]=CCHH"\
--form "ticket_type[description]=Eius et animi quos velit et."\
--form "ticket_type[issue]=60"\
--form "ticket_type[available_in_backend]=1"\
--form "ticket_type[ticket_category_id]=16"\
--form "ticket_type[valid_from]=2026-08-19T22:44:41"\
--form "ticket_type[valid_to]=2052-09-11"\
--form "ticket_type[additional_html]=<p>Welcome, Club Hamburg!</p>"\
--form "offer_category_id=91"\
--form "offer_category[name]=Camping"\
--form "offer_category[shortname]=camping"\
--form "offer_category[description]=Eius et animi quos velit et."\
--form "tax_rules[][layout]=architecto"\
--form "tax_rules[][key]=architecto"\
--form "tax_rules[][attributes][label]=n"\
--form "tax_rules[][attributes][percent]=1"\
--form "tax_rules[][attributes][rate]=1"\
--form "image=@/tmp/phpQNKm3Z" \
--form "ticket_type[ticket_background]=@/tmp/phpoJ4mqo" const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offers"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('shortname', 'Camping Club Hamburg');
body.append('name[de]', 'Camping Club Hamburg');
body.append('name[en]', 'Camping Club Hamburg');
body.append('description', 'Eius et animi quos velit et.');
body.append('price', '0');
body.append('tax', '0');
body.append('issue', '60');
body.append('hidden_link', 'club-hamburg');
body.append('hide_show_all_link', '1');
body.append('email_product', '');
body.append('max_order_amount', '16');
body.append('published_from', '2026-08-19T22:44:41');
body.append('published_to', '2052-09-11');
body.append('available_from', '2026-08-19T22:44:41');
body.append('available_to', '2052-09-11');
body.append('ticket_type_id', '208');
body.append('ticket_type[name]', 'Camping Club Hamburg');
body.append('ticket_type[shortname]', 'CCHH');
body.append('ticket_type[description]', 'Eius et animi quos velit et.');
body.append('ticket_type[issue]', '60');
body.append('ticket_type[available_in_backend]', '1');
body.append('ticket_type[ticket_category_id]', '16');
body.append('ticket_type[valid_from]', '2026-08-19T22:44:41');
body.append('ticket_type[valid_to]', '2052-09-11');
body.append('ticket_type[additional_html]', '<p>Welcome, Club Hamburg!</p>');
body.append('offer_category_id', '91');
body.append('offer_category[name]', 'Camping');
body.append('offer_category[shortname]', 'camping');
body.append('offer_category[description]', 'Eius et animi quos velit et.');
body.append('tax_rules[][layout]', 'architecto');
body.append('tax_rules[][key]', 'architecto');
body.append('tax_rules[][attributes][label]', 'n');
body.append('tax_rules[][attributes][percent]', '1');
body.append('tax_rules[][attributes][rate]', '1');
body.append('image', document.querySelector('input[name="image"]').files[0]);
body.append('ticket_type[ticket_background]', document.querySelector('input[name="ticket_type[ticket_background]"]').files[0]);
fetch(url, {
method: "POST",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offers';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'shortname',
'contents' => 'Camping Club Hamburg'
],
[
'name' => 'name[de]',
'contents' => 'Camping Club Hamburg'
],
[
'name' => 'name[en]',
'contents' => 'Camping Club Hamburg'
],
[
'name' => 'description',
'contents' => 'Eius et animi quos velit et.'
],
[
'name' => 'price',
'contents' => '0'
],
[
'name' => 'tax',
'contents' => '0'
],
[
'name' => 'issue',
'contents' => '60'
],
[
'name' => 'hidden_link',
'contents' => 'club-hamburg'
],
[
'name' => 'hide_show_all_link',
'contents' => '1'
],
[
'name' => 'email_product',
'contents' => ''
],
[
'name' => 'max_order_amount',
'contents' => '16'
],
[
'name' => 'published_from',
'contents' => '2026-08-19T22:44:41'
],
[
'name' => 'published_to',
'contents' => '2052-09-11'
],
[
'name' => 'available_from',
'contents' => '2026-08-19T22:44:41'
],
[
'name' => 'available_to',
'contents' => '2052-09-11'
],
[
'name' => 'ticket_type_id',
'contents' => '208'
],
[
'name' => 'ticket_type[name]',
'contents' => 'Camping Club Hamburg'
],
[
'name' => 'ticket_type[shortname]',
'contents' => 'CCHH'
],
[
'name' => 'ticket_type[description]',
'contents' => 'Eius et animi quos velit et.'
],
[
'name' => 'ticket_type[issue]',
'contents' => '60'
],
[
'name' => 'ticket_type[available_in_backend]',
'contents' => '1'
],
[
'name' => 'ticket_type[ticket_category_id]',
'contents' => '16'
],
[
'name' => 'ticket_type[valid_from]',
'contents' => '2026-08-19T22:44:41'
],
[
'name' => 'ticket_type[valid_to]',
'contents' => '2052-09-11'
],
[
'name' => 'ticket_type[additional_html]',
'contents' => '<p>Welcome, Club Hamburg!</p>'
],
[
'name' => 'offer_category_id',
'contents' => '91'
],
[
'name' => 'offer_category[name]',
'contents' => 'Camping'
],
[
'name' => 'offer_category[shortname]',
'contents' => 'camping'
],
[
'name' => 'offer_category[description]',
'contents' => 'Eius et animi quos velit et.'
],
[
'name' => 'tax_rules[][layout]',
'contents' => 'architecto'
],
[
'name' => 'tax_rules[][key]',
'contents' => 'architecto'
],
[
'name' => 'tax_rules[][attributes][label]',
'contents' => 'n'
],
[
'name' => 'tax_rules[][attributes][percent]',
'contents' => '1'
],
[
'name' => 'tax_rules[][attributes][rate]',
'contents' => '1'
],
[
'name' => 'image',
'contents' => fopen('/tmp/phpQNKm3Z', 'r')
],
[
'name' => 'ticket_type[ticket_background]',
'contents' => fopen('/tmp/phpoJ4mqo', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Created):
{
"data": {
"id": 311,
"shortname": "Camping Club Hamburg",
"price": 0,
"tax": 0,
"hidden_link": "club-hamburg",
"image_url": "https://instance.example/storage/api-uploads/offers/1753364000_logo.png",
"offerable": {
"id": 208,
"shortname": "CCHH",
"ticket_background_url": "https://instance.example/storage/api-uploads/ticket-backgrounds/1753364000_bg.jpg",
"additional_html": "<p>Welcome!</p>"
},
"offer_category": {
"id": 91,
"name": "Camping",
"shortname": "camping"
}
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"ticket_type.shortname": [
"The ticket type.shortname field format is invalid."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show a single Offer.
requires authentication
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/offers/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offers/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offers/16';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": {
"id": 311,
"shortname": "Camping Club Hamburg",
"price": 0,
"hidden_link": "club-hamburg",
"image_url": null,
"offerable": {
"id": 208,
"shortname": "CCHH"
},
"offer_category": {
"id": 91,
"shortname": "camping"
}
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update an Offer (offer fields only). Both PUT and PATCH route here.
requires authentication
Optimistic lock via updated_at — mismatch → 409
optimistic_lock_failed. Omitted keys keep their current value on both
verbs; PUT additionally requires every mandatory field (shortname,
price, tax, issue, email_product), PATCH requires none of them.
Ticket-type fields are rejected with 422 — change them via the
ticket-types endpoint. Image changes: multipart POST + _method=PATCH.
Example request:
curl --request PUT \
"https://{instance}.festiwa.re/api/v2/projects/564/offers/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: multipart/form-data" \
--header "Accept: application/json" \
--form "shortname=Camping Club Hamburg"\
--form "price=0"\
--form "tax=0"\
--form "issue=0"\
--form "email_product=1"\
--form "name[de]=Camping Club Hamburg"\
--form "description=Eius et animi quos velit et."\
--form "hidden_link=club-hamburg-2027"\
--form "hide_show_all_link=1"\
--form "max_order_amount=16"\
--form "published_from=2026-08-19T22:44:41"\
--form "published_to=2052-09-11"\
--form "available_from=2026-08-19T22:44:41"\
--form "available_to=2052-09-11"\
--form "offer_category_id=91"\
--form "updated_at=2026-07-24T09:00:00+00:00"\
--form "image=@/tmp/phpylAAto" const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offers/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "multipart/form-data",
"Accept": "application/json",
};
const body = new FormData();
body.append('shortname', 'Camping Club Hamburg');
body.append('price', '0');
body.append('tax', '0');
body.append('issue', '0');
body.append('email_product', '1');
body.append('name[de]', 'Camping Club Hamburg');
body.append('description', 'Eius et animi quos velit et.');
body.append('hidden_link', 'club-hamburg-2027');
body.append('hide_show_all_link', '1');
body.append('max_order_amount', '16');
body.append('published_from', '2026-08-19T22:44:41');
body.append('published_to', '2052-09-11');
body.append('available_from', '2026-08-19T22:44:41');
body.append('available_to', '2052-09-11');
body.append('offer_category_id', '91');
body.append('updated_at', '2026-07-24T09:00:00+00:00');
body.append('image', document.querySelector('input[name="image"]').files[0]);
fetch(url, {
method: "PUT",
headers,
body,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offers/16';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'multipart/form-data',
'Accept' => 'application/json',
],
'multipart' => [
[
'name' => 'shortname',
'contents' => 'Camping Club Hamburg'
],
[
'name' => 'price',
'contents' => '0'
],
[
'name' => 'tax',
'contents' => '0'
],
[
'name' => 'issue',
'contents' => '0'
],
[
'name' => 'email_product',
'contents' => '1'
],
[
'name' => 'name[de]',
'contents' => 'Camping Club Hamburg'
],
[
'name' => 'description',
'contents' => 'Eius et animi quos velit et.'
],
[
'name' => 'hidden_link',
'contents' => 'club-hamburg-2027'
],
[
'name' => 'hide_show_all_link',
'contents' => '1'
],
[
'name' => 'max_order_amount',
'contents' => '16'
],
[
'name' => 'published_from',
'contents' => '2026-08-19T22:44:41'
],
[
'name' => 'published_to',
'contents' => '2052-09-11'
],
[
'name' => 'available_from',
'contents' => '2026-08-19T22:44:41'
],
[
'name' => 'available_to',
'contents' => '2052-09-11'
],
[
'name' => 'offer_category_id',
'contents' => '91'
],
[
'name' => 'updated_at',
'contents' => '2026-07-24T09:00:00+00:00'
],
[
'name' => 'image',
'contents' => fopen('/tmp/phpylAAto', 'r')
],
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": {
"id": 311,
"shortname": "Camping Club Hamburg",
"hidden_link": "club-hamburg-2027",
"updated_at": "2026-07-24T12:00:00+00:00"
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (409, OptimisticLockFailed):
{
"error": {
"code": "optimistic_lock_failed",
"message": "The resource was modified after your last read. Re-fetch and retry.",
"errors": {
"current": {
"updated_at": "2026-07-24T12:00:00+00:00"
}
}
}
}
Example response (422, TicketTypeFieldsRejected):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"ticket_type": [
"Ticket type fields cannot be changed through the offer. Please use the ticket-types endpoint."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Soft-delete an Offer — the offer only, never the ticket type.
requires authentication
Requires {"confirm": true} body. Deletion goes through the model
(identical to backend deletion), so sales history, coupon links and
rebooking links survive by design. To also remove an orphaned ticket
type, call DELETE /ticket-types/{id} separately — it refuses with 409
while the type is still in use.
Example request:
curl --request DELETE \
"https://{instance}.festiwa.re/api/v2/projects/564/offers/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"confirm\": true
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/offers/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"confirm": true
};
fetch(url, {
method: "DELETE",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/offers/16';
$response = $client->delete(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'confirm' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (204, Deleted):
Empty response
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"confirm": [
"The confirm field must be accepted."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Coupons
Create a coupon in a project.
requires authentication
Generates a unique code server-side and returns the coupon with its linked offers.
Restrict the coupon to specific products via offer_ids; omit them to apply to all.
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/coupons" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"type\": \"relative\",
\"value\": 100,
\"max_units\": 1,
\"is_singleuse\": true,
\"active\": true,
\"uses_reserved_contingent\": true,
\"valid_from\": \"2026-06-01T00:00:00+00:00\",
\"valid_until\": \"2026-06-30T00:00:00+00:00\",
\"code_prefix\": \"BUS\",
\"offer_ids\": [
16
],
\"person_id\": \"9b1c8a7e-4f3d-4c2a-9e1b-2f5a6c7d8e9f\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/coupons"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"type": "relative",
"value": 100,
"max_units": 1,
"is_singleuse": true,
"active": true,
"uses_reserved_contingent": true,
"valid_from": "2026-06-01T00:00:00+00:00",
"valid_until": "2026-06-30T00:00:00+00:00",
"code_prefix": "BUS",
"offer_ids": [
16
],
"person_id": "9b1c8a7e-4f3d-4c2a-9e1b-2f5a6c7d8e9f"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/coupons';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'type' => 'relative',
'value' => 100.0,
'max_units' => 1,
'is_singleuse' => true,
'active' => true,
'uses_reserved_contingent' => true,
'valid_from' => '2026-06-01T00:00:00+00:00',
'valid_until' => '2026-06-30T00:00:00+00:00',
'code_prefix' => 'BUS',
'offer_ids' => [
16,
],
'person_id' => '9b1c8a7e-4f3d-4c2a-9e1b-2f5a6c7d8e9f',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Created):
{
"id": 12,
"project_id": 5,
"code": "BUS-7F3K9A",
"type": "relative",
"value": 100,
"active": true,
"is_singleuse": true,
"valid_from": null,
"valid_until": null,
"person_id": null,
"offer_ids": [
300,
301
],
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T09:00:00+00:00"
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"type": [
"The type field is required."
],
"value": [
"The value field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show a coupon, including its redemption status.
requires authentication
order_amount is the number of orders placed on the coupon, redeemed is a
convenience boolean, and is_valid reflects the model's own validity check
(active + within the validity window + not a spent single-use coupon).
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/coupons/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/coupons/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/coupons/16';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": {
"id": 12,
"project_id": 5,
"code": "BUS-7F3K9A",
"type": "relative",
"value": 100,
"active": true,
"is_singleuse": true,
"valid_from": null,
"valid_until": null,
"person_id": null,
"offer_ids": [
300,
301
],
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T09:00:00+00:00",
"order_amount": 0,
"redeemed": false,
"is_valid": true
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Update a coupon (deactivate / reactivate).
requires authentication
Toggles active only. Deactivation is the storno path and works even for an
already-redeemed coupon — such a coupon cannot be deleted (its order positions
pin it via a RESTRICT foreign key), but it can always be switched inactive.
Example request:
curl --request PATCH \
"https://{instance}.festiwa.re/api/v2/projects/564/coupons/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"active\": false,
\"max_units\": 1,
\"uses_reserved_contingent\": true
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/coupons/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"active": false,
"max_units": 1,
"uses_reserved_contingent": true
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/coupons/16';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'active' => false,
'max_units' => 1,
'uses_reserved_contingent' => true,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": {
"id": 12,
"project_id": 5,
"code": "BUS-7F3K9A",
"type": "relative",
"value": 100,
"active": false,
"is_singleuse": true,
"valid_from": null,
"valid_until": null,
"person_id": null,
"offer_ids": [
300,
301
],
"created_at": "2026-06-23T09:00:00+00:00",
"updated_at": "2026-06-23T09:10:00+00:00",
"order_amount": 0,
"redeemed": false,
"is_valid": false
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "Not found."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"active": [
"The active field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Scheduler
List scheduler plans visible to the authenticated caller.
requires authentication
Paginated list filtered by SchedulerPlan::scopeVisibleToUser($user) —
plans with view_roles are restricted to users whose roles match;
plans with no view_roles fall back to the scheduler_see permission.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/event-plans?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/event-plans"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/event-plans';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (500):
Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
{
"message": "An error occurred and was reported automatically.<br>(Event ID: 02d09cdf81c546d0b62667612fa4c989)<br>We usually resolve errors within a few hours.<br>Please try again later!"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show a single scheduler plan.
requires authentication
Default returns plan scalars only — no events key on the response.
Set ?withEvents=1 to receive the full v1-equivalent shape: events
embedded as nested array, each event with area, jobs, attendees,
applications populated. For paginated event reads, use the dedicated
/event-plans/{plan}/events sub-resource instead.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/event-plans/16?withEvents=" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/event-plans/16"
);
const params = {
"withEvents": "0",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/event-plans/16';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'withEvents' => '0',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (500):
Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
{
"message": "An error occurred and was reported automatically.<br>(Event ID: b3975481c0f04d10b6ef89ddfd53eaef)<br>We usually resolve errors within a few hours.<br>Please try again later!"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List events in a scheduler plan.
requires authentication
Paginated. Default emission per event: area and jobs as full nested
resources; attendees and applications as UUID arrays. Upgrade
attendees / applications to full nested Person / Application resources
via the opt-in ?withAttendees=1 / ?withApplications=1 flags
(mirrors ApplicationsController's sideload pattern).
403 if the caller cannot view the plan (canUserView). 404 if the
plan does not belong to the URL's project.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/event-plans/16/events?withAttendees=&withApplications=&per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/event-plans/16/events"
);
const params = {
"withAttendees": "0",
"withApplications": "0",
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/event-plans/16/events';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'withAttendees' => '0',
'withApplications' => '0',
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (500):
Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
{
"message": "An error occurred and was reported automatically.<br>(Event ID: 8e92aeb213334a439996c3454b591c34)<br>We usually resolve errors within a few hours.<br>Please try again later!"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List areas in a project.
requires authentication
Paginated list of areas scoped to the project.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/areas?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/areas"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/areas';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (500):
Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
{
"message": "An error occurred and was reported automatically.<br>(Event ID: c63907147910409ba5c6a9165a8da029)<br>We usually resolve errors within a few hours.<br>Please try again later!"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List events scheduled in an area.
requires authentication
Paginated. Each event carries area, jobs, attendees, applications
eager-loaded inline (mirrors the v1 emission).
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/areas/16/events?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/areas/16/events"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/areas/16/events';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (500):
Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
{
"message": "An error occurred and was reported automatically.<br>(Event ID: c9471bb78be54c3b8556865f95386fcb)<br>We usually resolve errors within a few hours.<br>Please try again later!"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List scheduler event types in a project.
requires authentication
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/event-types?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/event-types"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/event-types';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (500):
Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
{
"message": "An error occurred and was reported automatically.<br>(Event ID: 46b4ea6f2c504ea3a22202468808d27b)<br>We usually resolve errors within a few hours.<br>Please try again later!"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Supply
Paginated list of supplies (resources) in a project.
requires authentication
A supply is the type of equipment ("Label printer"), a supply item is the single
physical device. category_ids lets a client group the list without a second call.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/supplies?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/supplies"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/supplies';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": 7,
"project_id": 5,
"name": "Label printer",
"description": "Zebra label printers",
"image_url": null,
"requires_existing_stock": true,
"allow_partial_duration": false,
"orderable_from": null,
"orderable_to": null,
"category_ids": [
2
],
"created_at": "2026-07-27T09:00:00+00:00",
"updated_at": "2026-07-27T09:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Paginated list of supply categories that carry at least one supply of this project.
requires authentication
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/supply-categories?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/supply-categories"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/supply-categories';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": 2,
"name": "Technology",
"description": "Printers, scanners, radios",
"image_url": null,
"supply_ids": [
7,
8
],
"created_at": "2026-07-27T09:00:00+00:00",
"updated_at": "2026-07-27T09:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Paginated list of the single devices belonging to one supply.
requires authentication
latest_tracking_event tells a client who currently holds the device.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/supplies/16/items?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/supplies/16/items"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/supplies/16/items';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": 17,
"supply_id": 7,
"name": "Zebra ZD421 #3",
"description": null,
"serial_number": "ZD421-0099123",
"tag_number": "A1B2C3D4",
"owner_type": "team",
"owner_id": "4",
"owner_name": "Crew Technik",
"available_from": null,
"available_to": null,
"image_url": null,
"label_url": "https://demo.festiwa.re/supply-label/5/7/17.pdf",
"latest_tracking_event": null,
"created_at": "2026-07-27T09:00:00+00:00",
"updated_at": "2026-07-27T09:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Register a single device on a supply.
requires authentication
This is the write half of the "register device" kiosk flow: pick a resource, name the
device, let the server assign a scannable tag, optionally set an owner, then print the
label from label_url.
Tag assignment: send tag_number to use your own code, send nothing to have one
generated (this bypasses the project setting "generate tag numbers"), or send
generate_tag: false to create an item without a tag.
Owner: either owner_type + owner_id, or owner_person_tag_number for the scan
path. The owner must belong to the same project.
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/supplies/16/items" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Zebra ZD421 #3\",
\"description\": \"Label printer, 300 dpi\",
\"serial_number\": \"ZD421-0099123\",
\"tag_number\": \"A1B2C3D4\",
\"generate_tag\": true,
\"owner_type\": \"team\",
\"owner_id\": \"17\",
\"owner_person_tag_number\": \"0010000101\",
\"available_from\": \"2026-08-01T08:00:00+02:00\",
\"available_to\": \"2026-08-10T20:00:00+02:00\",
\"image_url\": \"https:\\/\\/storage.example.com\\/supply\\/zd421.jpg\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/supplies/16/items"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Zebra ZD421 #3",
"description": "Label printer, 300 dpi",
"serial_number": "ZD421-0099123",
"tag_number": "A1B2C3D4",
"generate_tag": true,
"owner_type": "team",
"owner_id": "17",
"owner_person_tag_number": "0010000101",
"available_from": "2026-08-01T08:00:00+02:00",
"available_to": "2026-08-10T20:00:00+02:00",
"image_url": "https:\/\/storage.example.com\/supply\/zd421.jpg"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/supplies/16/items';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Zebra ZD421 #3',
'description' => 'Label printer, 300 dpi',
'serial_number' => 'ZD421-0099123',
'tag_number' => 'A1B2C3D4',
'generate_tag' => true,
'owner_type' => 'team',
'owner_id' => '17',
'owner_person_tag_number' => '0010000101',
'available_from' => '2026-08-01T08:00:00+02:00',
'available_to' => '2026-08-10T20:00:00+02:00',
'image_url' => 'https://storage.example.com/supply/zd421.jpg',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Created):
{
"data": {
"id": 17,
"supply_id": 7,
"name": "Zebra ZD421 #3",
"description": null,
"serial_number": "ZD421-0099123",
"tag_number": "A1B2C3D4",
"owner_type": "team",
"owner_id": "4",
"owner_name": "Crew Technik",
"available_from": null,
"available_to": null,
"image_url": null,
"label_url": "https://demo.festiwa.re/supply-label/5/7/17.pdf",
"latest_tracking_event": null,
"created_at": "2026-07-27T09:00:00+00:00",
"updated_at": "2026-07-27T09:00:00+00:00"
}
}
Example response (400, InvalidScan):
{
"error": {
"code": "bad_request",
"message": "Dieser Pass ist nicht mehr aktuell."
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"tag_number": [
"The tag number has already been taken."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Show a single device by id, without knowing its supply.
requires authentication
The kiosk scans a tag, gets the id from the tracking response, and re-reads the item here to refresh owner and holder.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/supply-items/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/supply-items/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/supply-items/16';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": {
"id": 17,
"supply_id": 7,
"name": "Zebra ZD421 #3",
"description": null,
"serial_number": "ZD421-0099123",
"tag_number": "A1B2C3D4",
"owner_type": "team",
"owner_id": "4",
"owner_name": "Crew Technik",
"available_from": null,
"available_to": null,
"image_url": null,
"label_url": "https://demo.festiwa.re/supply-label/5/7/17.pdf",
"latest_tracking_event": null,
"created_at": "2026-07-27T09:00:00+00:00",
"updated_at": "2026-07-27T09:00:00+00:00"
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Book a tracking event for one or many devices: hand out, take back, send to maintenance.
requires authentication
One event is written per item, so a kiosk can scan a whole box of radios and hand them
to one person in a single call. issued, received and transferred need a person
(by id or by scan); every other event type is a stock movement and works without one.
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/supply-tracking-events" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"event_type\": \"issued\",
\"supply_item_ids\": [
16
],
\"supply_item_tag_numbers\": [
\"architecto\"
],
\"person_id\": \"550e8400-e29b-41d4-a716-446655440000\",
\"person_tag_number\": \"0010000101\",
\"supply_order_id\": 42,
\"area_id\": 3,
\"stock_location\": \"Warehouse A, Shelf 3\",
\"geolocation\": \"51.3397,12.3731\",
\"w3w\": \"filled.count.soap\",
\"annotations\": \"Cracked housing, sent to repair\",
\"image_url\": \"https:\\/\\/storage.example.com\\/tracking\\/img001.jpg\",
\"tracked_at\": \"2026-07-15T10:30:00+02:00\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/supply-tracking-events"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"event_type": "issued",
"supply_item_ids": [
16
],
"supply_item_tag_numbers": [
"architecto"
],
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"person_tag_number": "0010000101",
"supply_order_id": 42,
"area_id": 3,
"stock_location": "Warehouse A, Shelf 3",
"geolocation": "51.3397,12.3731",
"w3w": "filled.count.soap",
"annotations": "Cracked housing, sent to repair",
"image_url": "https:\/\/storage.example.com\/tracking\/img001.jpg",
"tracked_at": "2026-07-15T10:30:00+02:00"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/supply-tracking-events';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'event_type' => 'issued',
'supply_item_ids' => [
16,
],
'supply_item_tag_numbers' => [
'architecto',
],
'person_id' => '550e8400-e29b-41d4-a716-446655440000',
'person_tag_number' => '0010000101',
'supply_order_id' => 42,
'area_id' => 3,
'stock_location' => 'Warehouse A, Shelf 3',
'geolocation' => '51.3397,12.3731',
'w3w' => 'filled.count.soap',
'annotations' => 'Cracked housing, sent to repair',
'image_url' => 'https://storage.example.com/tracking/img001.jpg',
'tracked_at' => '2026-07-15T10:30:00+02:00',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (201, Created):
{
"data": [
{
"id": 901,
"supply_item_id": 17,
"supply_order_id": null,
"event_type": "issued",
"person_id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": 3,
"area_id": null,
"stock_location": null,
"geolocation": null,
"w3w": null,
"annotations": null,
"image_url": null,
"tracked_at": "2026-07-27T09:00:00+00:00",
"created_at": "2026-07-27T09:00:00+00:00",
"updated_at": "2026-07-27T09:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (400, InvalidScan):
{
"error": {
"code": "bad_request",
"message": "Dieser Pass ist nicht mehr aktuell."
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"person_tag_number": [
"The person tag number field is required."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Teams
Paginated list of teams in a project.
requires authentication
Added for the supply kiosk's owner picker ("this device belongs to team X"); it is a plain project-scoped list and useful to any client that needs team names and ids.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/teams?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/teams"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/teams';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": 4,
"project_id": 5,
"name": "Crew Technik",
"shortname": "tech",
"description": null,
"email": null,
"team_type_id": 2,
"team_id": null,
"order": 100,
"created_at": "2026-07-27T09:00:00+00:00",
"updated_at": "2026-07-27T09:00:00+00:00"
}
],
"meta": {
"current_page": 1,
"per_page": 25,
"total": 1,
"last_page": 1
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Access Control
Check a scanned person against this device's access gates.
requires authentication
The "Scan Access" page of the check-in app. Send the scanned NFC chip UID or pass QR
value as code, or a person_id for a manual lookup. The answer is always HTTP 200 —
an unknown or superseded badge is a scan outcome, not a request error.
status is one of:
granted— the person holds at least one configured gatedenied— the person holds none of them; compareaccess_groupsagainstrequired_gatesidentified— no gate is configured for this project on this user, so no verdict is rendered. This is the identify-only mode ("whose wristband is this?"), not an error.tag_devalidated— the badge exists but was superseded (pass reprinted) or deleted. Show this differently from a denial: the person is usually entitled, the badge is stale.unknown— nothing resolves for this code
Which person fields appear is an instance setting (access_scan_show_*); disabled
fields are absent from the payload entirely rather than nulled.
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/access-checks" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"code\": \"04A1B2C3D4E5F6\",
\"person_id\": \"550e8400-e29b-41d4-a716-446655440000\"
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/access-checks"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"code": "04A1B2C3D4E5F6",
"person_id": "550e8400-e29b-41d4-a716-446655440000"
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/access-checks';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'code' => '04A1B2C3D4E5F6',
'person_id' => '550e8400-e29b-41d4-a716-446655440000',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Granted):
{
"data": {
"status": "granted",
"person": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"firstname": "Alex",
"lastname": "Rieger",
"fullname": "Alex Rieger",
"person_types": [
"Crew"
],
"access_groups": [
{
"id": 104,
"name": "Backstage",
"shortname": "BS"
}
]
},
"matched_gates": [
{
"id": 104,
"project_id": 7,
"name": "Backstage",
"shortname": "BS",
"level": "access_group"
}
],
"required_gates": [
{
"id": 104,
"project_id": 7,
"name": "Backstage",
"shortname": "BS",
"level": "access_group"
}
],
"fields": [
"person_types",
"access_groups"
]
}
}
Example response (200, Denied):
{
"data": {
"status": "denied",
"person": {
"id": "550e8400-e29b-41d4-a716-446655440000",
"firstname": "Alex",
"lastname": "Rieger",
"fullname": "Alex Rieger",
"person_types": [
"Visitor"
],
"access_groups": [
{
"id": 99,
"name": "Camping",
"shortname": "CA"
}
]
},
"matched_gates": [],
"required_gates": [
{
"id": 104,
"project_id": 7,
"name": "Backstage",
"shortname": "BS",
"level": "access_group"
}
],
"fields": [
"person_types",
"access_groups"
]
}
}
Example response (200, Devalidated):
{
"data": {
"status": "tag_devalidated",
"person": null,
"matched_gates": [],
"required_gates": [],
"fields": [
"person_types",
"access_groups"
]
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Example response (422, ValidationFailed):
{
"error": {
"code": "validation_failed",
"message": "The given data was invalid.",
"errors": {
"code": [
"The code field is required when person id is not present."
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Offline dataset for the "Scan Access" page: every scannable person of a project with their codes and access groups.
requires authentication
This is a full snapshot, paginated — not a delta. Deliberately so: a revoked access group or a devalidated badge is a deletion, and an incremental feed that only ships changed rows would leave the removed ones sitting in the device's IndexedDB, letting somebody through a gate they were taken off hours ago. Getting deletions right needs tombstones on three tables; until then the honest primitive is a snapshot the device swaps in wholesale.
To avoid re-downloading it every minute, poll {@see version()} — one cheap request that returns the dataset's fingerprint — and only refetch when it changed.
codes holds every value that resolves to this person when scanned: valid NFC chip
UIDs plus the numbers of printed passes (a pass whose QR was never mirrored into
nfc_tags still resolves server-side, so it must resolve offline too).
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/access-holders?per_page=100&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/access-holders"
);
const params = {
"per_page": "100",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/access-holders';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '100',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": [
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"firstname": "Alex",
"lastname": "Rieger",
"fullname": "Alex Rieger",
"person_types": [
"Crew"
],
"access_groups": [
{
"id": 104,
"name": "Backstage",
"shortname": "BS"
}
],
"access_group_ids": [
104
],
"codes": [
"04A1B2C3D4E5F6",
"0070000123"
]
}
],
"meta": {
"current_page": 1,
"per_page": 100,
"total": 1,
"last_page": 1,
"version": "7:2026-08-01T18:32:18+00:00",
"fields": [
"person_types",
"access_groups"
]
},
"links": {
"first": null,
"last": null,
"prev": null,
"next": null
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Fingerprint of the current dataset — poll this instead of refetching the snapshot.
requires authentication
The value is opaque; compare it for equality and nothing else. It changes whenever a person, an access-group assignment, a badge or a pass in this project changes.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/access-holders/version" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/access-holders/version"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/access-holders/version';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, OK):
{
"data": {
"version": "7:2026-08-01T18:32:18+00:00",
"total": 842
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You are not authorized to perform this action."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Projects
List projects the authenticated caller can access.
requires authentication
Filtered by ProjectContext::getAllowedProjectIdsForUser(). Callers without
an explicit user_projects restriction receive every project on the instance.
Use any returned id as the {project} parameter for project-nested v2 endpoints.
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects?per_page=25&page=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects"
);
const params = {
"per_page": "25",
"page": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'per_page' => '25',
'page' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (500):
Show headers
cache-control: no-cache, private
content-type: application/json
access-control-allow-origin: *
access-control-expose-headers: Cache-Control, Content-Language, Content-Type, Expires, Last-Modified, Pragma
{
"message": "An error occurred and was reported automatically.<br>(Event ID: adfe73e9e49848f7ab6a74aacb993ff2)<br>We usually resolve errors within a few hours.<br>Please try again later!"
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Workflows
Export a workflow as a portable payload.
requires authentication
Serialises the workflow, its steps and triggers into a project-agnostic payload that uses shortnames instead of project-local ids. Feed the result straight into the import endpoint.
Required permission:
workflow_see
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/workflows/16/export" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/workflows/16/export"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/workflows/16/export';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Exported):
{
"data": {
"version": 1,
"workflow_shortname": "onboarding",
"type_shortname": "artist",
"type_model": "person",
"name": "Onboarding",
"steps": [
{
"shortname": "NEW",
"is_start": true,
"triggers": [
{
"action_type": "notify_user",
"active": true,
"data": {
"notifiable_id": "crew@example.com"
}
}
]
}
]
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You do not have permission to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Import a workflow into a project.
requires authentication
Resolves every reference in the payload against the target project. If everything resolves the
workflow is created (or replaces a same-shortname workflow with no started journey) with all
triggers on. A missing dependency is a blocker: the import is refused (422) and nothing is
written — unless deactivate_unresolved is set, which commits with the affected triggers
switched off (listed in the report). A problem on a start or endpoint step is always a
showstopper and cannot be waived.
Required permission:
workflow_manage
Example request:
curl --request POST \
"https://{instance}.festiwa.re/api/v2/projects/564/workflows/import" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"payload\": {
\"workflow_shortname\": \"onboarding\",
\"type_shortname\": \"artist\",
\"steps\": [
[]
]
},
\"target_type\": \"artist\",
\"deactivate_unresolved\": false
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/workflows/import"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"payload": {
"workflow_shortname": "onboarding",
"type_shortname": "artist",
"steps": [
[]
]
},
"target_type": "artist",
"deactivate_unresolved": false
};
fetch(url, {
method: "POST",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/workflows/import';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'payload' => [
'workflow_shortname' => 'onboarding',
'type_shortname' => 'artist',
'steps' => [
[],
],
],
'target_type' => 'artist',
'deactivate_unresolved' => false,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Imported):
{
"data": {
"committed": true,
"workflow_id": 42,
"steps_created": 3,
"triggers_created": 5,
"deactivated_triggers": [],
"issues": []
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You do not have permission to perform this action."
}
}
Example response (422, Blockers):
{
"error": {
"code": "import_has_blockers",
"message": "The workflow has unresolved references. Prepare the target or re-send with deactivate_unresolved to import the affected triggers switched off.",
"report": {
"can_commit": false,
"issues": [
{
"severity": "blocker",
"code": "reference_unresolved",
"step_shortname": "NEW",
"field": "area",
"token": "mainstage"
}
]
}
}
}
Example response (422, Showstoppers):
{
"error": {
"code": "import_has_showstoppers",
"message": "The workflow cannot be imported. Resolve the showstoppers in the report first.",
"report": {
"can_commit": false,
"issues": [
{
"severity": "showstopper",
"code": "workflow_type_mismatch",
"token": "artist"
}
]
}
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
List a workflow's triggers.
requires authentication
Lists every trigger of the workflow's steps. Filter by on/off with active to find the
triggers an import switched off, then complete them via the update endpoint.
Required permission:
workflow_see
Example request:
curl --request GET \
--get "https://{instance}.festiwa.re/api/v2/projects/564/workflows/16/triggers?active=1" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json"const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/workflows/16/triggers"
);
const params = {
"active": "1",
};
Object.keys(params)
.forEach(key => url.searchParams.append(key, params[key]));
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
fetch(url, {
method: "GET",
headers,
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/workflows/16/triggers';
$response = $client->get(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'query' => [
'active' => '1',
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Listed):
{
"data": [
{
"id": 77,
"name": "Notify crew",
"action_type": "notify_user",
"triggers_on": [
"enter"
],
"active": false,
"observed_id": 42,
"data": {
"notifiable_id": null
}
}
]
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You do not have permission to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.
Fix and reactivate a workflow trigger.
requires authentication
Merge missing values into the trigger data and/or switch it back on — the final step of the transfer flow after an import deactivated a trigger it could not resolve. Scoped to the project: a trigger from another project is rejected.
Required permission:
workflow_manage
Example request:
curl --request PATCH \
"https://{instance}.festiwa.re/api/v2/projects/564/workflow-triggers/16" \
--header "Authorization: Bearer {ACCESS_TOKEN}" \
--header "Content-Type: application/json" \
--header "Accept: application/json" \
--data "{
\"active\": true,
\"data\": null
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/workflow-triggers/16"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"active": true,
"data": null
};
fetch(url, {
method: "PATCH",
headers,
body: JSON.stringify(body),
}).then(response => response.json());$client = new \GuzzleHttp\Client();
$url = 'https://{instance}.festiwa.re/api/v2/projects/564/workflow-triggers/16';
$response = $client->patch(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'active' => true,
'data' => null,
],
]
);
$body = $response->getBody();
print_r(json_decode((string) $body));Example response (200, Updated):
{
"data": {
"id": 77,
"name": "Notify crew",
"action_type": "notify_user",
"triggers_on": [
"enter"
],
"active": true,
"observed_id": 42,
"data": {
"notifiable_id": 123
}
}
}
Example response (401, Unauthorized):
{
"error": {
"code": "unauthorized",
"message": "Authentication required."
}
}
Example response (403, Forbidden):
{
"error": {
"code": "permission_denied",
"message": "You do not have permission to perform this action."
}
}
Example response (404, NotFound):
{
"error": {
"code": "not_found",
"message": "The requested resource was not found."
}
}
Received response:
Request failed with error:
Tip: Check that you're properly connected to the network.
If you're a maintainer of ths API, verify that your API is running and you've enabled CORS.
You can check the Dev Tools console for debugging information.