MENU navbar-image

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


Step 1 — Create an API User

In Nova: API Users → Create API User


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:

  1. Enter email + password → Login
  2. The consent screen shows the client name → Authorize
  3. Browser closes → n8n shows Connected

This step runs once. n8n stores the refresh token and auto-renews from this point.


Step 5 — Token lifecycle


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!"
}
 

Request      

GET api/v2/person-types

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

page   integer  optional    

Page number (1-indexed). Example: 1

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."
            ]
        }
    }
}
 

Request      

GET api/v2/projects/{project_id}/people

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Query Parameters

email   string  optional    

Optional exact-match filter against data.email. Example: anna@example.com

updated_since   string  optional    

Optional ISO 8601 timestamp; returns Persons modified at or after this moment. Example: 2026-04-30T00:00:00+00:00

person_type   string  optional    

Optional PersonType shortname filter. Example: volunteer

step   string  optional    

Optional. Filter by the Person's CURRENT workflow-step shortname. LIMITATION: "current" is the Person's latest step OVERALL, not per workflow — for a Person in several workflows an older step in one workflow is not matched while a newer step in another is active. Pass workflow_id to scope, and note it still filters on the latest-overall status. Single-workflow Persons (the norm) are unaffected. Example: acc_buildup

workflow_id   integer  optional    

Optional. Narrow the step filter to one Workflow — needed when a Person is in several. Example: 209

withJourney   boolean  optional    

Optional. Sideload current_steps (latest step per workflow) and journey (full step history) on each Person. Default false. Example: false

per_page   integer  optional    

Page size, clamped to 100. Example: 25

Body Parameters

per_page   integer  optional    

Must be at least 1. Example: 16

email   string  optional    

Must not be greater than 255 characters. Example: zbailey@example.net

updated_since   string  optional    

Must be a valid date in the format Y-m-d\TH:i:sP. Example: 2026-07-14T15:19:22+02:00

person_type   string  optional    

The shortname of an existing record in the types table. Example: architecto

step   string  optional    

Example: architecto

workflow_id   integer  optional    

The id of an existing record in the workflows table. Example: 16

withJourney   boolean  optional    

Example: true

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."
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/people

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Body Parameters

id   string  optional    

Optional caller-supplied UUID. Auto-generated if omitted; rejected with 409 if already used. Example: architecto

person_types   string[]     

Array of PersonType shortnames. At least one.

act_as_backend_creation   boolean  optional    

If true, the write is treated as backend-originated by downstream WorkflowTrigger evaluation. Default: false. Example: false

data   object     

Map of custom-data fields. Scalar values only.

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/people/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   string     

The ID of the person. Example: architecto

project   integer     

Project id. Example: 16

person   string     

Person UUID. Example: architecto

Update a Person.

requires authentication

Both PUT and PATCH route here. The behavioural switch is replaceMissing:

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."
            ]
        }
    }
}
 

Request      

PUT api/v2/projects/{project_id}/people/{id}

PATCH api/v2/projects/{project_id}/people/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   string     

The ID of the person. Example: architecto

project   integer     

Project id. Example: 16

person   string     

Person UUID. Example: architecto

Body Parameters

person_types   string[]  optional    

Optional array of PersonType shortnames. If present, REPLACES existing assignments. If omitted, pivot is unchanged.

act_as_backend_creation   boolean  optional    

When true, downstream WorkflowTrigger evaluation treats this write as backend-originated (matches create/v1 semantics) — triggers flagged skip_after_backend_creation do NOT fire. Used so API/n8n write-backs (e.g. relaying coupon codes onto a Person) don't re-trigger the outbound webhook. Defaults to false. The legacy camelCase alias actAsBackendCreation is accepted (snake-case wins on collision). Example: false

data   object     

Map of custom-data fields (scalar values only).

updated_at   string     

ISO 8601 timestamp of the last known version (optimistic-lock token). Example: 2026-04-30T11:24:53+00:00

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."
            ]
        }
    }
}
 

Request      

DELETE api/v2/projects/{project_id}/people/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   string     

The ID of the person. Example: architecto

project   integer     

Project id. Example: 16

