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 Nova admin access
- An OAuth 2.0 client (n8n, custom integration, postman, etc.)
Step 1 — Create an API User
In Nova: 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 Nova: 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 Nova (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.
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 Nova doesn't match n8n's callback URL | Update the client in Nova with n8n's exact callback URL |
| API calls return 403 | The API user lacks the required permission | Add the permission in Nova → 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 Nova → 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: 0ef84cc6566c432dabe5508d4223f26f)<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-07-14T15:19:22+02:00\",
\"person_type\": \"architecto\",
\"step\": \"architecto\",
\"workflow_id\": 16,
\"withJourney\": true
}"
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-07-14T15:19:22+02:00",
"person_type": "architecto",
"step": "architecto",
"workflow_id": 16,
"withJourney": true
};
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-07-14T15:19:22+02:00',
'person_type' => 'architecto',
'step' => 'architecto',
'workflow_id' => 16,
'withJourney' => true,
],
]
);
$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: b7a6ed21bdc1484fb10be3cae996177c)<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-07-14T15:19:22+02:00\",
\"withOwner\": false,
\"withMembersCollection\": false,
\"withSubordinatedApplications\": false,
\"withJourney\": true,
\"withEvents\": false,
\"withEventAttendees\": false,
\"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-07-14T15:19:22+02:00",
"withOwner": false,
"withMembersCollection": false,
"withSubordinatedApplications": false,
"withJourney": true,
"withEvents": false,
"withEventAttendees": false,
"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-07-14T15:19:22+02:00',
'withOwner' => false,
'withMembersCollection' => false,
'withSubordinatedApplications' => false,
'withJourney' => true,
'withEvents' => false,
'withEventAttendees' => false,
'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 via Nova / 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: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Shuttle Berlin\",
\"shortname\": \"BFST\",
\"description\": \"Bus shuttle from Berlin Ostbahnhof to the festival site.\",
\"issue\": 100,
\"available_in_backend\": true,
\"ticket_category_id\": 7
}"
const url = new URL(
"https://{instance}.festiwa.re/api/v2/projects/564/ticket-types"
);
const headers = {
"Authorization": "Bearer {ACCESS_TOKEN}",
"Content-Type": "application/json",
"Accept": "application/json",
};
let body = {
"name": "Shuttle Berlin",
"shortname": "BFST",
"description": "Bus shuttle from Berlin Ostbahnhof to the festival site.",
"issue": 100,
"available_in_backend": true,
"ticket_category_id": 7
};
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/ticket-types';
$response = $client->post(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Shuttle Berlin',
'shortname' => 'BFST',
'description' => 'Bus shuttle from Berlin Ostbahnhof to the festival site.',
'issue' => 100,
'available_in_backend' => true,
'ticket_category_id' => 7,
],
]
);
$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: application/json" \
--header "Accept: application/json" \
--data "{
\"name\": \"Shuttle Berlin\",
\"shortname\": \"BFST\",
\"description\": \"Bus shuttle from Berlin Ostbahnhof to the festival site.\",
\"issue\": 100,
\"available_in_backend\": true,
\"ticket_category_id\": 7
}"
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",
};
let body = {
"name": "Shuttle Berlin",
"shortname": "BFST",
"description": "Bus shuttle from Berlin Ostbahnhof to the festival site.",
"issue": 100,
"available_in_backend": true,
"ticket_category_id": 7
};
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/ticket-types/16';
$response = $client->put(
$url,
[
'headers' => [
'Authorization' => 'Bearer {ACCESS_TOKEN}',
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'json' => [
'name' => 'Shuttle Berlin',
'shortname' => 'BFST',
'description' => 'Bus shuttle from Berlin Ostbahnhof to the festival site.',
'issue' => 100,
'available_in_backend' => true,
'ticket_category_id' => 7,
],
]
);
$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
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.
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,
\"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,
"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,
'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
}"
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
};
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,
],
]
);
$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: fb51b630af14400e9121e987e42d116f)<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: 6cc67225d1634352933f8ee0d373c4b1)<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: f4a4ba4b1722450ab6ee3cb533aedd44)<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: 35c636bb40de4c7fa1bd521a9b160a18)<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: d9a29e4d16a44c36b094489f376ee7d3)<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: 91f0e52aab514848a4400f16f1cf80ba)<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.
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: 6887d4d7c4334db492569de181e8d94f)<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.