person   string     

Person UUID. Example: architecto

Body Parameters

confirm   boolean     

Must be exactly true to confirm deletion. Example: true

Advance many Persons in one request.

requires authentication

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."
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/people/transitions/bulk

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Body Parameters

ids   string[]     

Array of Person UUIDs to transition.

to_step_id   integer  optional    

Target WorkflowStep id (one-of with to_step_name). Example: 42

to_step_name   string  optional    

Target WorkflowStep shortname (one-of with to_step_id). Example: APPROVED

person_type   string  optional    

PersonType shortname (one-of with workflow_id). Example: volunteer

workflow_id   integer  optional    

Workflow id (one-of with person_type; wins on collision). Example: 5

note   string  optional    

Optional audit note. Saved as a Comment on each transitioned Person. Example: Approved.

act_as_backend_creation   boolean  optional    

See single-endpoint docs. Default: false. Example: false

dry_run   boolean  optional    

If true, no DB writes and no triggers fire — the response reports per-id intended outcomes. Default: false. Example: false

force   boolean  optional    

If true, partial failures are graceful; if false, any failure aborts the batch with 400. Default: false.

Required permission: all_persons_and_applications_manage Example: false

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."
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/people/{person_id}/transitions

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

person_id   string     

The ID of the person. Example: architecto

project   integer     

Project id. Example: 16

person   string     

Person UUID. Example: architecto

Body Parameters

to_step_id   integer  optional    

Target WorkflowStep id (one-of with to_step_name; wins on collision). Example: 42

to_step_name   string  optional    

Target WorkflowStep shortname, case-insensitive (one-of with to_step_id). Example: APPROVED

person_type   string  optional    

PersonType shortname (one-of with workflow_id). Example: volunteer

workflow_id   integer  optional    

Workflow id (one-of with person_type; wins on collision). Example: 5

note   string  optional    

Optional audit note saved as a Comment on the Person. Example: Approved via n8n.

act_as_backend_creation   boolean  optional    

If true, triggers with skip_after_backend_creation=true do not fire. Default: false. CamelCase alias actAsBackendCreation accepted. Example: false

dry_run   boolean  optional    

If true, validate and report the intended outcome without writing to the DB and without firing triggers. Default: false.

Required permission: all_persons_and_applications_manage Example: false

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!"
}
 

Request      

GET api/v2/application-types

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

page   integer  optional    

Page number (1-indexed). Example: 1

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."
            ]
        }
    }
}
 

Request      

GET api/v2/projects/{project_id}/applications

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Query Parameters

application_type   string  optional    

Optional ApplicationType shortname filter. Example: volunteer

person_id   string  optional    

Optional Person UUID filter. Example: 01a1e3a6-efbf-4413-9db1-f36e60489254

step   string  optional    

Optional current workflow step shortname filter (read-only — does not transition). Example: applied

updated_since   string  optional    

Optional ISO 8601 timestamp; returns Applications modified at or after this moment. Example: 2026-05-07T00:00:00+00:00

per_page   integer  optional    

Page size, clamped to 100. Example: 25

withOwner   boolean  optional    

Sideload: inline the owner as a nested PersonV2Resource at owner. Default false. Example: false

withMembersCollection   boolean  optional    

Sideload: replace the members[] UUID array with nested PersonV2Resources. Default false. Example: false

withSubordinatedApplications   boolean  optional    

Sideload: inline subordinated applications (depth 1) at subordinated_applications. Default false. Example: false

withJourney   boolean  optional    

Sideload: inline workflow step history at journey[]. Default false. Example: false

withEvents   boolean  optional    

Sideload: inline scheduler events at events[]. Default false. Example: false

withEventAttendees   boolean  optional    

Sideload: only meaningful alongside withEvents=1. Replaces each event's attendees[] UUID array with PersonV2Resources. Default false. Example: false

withTickets   boolean  optional    

Sideload: inline ticket types at ticket_types[]. Default false. Example: false

withTranslatedValues   string  optional    

Translate data.* values via the application type's FormDefinition. Truthy non-locale values fall back to en. Example: `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[]).`

Body Parameters

per_page   integer  optional    

Must be at least 1. Example: 16

application_type   string  optional    

Example: architecto

person_id   string  optional    

Must be a valid UUID. Example: a4855dc5-0acb-33c3-b921-f4291f719ca0

step   string  optional    

Example: architecto

updated_since   string  optional    

Must be a valid date in the format Y-m-d\TH:i:sP. Example: 2026-07-14T15:19:22+02:00

withOwner   boolean  optional    

Example: false

withMembersCollection   boolean  optional    

Example: false

withSubordinatedApplications   boolean  optional    

Example: false

withJourney   boolean  optional    

Example: true

withEvents   boolean  optional    

Example: false

withEventAttendees   boolean  optional    

Example: false

withTickets   boolean  optional    

Example: true

withTranslatedValues   string  optional    

Example: architecto

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."
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/applications

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Body Parameters

id   string  optional    
application_type   string     

ApplicationType shortname. Immutable post-create. Example: volunteer

person_id   string  optional    

Optional Person UUID for the owner. Example: 01a1e3a6-efbf-4413-9db1-f36e60489254

data   object     

Map of custom-data fields. Scalar values only.

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/applications/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   integer     

The ID of the application. Example: 16

project   integer     

Project id. Example: 16

application   string     

Application UUID. Example: architecto

Query Parameters

withOwner   boolean  optional    

Sideload owner as nested PersonV2Resource. Default false. Example: false

withMembersCollection   boolean  optional    

Sideload members as nested PersonV2Resources. Default false. Example: false

withJourney   boolean  optional    

Sideload workflow step history. Default false. Example: false

withEvents   boolean  optional    

Sideload scheduler events. Default false. Example: false

withEventAttendees   boolean  optional    

Replace event attendees[] UUIDs with PersonV2Resources. Default false. Example: false

withTickets   boolean  optional    

Sideload ticket types. Default false. Example: false

withSubordinatedApplications   boolean  optional    

Sideload subordinated applications (depth 1). Default false. Example: false

withTranslatedValues   string  optional    

Translate data.* via FormDefinition. Example: de

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."
            ]
        }
    }
}
 

Request      

PUT api/v2/projects/{project_id}/applications/{id}

PATCH api/v2/projects/{project_id}/applications/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   integer     

The ID of the application. Example: 16

project   integer     

Project id. Example: 16

application   string     

Application UUID. Example: architecto

Body Parameters

data   object     

Map of custom-data fields (scalar values only).

updated_at   string     

ISO 8601 timestamp of the last known version (optimistic-lock token). Example: 2026-05-07T11:24:53+00:00

person_id   string  optional    

Optional owner Person UUID. Pass null to clear. Example: architecto

application_type   string  optional    
application_type_id   string  optional    
step_id   string  optional    
workflow_step_id   string  optional    

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."
            ]
        }
    }
}
 

Request      

DELETE api/v2/projects/{project_id}/applications/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   integer     

The ID of the application. Example: 16

project   integer     

Project id. Example: 16

application   string     

Application UUID. Example: architecto

Body Parameters

confirm   boolean     

Must be exactly true to confirm deletion. Example: true

Advance many Applications in one request.

requires authentication

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."
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/applications/transitions/bulk

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Body Parameters

ids   integer[]     

Array of Application ids to transition.

to_step_id   integer  optional    

Target WorkflowStep id (one-of with to_step_name). Example: 42

to_step_name   string  optional    

Target WorkflowStep shortname (one-of with to_step_id). Example: APPROVED

application_type   string  optional    

ApplicationType shortname (one-of with workflow_id). Example: vendor

workflow_id   integer  optional    

Workflow id (one-of with application_type; wins on collision). Example: 5

note   string  optional    

Optional audit note. Saved as a Comment on each transitioned Application. Example: Approved.

act_as_backend_creation   boolean  optional    

See single-endpoint docs. Default: false. Example: false

dry_run   boolean  optional    

If true, no DB writes and no triggers fire — the response reports per-id intended outcomes. Default: false. Example: false

force   boolean  optional    

If true, partial failures are graceful; if false, any failure aborts the batch with 400. Default: false.

Required permission: all_persons_and_applications_manage Example: false

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."
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/applications/{application_id}/transitions

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

application_id   integer     

The ID of the application. Example: 16

project   integer     

Project id. Example: 16

application   integer     

Application id. Example: 16

Body Parameters

to_step_id   integer  optional    

Target WorkflowStep id (one-of with to_step_name; wins on collision). Example: 42

to_step_name   string  optional    

Target WorkflowStep shortname, case-insensitive (one-of with to_step_id). Example: APPROVED

application_type   string  optional    

ApplicationType shortname (one-of with workflow_id). Example: vendor

workflow_id   integer  optional    

Workflow id (one-of with application_type; wins on collision). Example: 5

note   string  optional    

Optional audit note saved as a Comment on the Application. Example: Approved via n8n.

act_as_backend_creation   boolean  optional    

If true, triggers with skip_after_backend_creation=true do not fire. Default: false. CamelCase alias actAsBackendCreation accepted. Example: false

dry_run   boolean  optional    

If true, validate and report the intended outcome without writing to the DB and without firing triggers. Default: false.

Required permission: all_persons_and_applications_manage Example: false

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/ticket-types

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

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."
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/ticket-types

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Body Parameters

name   string     

Required on POST, optional on PATCH. Display name (max 255 chars). Must not be greater than 255 characters. Example: Shuttle Berlin

shortname   string     

Required on POST, optional on PATCH. TicketCode prefix — 2-4 alphanumeric characters. Must match the regex /^[a-zA-Z0-9]{2,4}$/. Example: BFST

description   string  optional    

Optional descriptive text. Nullable. Example: Bus shuttle from Berlin Ostbahnhof to the festival site.

issue   integer  optional    

Optional total capacity. 0 means unlimited (capacity managed via options instead). Must be at least 0. Example: 100

available_in_backend   boolean  optional    

Optional. Whether the ticket type is visible to backend operators. Example: true

ticket_category_id   integer  optional    

Optional category foreign key. Nullable. Example: 7

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/ticket-types/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   integer     

The ID of the ticket type. Example: 16

project   integer     

Project id. Example: 16

ticketType   integer     

Ticket Type id. Example: 16

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."
            ]
        }
    }
}
 

Request      

PUT api/v2/projects/{project_id}/ticket-types/{id}

PATCH api/v2/projects/{project_id}/ticket-types/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   integer     

The ID of the ticket type. Example: 16

project   integer     

Project id. Example: 16

ticketType   integer     

Ticket Type id. Example: 16

Body Parameters

name   string     

Required on POST, optional on PATCH. Display name (max 255 chars). Must not be greater than 255 characters. Example: Shuttle Berlin

shortname   string     

Required on POST, optional on PATCH. TicketCode prefix — 2-4 alphanumeric characters. Must match the regex /^[a-zA-Z0-9]{2,4}$/. Example: BFST

description   string  optional    

Optional descriptive text. Nullable. Example: Bus shuttle from Berlin Ostbahnhof to the festival site.

issue   integer  optional    

Optional total capacity. 0 means unlimited (capacity managed via options instead). Must be at least 0. Example: 100

available_in_backend   boolean  optional    

Optional. Whether the ticket type is visible to backend operators. Example: true

ticket_category_id   integer  optional    

Optional category foreign key. Nullable. Example: 7

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."
    }
}
 

Request      

DELETE api/v2/projects/{project_id}/ticket-types/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   integer     

The ID of the ticket type. Example: 16

project   integer     

Project id. Example: 16

ticketType   integer     

Ticket Type id. Example: 16

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/ticket-codes

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/orders

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/offer-categories

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

page   integer  optional    

Page number (1-indexed). Example: 1

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."
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/offer-categories

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Body Parameters

name   string     

Required. Display name, shown as the shop group header (max 255 chars). Must not be greater than 255 characters. Example: Outbound

shortname   string     

Required. Short identifier, unique per project (max 255 chars). Must not be greater than 255 characters. Example: outbound

description   string  optional    

Optional explanation. Nullable. Example: Bus shuttles leaving the festival.

project_id   string  optional    
typeable_model   string  optional    
id   string  optional    

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/offer-categories/{category_id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

category_id   integer     

The ID of the category. Example: 16

project   integer     

Project id. Example: 16

category   integer     

OfferCategory id. Example: 16

Query Parameters

withOffers   boolean  optional    

Include offers[] with each offer's offerable inlined (legacy v1-shape). Default: false. Example: false

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."
            ]
        }
    }
}
 

Request      

PUT api/v2/projects/{project_id}/offer-categories/{category_id}

PATCH api/v2/projects/{project_id}/offer-categories/{category_id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

category_id   integer     

The ID of the category. Example: 16

project   integer     

Project id. Example: 16

category   integer     

OfferCategory id. Example: 16

Body Parameters

name   string     

Required on PUT, optional on PATCH. Display name (max 255 chars). Must not be greater than 255 characters. Example: Outbound

shortname   string     

Required on PUT, optional on PATCH. Short identifier (max 255 chars). Must not be greater than 255 characters. Example: outbound

description   string  optional    

Optional explanation. Nullable. Example: Bus shuttles leaving the festival.

order   integer  optional    

Optional sort position within the project. Example: 2

updated_at   string     

Required ISO 8601 last-known timestamp (optimistic-lock token). Must be a valid date in the format Y-m-d\TH:i:sP. Example: 2026-06-23T09:00:00+00:00

project_id   string  optional    
typeable_model   string  optional    
id   string  optional    

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."
            ]
        }
    }
}
 

Request      

DELETE api/v2/projects/{project_id}/offer-categories/{category_id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

category_id   integer     

The ID of the category. Example: 16

project   integer     

Project id. Example: 16

category   integer     

OfferCategory id. Example: 16

Body Parameters

confirm   boolean     

Required guard against accidental deletion. Must be exactly truefalse or omission returns 422 and leaves the category intact. Must be accepted. Example: true

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/offerable-options

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Query Parameters

offerable_id   integer  optional    

Filter by the parent offerable's id. Example: 16

offerable_type   string  optional    

Filter by offerable class. Currently allow-listed to App\TicketType. Example: architecto

per_page   integer  optional    

Page size, clamped to 100. Example: 25

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."
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/offerable-options

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Body Parameters

offerable_id   integer     

Required. Parent TicketType id. Available types via GET /api/v2/projects/{project}/ticket-types. Example: 161

offerable_type   string     

Required. Always App\TicketType — the only allow-listed value. Example: App\TicketType

Must be one of:
  • App\TicketType
name   string     

Required. Display label (max 255 chars). Must not be greater than 255 characters. Example: Early bird seat

description   string  optional    

Optional help text. Nullable. Example: Discounted seat for early buyers.

issue   integer     

Required. Capacity. 0 means unlimited. Must be at least 0. Example: 50

overbook_allowance   integer  optional    

Optional. Coupon-only extra seats on top of issue (0 = off). Example: 16

active   boolean     

Required. Whether the option is purchasable. Example: true

valid_from   string  optional    

Optional availability window start (date). Nullable. Must be a valid date. Example: 2026-06-01

valid_to   string  optional    

Optional availability window end (date, after valid_from). Nullable. Must be a valid date. Must be a date after valid_from. Example: 2026-06-20

id   string  optional    
sort_order   string  optional    

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/offerable-options/{option_id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

option_id   integer     

The ID of the option. Example: 16

project   integer     

Project id. Example: 16

option   integer     

OfferableOption id. Example: 16

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."
            ]
        }
    }
}
 

Request      

PUT api/v2/projects/{project_id}/offerable-options/{option_id}

PATCH api/v2/projects/{project_id}/offerable-options/{option_id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

option_id   integer     

The ID of the option. Example: 16

project   integer     

Project id. Example: 16

option   integer     

OfferableOption id. Example: 16

Body Parameters

name   string     

Required on PUT, optional on PATCH. Display label (max 255 chars). Must not be greater than 255 characters. Example: Early bird seat

description   string  optional    

Optional help text. Nullable. Example: Discounted seat for early buyers.

issue   integer     

Required on PUT, optional on PATCH. Capacity. 0 means unlimited. Must be at least 0. Example: 75

overbook_allowance   integer  optional    

Optional. Coupon-only extra seats on top of issue (0 = off). Example: 16

active   boolean     

Required on PUT, optional on PATCH. Whether the option is purchasable. Example: true

valid_from   string  optional    

Optional availability window start (date). Nullable. Must be a valid date. Example: 2026-06-01

valid_to   string  optional    

Optional availability window end (date, after valid_from). Nullable. Must be a valid date. Must be a date after valid_from. Example: 2026-06-20

updated_at   string     

Required ISO 8601 last-known timestamp (optimistic-lock token). Must be a valid date in the format Y-m-d\TH:i:sP. Example: 2026-06-23T09:00:00+00:00

offerable_id   string  optional    
offerable_type   string  optional    
id   string  optional    
sort_order   string  optional    

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."
            ]
        }
    }
}
 

Request      

DELETE api/v2/projects/{project_id}/offerable-options/{option_id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

option_id   integer     

The ID of the option. Example: 16

project   integer     

Project id. Example: 16

option   integer     

OfferableOption id. Example: 16

Body Parameters

confirm   boolean     

Required guard against accidental deletion. Must be exactly truefalse or omission returns 422 and leaves the option intact. Must be accepted. Example: true

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/offer-categories/{category_id}/offers

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

category_id   integer     

The ID of the category. Example: 16

project   integer     

Project id. Example: 16

category   integer     

OfferCategory id. Example: 16

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

page   integer  optional    

Page number (1-indexed). Example: 1

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."
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/coupons

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Body Parameters

type   string     

Required. Discount type — relative (percentage) or absolute (fixed amount). Example: relative

Must be one of:
  • absolute
  • relative
value   number     

Required. Discount value. relative: 0 < value <= 100. absolute: value > 0. Example: 100

max_units   integer  optional    

Optional. Cap the discount to at most N covered order-position units (the most expensive ones). 0 = unlimited (covers every covered unit). Defaults to 1. relative/100 + max_units=1 grants exactly one free unit of any price. Must be at least 0. Example: 1

is_singleuse   boolean  optional    

Optional. Whether the coupon may be redeemed only once. Defaults to true. Example: true

active   boolean  optional    

Optional. Whether the coupon is active. Defaults to true. Example: true

valid_from   string  optional    

Optional ISO 8601 start of the validity window. Nullable. Must be a valid date. Example: 2026-06-01T00:00:00+00:00

valid_until   string  optional    

Optional ISO 8601 end of the validity window (>= valid_from). Nullable. Must be a valid date. Must be a date after or equal to valid_from. Example: 2026-06-30T00:00:00+00:00

code_prefix   string  optional    

Optional prefix prepended to the generated code (max 40, [A-Za-z0-9-]). Nullable. Must match the regex /^[A-Za-z0-9-]*$/. Must not be greater than 40 characters. Example: BUS

offer_ids   integer[]  optional    

The id of an existing record in the offers table.

person_id   string  optional    

Optional person (uuid) to link the coupon to. Nullable. Available people via GET /api/v2/projects/{project}/people. Must be a valid UUID. The id of an existing record in the people table. Example: 9b1c8a7e-4f3d-4c2a-9e1b-2f5a6c7d8e9f

code   string  optional    
id   string  optional    
project_id   string  optional    

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/coupons/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   integer     

The ID of the coupon. Example: 16

project   integer     

Project id. Example: 16

coupon   integer     

Coupon id. Example: 16

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."
            ]
        }
    }
}
 

Request      

PATCH api/v2/projects/{project_id}/coupons/{id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

id   integer     

The ID of the coupon. Example: 16

project   integer     

Project id. Example: 16

coupon   integer     

Coupon id. Example: 16

Body Parameters

active   boolean     

Required. Whether the coupon is active. Set false to deactivate (works even for an already-redeemed coupon — a redeemed coupon cannot be deleted, only deactivated). Example: false

max_units   integer  optional    

Optional. Cap the discount to at most N covered order-position units (the most expensive ones). 0 = unlimited. Must be at least 0. Example: 1

code   string  optional    
id   string  optional    
project_id   string  optional    

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!"
}
 

Request      

GET api/v2/projects/{project_id}/event-plans

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

page   integer  optional    

Page number (1-indexed). Example: 1

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!"
}
 

Request      

GET api/v2/projects/{project_id}/event-plans/{plan_id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

plan_id   integer     

The ID of the plan. Example: 16

project   integer     

Project id. Example: 16

plan   integer     

Scheduler plan id. Example: 16

Query Parameters

withEvents   boolean  optional    

Include the full nested events array (v1-shape). Default false. Example: false

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!"
}
 

Request      

GET api/v2/projects/{project_id}/event-plans/{plan_id}/events

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

plan_id   integer     

The ID of the plan. Example: 16

project   integer     

Project id. Example: 16

plan   integer     

Scheduler plan id (must belong to the project). Example: 16

Query Parameters

withAttendees   boolean  optional    

Upgrade attendees from UUIDs to nested Person resources. Default false. Example: false

withApplications   boolean  optional    

Upgrade applications from UUIDs to nested Application resources. Default false. Example: false

per_page   integer  optional    

Page size, clamped to 100. Example: 25

page   integer  optional    

Page number (1-indexed). Example: 1

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!"
}
 

Request      

GET api/v2/projects/{project_id}/areas

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

page   integer  optional    

Page number (1-indexed). Example: 1

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!"
}
 

Request      

GET api/v2/projects/{project_id}/areas/{area_id}/events

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

area_id   integer     

The ID of the area. Example: 16

project   integer     

Project id. Example: 16

area   integer     

Area id (must belong to the project). Example: 16

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

page   integer  optional    

Page number (1-indexed). Example: 1

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!"
}
 

Request      

GET api/v2/projects/{project_id}/event-types

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Project id. Example: 16

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

page   integer  optional    

Page number (1-indexed). Example: 1

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!"
}
 

Request      

GET api/v2/projects

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

Query Parameters

per_page   integer  optional    

Page size, clamped to 100. Example: 25

page   integer  optional    

Page number (1-indexed). Example: 1

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/workflows/{workflow_id}/export

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

workflow_id   integer     

The ID of the workflow. Example: 16

project   integer     

Project id. Example: 1

workflow   integer     

Workflow id (must belong to the project). Example: 5

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"
                }
            ]
        }
    }
}
 

Request      

POST api/v2/projects/{project_id}/workflows/import

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

project   integer     

Target project id. Example: 2

Body Parameters

payload   object     

The exported workflow payload (from the export endpoint).

workflow_shortname   string     

Workflow shortname. Example: onboarding

type_shortname   string     

Person/group type shortname. Example: artist

steps   object[]     

The workflow steps with their triggers.

target_type   string     

Shortname of the target type; must match the payload type. Example: artist

deactivate_unresolved   boolean  optional    

Commit despite blockers, importing unresolvable triggers switched off. Does not override a showstopper. Default: false.

Required permission: workflow_manage Example: false

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."
    }
}
 

Request      

GET api/v2/projects/{project_id}/workflows/{workflow_id}/triggers

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

workflow_id   integer     

The ID of the workflow. Example: 16

project   integer     

Project id. Example: 2

workflow   integer     

Workflow id (must belong to the project). Example: 42

Query Parameters

active   boolean  optional    

Filter by trigger state: 1 for on, 0 for off. Omit for all. Example: true

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."
    }
}
 

Request      

PATCH api/v2/projects/{project_id}/workflow-triggers/{workflowTrigger_id}

Headers

Authorization        

Example: Bearer {ACCESS_TOKEN}

Content-Type        

Example: application/json

Accept        

Example: application/json

URL Parameters

project_id   integer     

The ID of the project. Example: 564

workflowTrigger_id   integer     

The ID of the workflowTrigger. Example: 16

project   integer     

Project id. Example: 2

workflowTrigger   integer     

Workflow trigger id (must belong to the project). Example: 77

Body Parameters

active   boolean  optional    

Switch the trigger on/off. Example: true

data   object  optional    

Fields to merge into the trigger data (e.g. set notifiable_id).