Endpoints
GET/api/v1/agency
Your agency: branding, contacts, plan features and current usage. Handy as a connectivity check.
Request
curl -H "Authorization: Bearer $CASTINGS_TOKEN" https://castings.com.ua/api/v1/agency
Response
{
"data": {
"id": 11,
"name": "Star Casting",
"slug": "star-casting",
"logo_url": "https://…/storage/logos/star.webp",
"public_url": "https://star-casting.castings.com.ua",
"contacts": { "email": "hi@star.example", "phone": "+380…" },
"plan": { "name": "Basic", "expires_at": "2026-09-01T00:00:00+00:00", "active": true },
"features": { "web_intake": true, "email_intake": true, "telegram_intake": true,
"subdomain": true, "pdf_export": true },
"limits": { "castings_this_month": 4, "castings_per_month": 30,
"applications_this_month": 128, "applications_per_month": 0,
"max_photo_mb": 30, "max_video_mb": 1024 }
}
}A numeric limit of 0 means unlimited.
GET/api/v1/castings
Your published castings, newest first. Drafts are never returned.
Parameters
| status | string | open (default), closed or all. |
| gender | string | any, male or female. A casting open to "any" always matches. |
| age | integer | Keep only castings whose age range covers this age. |
| search | string | Substring match on the role name. |
| page | integer | Page number, default 1. |
| per_page | integer | Items per page, default 20, max 100. |
Request
curl -H "Authorization: Bearer $CASTINGS_TOKEN" \
"https://castings.com.ua/api/v1/castings?status=open&gender=female&per_page=10"Response
{
"data": [
{
"id": 1474,
"role_name": "Blanchet",
"excerpt": "Lead role in the feature film…",
"gender": "female",
"age_min": 20,
"age_max": 35,
"is_open": true,
"published_at": "2026-06-26T00:00:00+00:00",
"closes_at": null,
"public_url": "https://castings.com.ua/casting/1474"
}
],
"meta": { "page": 1, "per_page": 10, "total": 23, "last_page": 3 }
}GET/api/v1/castings/{id}
One casting with its full description, images and application fields.
Request
curl -H "Authorization: Bearer $CASTINGS_TOKEN" https://castings.com.ua/api/v1/castings/1474
Response
{
"data": {
"id": 1474,
"role_name": "Blanchet",
"description": "Full text of the casting call…",
"project": "A Man Escaped",
"gender": "female",
"age_min": 20,
"age_max": 35,
"is_open": true,
"closes_at": "2026-09-30T00:00:00+00:00",
"public_url": "https://castings.com.ua/casting/1474",
"images": [
{ "url": "https://…/o/photo.webp", "thumb_url": "https://…/360/photo.webp" }
],
"fields": [
{ "key": "first_name", "label": "First Name", "type": "string",
"required": true, "photos_min_count": null, "photos_max_count": null }
]
}
}GET/api/v1/castings/{id}/fields
Just the application form definition — what to render and where to submit it.
Request
curl -H "Authorization: Bearer $CASTINGS_TOKEN" https://castings.com.ua/api/v1/castings/1474/fields
Response
{
"data": {
"casting_id": 1474,
"accepts_applications": true,
"submit_url": "https://castings.com.ua/api/v1/castings/1474/applications",
"max_photo_mb": 30,
"max_video_mb": 1024,
"fields": [
{ "key": "first_name", "label": "First Name", "type": "string", "required": true,
"photos_min_count": null, "photos_max_count": null },
{ "key": "photos", "label": "Photos", "type": "file[]", "required": true,
"photos_min_count": 2, "photos_max_count": 6 }
]
}
}accepts_applications is false when the casting has closed or your plan has no web intake — hide the form in that case.
POST/api/v1/castings/{id}/applications
Submit an application. Send multipart/form-data whenever the payload carries photos or a video file; plain form fields or JSON work otherwise.
Parameters
| <field keys> | mixed | The casting's fields, exactly as returned by /fields. |
| photos[] | file | Repeat the parameter once per photo (jpeg, png, webp, gif). |
| video_demo_file | file | mp4, mov, webm, avi, mkv, ogv or 3gp. |
| update_existing | boolean | Confirm updating an application that already exists for this email. |
Request
curl -X POST \
-H "Authorization: Bearer $CASTINGS_TOKEN" \
-F "first_name=Anna" \
-F "last_name=Kovalenko" \
-F "email=anna@example.com" \
-F "age=24" \
-F "phone=+380501112233" \
-F "photos[]=@portrait.jpg" \
-F "photos[]=@fullheight.jpg" \
https://castings.com.ua/api/v1/castings/1474/applicationsResponse
HTTP/1.1 201 Created
{
"data": {
"id": 37405,
"casting": { "id": 1474, "role_name": "Blanchet" },
"status": "pending",
"source": "web",
"submitted_at": "2026-08-01T07:23:34+00:00",
"fields": {
"first_name": { "label": "First Name", "value": "Anna" },
"photos": { "label": "Photos", "value": [
{ "url": "https://…/o/a.webp", "thumb_url": "https://…/360/a.webp" }
] }
}
}
}A second submission with the same email answers 409 and the existing application id; repeat the call with update_existing=1 to refresh it in place. Applications count against your plan quota — over the quota the API answers 429.
GET/api/v1/applications
Applications received across your castings, newest first, whatever channel they arrived through (web, email, Telegram, added by a director).
Parameters
| casting_id | integer | Restrict to one casting. |
| status | string | pending, reviewed, shortlisted, approved or rejected. |
| source | string | web, email, telegram or director. |
| since | date | Only applications submitted on or after this date/time. |
| page, per_page | integer | Pagination, as above. |
Request
curl -H "Authorization: Bearer $CASTINGS_TOKEN" \
"https://castings.com.ua/api/v1/applications?casting_id=1474&status=pending&since=2026-07-01"Response
{
"data": [
{
"id": 37405,
"casting": { "id": 1474, "role_name": "Blanchet" },
"status": "pending",
"source": "web",
"submitted_at": "2026-08-01T07:23:34+00:00",
"fields": { "first_name": { "label": "First Name", "value": "Anna" } }
}
],
"meta": { "page": 1, "per_page": 20, "total": 1, "last_page": 1 }
}GET/api/v1/applications/{id}
A single application with all submitted answers and media URLs.
Request
curl -H "Authorization: Bearer $CASTINGS_TOKEN" https://castings.com.ua/api/v1/applications/37405
GET/api/v1/talents
Your agency's own talent database — the profiles your directors maintain. Talents registered on the platform are not exposed here.
Parameters
| search | string | Substring match on first or last name. |
| gender | string | male, female or other. |
| age_min, age_max | integer | Age range (profiles without a birth date are excluded). |
| height_min, height_max | integer | Height range in cm. |
| include | string | Pass contacts to include phone, email and messengers. |
| page, per_page | integer | Pagination, as above. |
Request
curl -H "Authorization: Bearer $CASTINGS_TOKEN" \
"https://castings.com.ua/api/v1/talents?gender=female&age_min=18&age_max=30&per_page=24"Response
{
"data": [
{
"id": 1441,
"display_name": "Maurice Beerblock",
"first_name": "Maurice",
"last_name": "Beerblock",
"age": 32,
"gender": "male",
"height": 184,
"thumb_url": "https://…/360/portrait.webp",
"photo_url": "https://…/o/portrait.webp"
}
],
"meta": { "page": 1, "per_page": 24, "total": 477, "last_page": 20 }
}Profiles that have a title photo are returned first, so a gallery looks right without extra sorting.
GET/api/v1/talents/{id}
A full talent card: measurements, appearance, bio, skills, photo gallery and converted videos.
Request
curl -H "Authorization: Bearer $CASTINGS_TOKEN" "https://castings.com.ua/api/v1/talents/1441?include=contacts"
Response
{
"data": {
"id": 1441,
"display_name": "Maurice Beerblock",
"age": 32,
"gender": "male",
"height": 184, "weight": 78, "bust": 96, "waist": 80, "hips": 94,
"shoe_size": "43", "clothing_size": "48",
"eye_color": "brown", "hair_color": "dark_brown",
"bio": "Theatre and film actor…",
"skills": ["horse riding", "fencing"],
"photos": [
{ "id": 90, "url": "https://…/o/a.webp", "thumb_url": "https://…/360/a.webp", "is_title": true }
],
"videos": [
{ "id": 12, "url": "https://…/cloud/demo.mp4",
"poster_url": "https://…/cloud/demo.webp", "type": "demo", "is_youtube": false }
],
"contacts": { "phone": "+380…", "email": "maurice@example.com",
"telegram": "@maurice", "viber": null, "whatsapp": null, "social_links": [] }
}
}Client libraries
Two single-file clients, no package manager and no third-party dependencies: download the file, drop it into your project, done. Both wrap every endpoint, page through lists for you, upload photos and videos, and turn HTTP errors into typed exceptions.
PHP
<?php
require __DIR__ . '/CastingsClient.php';
use Castings\CastingsClient;
$client = new CastingsClient(getenv('CASTINGS_TOKEN'), 'https://castings.com.ua/api/v1');
// Open castings
foreach ($client->castings(['status' => 'open']) as $casting) {
echo $casting['id'], ' ', $casting['role_name'], PHP_EOL;
}
// What the application form must contain
$form = $client->castingFields(1474);
foreach ($form['fields'] as $field) {
echo $field['key'], ' (', $field['type'], ')',
$field['required'] ? ' *required' : '', PHP_EOL;
}
// Submit an application, photos included
$application = $client->apply(1474, [
'first_name' => 'Anna',
'last_name' => 'Kovalenko',
'email' => 'anna@example.com',
'age' => 24,
], [
'photos' => ['/path/portrait.jpg', '/path/fullheight.jpg'],
]);
echo 'Application #', $application['id'], PHP_EOL;
// Your talent database
foreach ($client->talents(['gender' => 'female', 'per_page' => 24]) as $talent) {
echo $talent['display_name'], ' — ', $talent['thumb_url'], PHP_EOL;
}Python
import os
from castings_client import CastingsClient
client = CastingsClient(os.environ["CASTINGS_TOKEN"], "https://castings.com.ua/api/v1")
# Open castings
for casting in client.castings(status="open"):
print(casting["id"], casting["role_name"])
# What the application form must contain
form = client.casting_fields(1474)
for field in form["fields"]:
print(field["key"], field["type"], "*required" if field["required"] else "")
# Submit an application, photos included
application = client.apply(1474, {
"first_name": "Anna",
"last_name": "Kovalenko",
"email": "anna@example.com",
"age": 24,
}, files={"photos": ["portrait.jpg", "fullheight.jpg"]})
print("Application #", application["id"])
# Your talent database
for talent in client.talents(gender="female", per_page=24):
print(talent["display_name"], talent["thumb_url"])Errors as exceptions
Anything but a 2xx raises, so you can catch exactly the case you care about instead of comparing status codes.
| Status | PHP | Python | Extras |
|---|
| 401 | CastingsAuthException | CastingsAuthError | — |
| 403 | CastingsForbiddenException | CastingsForbiddenError | — |
| 404 | CastingsNotFoundException | CastingsNotFoundError | — |
| 409 | CastingsDuplicateException | CastingsDuplicateError | applicationId() / .application_id |
| 422 | CastingsValidationException | CastingsValidationError | errors(), firstError() / .errors, first_error() |
| 429 | CastingsRateLimitException | CastingsRateLimitError | retryAfter() / .retry_after |
| — | CastingsTransportException | CastingsTransportError | network failure or non-JSON reply |
# Python — refresh the application when this email already applied
try:
client.apply(casting_id, fields, files={"photos": paths})
except CastingsDuplicateError as e:
client.apply(casting_id, fields, update_existing=True)
except CastingsValidationError as e:
for field, messages in e.errors.items():
print(field, messages[0])
except CastingsRateLimitError as e:
time.sleep(e.retry_after or 60)Paging
List methods return one page that you can iterate directly (total, page, last_page, has_more come along). The all* / iter_* variants walk every page lazily.
// PHP — one page, then everything
$page = $client->talents(['per_page' => 50]);
$page->total(); // 477
$page->hasMore(); // true
foreach ($client->allTalents(['gender' => 'female']) as $talent) {
// every page, fetched on demand
}
# Python — the same two moves
page = client.talents(per_page=50)
page.total # 477
page.has_more # True
for talent in client.iter_talents(gender="female"):
...Library reference
Every method both clients expose, with its parameters, what it gives back and a runnable example. Names differ only in casing — castingFields() in PHP is casting_fields() in Python — the behaviour is identical.
Creating a client
PHPnew CastingsClient(string $token, string $baseUrl, int $timeout = 30, ?string $language = null)
PY CastingsClient(token, base_url=…, timeout=30, language=None)
| token | string | Agency API token. Required. |
| baseUrl / base_url | string | API root without a trailing slash. Defaults to https://castings.com.ua/api/v1. |
| timeout | int | Per-request timeout in seconds (default 30). Raise it for large video uploads. |
| language | string|null | Forces the response language for labels and validation messages: en, uk, ru, pl, fr, it, de, es. |
// PHP
$client = new Castings\CastingsClient(getenv('CASTINGS_TOKEN'), 'https://castings.com.ua/api/v1', timeout: 120, language: 'en');
# Python
client = CastingsClient(os.environ["CASTINGS_TOKEN"], "https://castings.com.ua/api/v1", timeout=120, language="en")Methods
PHPagency(): array
PY agency() -> dict
The agency behind the token: branding, contacts, plan features and current usage.
ReturnsThe agency record (the `data` object of GET /agency).
$agency = $client->agency();
echo $agency['name']; // "Star Casting"
echo $agency['features']['web_intake']; // true
echo $agency['limits']['applications_this_month']; // 128
# Python
agency = client.agency()
print(agency["plan"]["name"], agency["plan"]["expires_at"])
PHPping(): bool
PY ping() -> bool
Connectivity + credentials check. Swallows every API error and answers with a boolean — handy for a health check or a settings screen.
Returnstrue when the token works, false on any error.
if (! $client->ping()) {
exit('Check CASTINGS_TOKEN — the API rejected it.');
}
# Python
assert client.ping(), "Check CASTINGS_TOKEN"PHPcastings(array $filters = []): CastingsPage
PY castings(**filters) -> Page
One page of your published castings, newest first. Drafts never appear.
Parameters
| status | string | open (default), closed or all. |
| gender | string | any, male or female. A casting open to "any" always matches. |
| age | int | Keep only castings whose age range covers this age. |
| search | string | Substring match on the role name. |
| page / per_page | int | Page number (default 1) and page size (default 20, max 100). |
ReturnsA page of casting summaries: id, role_name, excerpt, gender, age_min, age_max, is_open, published_at, closes_at, public_url.
$page = $client->castings(['status' => 'open', 'gender' => 'female', 'per_page' => 10]);
foreach ($page as $casting) {
printf("#%d %s (%s)\n", $casting['id'], $casting['role_name'],
$casting['is_open'] ? 'open' : 'closed');
}
echo $page->total(), ' castings in total';
# Python
page = client.castings(status="open", gender="female", per_page=10)
for casting in page:
print(casting["id"], casting["role_name"])
print(page.total, "castings in total")PHPallCastings(array $filters = []): Generator
PY iter_castings(**filters) -> Iterator[dict]
The same filters, but every page — fetched lazily as you consume the sequence, so memory stays flat on large catalogues.
ReturnsA generator/iterator of casting summaries.
foreach ($client->allCastings(['status' => 'all']) as $casting) {
$csv->write([$casting['id'], $casting['role_name']]);
}
# Python
for casting in client.iter_castings(status="all"):
writer.writerow([casting["id"], casting["role_name"]])PHPcasting(int $id): array
PY casting(casting_id) -> dict
One casting in full: description, project, images and the application fields.
Parameters
| id | int | Casting id. Must belong to your agency and be published. |
ReturnsThe casting record, including images[] (url, thumb_url) and fields[].
RaisesCastingsNotFoundException / CastingsNotFoundError
$casting = $client->casting(1474);
echo $casting['description'];
echo $casting['images'][0]['url'] ?? 'no images';
# Python
casting = client.casting(1474)
print(casting["project"], len(casting["images"]), "images")
PHPcastingFields(int $id): array
PY casting_fields(casting_id) -> dict
The application form definition — everything needed to render a form and know where to send it.
Parameters
Returnscasting_id, accepts_applications (bool), submit_url, max_photo_mb, max_video_mb and fields[] with key, label, type, required, photos_min_count, photos_max_count.
$form = $client->castingFields(1474);
if (! $form['accepts_applications']) {
exit('This casting is closed.');
}
foreach ($form['fields'] as $field) {
printf("%-16s %-8s %s\n", $field['key'], $field['type'],
$field['required'] ? 'required' : 'optional');
}
// first_name string required
// photos file[] required
# Python
form = client.casting_fields(1474)
required = [f["key"] for f in form["fields"] if f["required"]]PHPapply(int $castingId, array $fields, array $files = [], bool $updateExisting = false): array
PY apply(casting_id, fields, files=None, update_existing=False) -> dict
Submit an application. Files are sent as multipart; a list of photos is indexed automatically so the server receives a proper array.
Parameters
| castingId / casting_id | int | Casting to apply to. |
| fields | array / dict | Field keys from castingFields(), e.g. first_name, email, age. |
| files | array / dict | Local paths: photos accepts a list, video_demo_file a single path. |
| updateExisting / update_existing | bool | Refresh a previous application from the same email instead of failing with 409. |
ReturnsThe created (or updated) application: id, casting, status, source, submitted_at and fields with the stored values — media expanded into URLs.
RaisesCastingsDuplicateException/Error (409), CastingsValidationException/Error (422), CastingsRateLimitException/Error (429)
$application = $client->apply(1474, [
'first_name' => 'Anna',
'email' => 'anna@example.com',
'age' => 24,
], [
'photos' => ['/tmp/portrait.jpg', '/tmp/fullheight.jpg'],
'video_demo_file' => '/tmp/reel.mp4',
]);
echo $application['id']; // 37405
echo $application['fields']['photos']['value'][0]['url']; // https://…/o/a.webp
# Python — handle the repeat submission
try:
client.apply(1474, fields, files={"photos": paths})
except CastingsDuplicateError:
client.apply(1474, fields, update_existing=True)PHPapplications(array $filters = []): CastingsPage
PY applications(**filters) -> Page
Applications received across your castings, newest first — whatever channel they arrived through.
Parameters
| casting_id | int | Restrict to one casting. |
| status | string | pending, reviewed, shortlisted, approved or rejected. |
| source | string | web, email, telegram or director. |
| since | string | Date or datetime; only applications submitted on or after it. |
| page / per_page | int | Pagination, as above. |
ReturnsA page of applications with their submitted answers.
$new = $client->applications(['status' => 'pending', 'since' => '2026-08-01']);
foreach ($new as $application) {
echo $application['fields']['email']['value'] ?? '—', PHP_EOL;
}
# Python
for app in client.applications(casting_id=1474, source="web"):
print(app["id"], app["submitted_at"])PHPallApplications(array $filters = []): Generator
PY iter_applications(**filters) -> Iterator[dict]
Every application matching the filters, paging for you — the shape to use when syncing into your own CRM.
ReturnsA generator/iterator of applications.
foreach ($client->allApplications(['since' => $lastSyncedAt]) as $application) {
$crm->upsert($application);
}
# Python
for application in client.iter_applications(since=last_synced_at):
crm.upsert(application)PHPapplication(int $id): array
PY application(application_id) -> dict
A single application with all answers and absolute media URLs.
Parameters
| id | int | Application id; must belong to one of your castings. |
ReturnsThe application record.
RaisesCastingsNotFoundException / CastingsNotFoundError
$application = $client->application(37405);
foreach ($application['fields'] as $key => $field) {
echo $field['label'], ': ', is_array($field['value']) ? '[media]' : $field['value'], PHP_EOL;
}
# Python
application = client.application(37405)
photos = application["fields"].get("photos", {}).get("value", [])PHPtalents(array $filters = []): CastingsPage
PY talents(**filters) -> Page
One page of your agency's own talent database. Profiles with a title photo come first, so a gallery looks right without extra sorting.
Parameters
| search | string | Substring match on first or last name. |
| gender | string | male, female or other. |
| age_min / age_max | int | Age range; profiles without a birth date are excluded. |
| height_min / height_max | int | Height range in cm. |
| include | string | Pass "contacts" to include phone, email and messengers. |
| page / per_page | int | Pagination, as above. |
ReturnsA page of talent summaries: id, display_name, first_name, last_name, age, gender, height, thumb_url, photo_url.
$models = $client->talents(['gender' => 'female', 'age_min' => 18, 'per_page' => 24]);
foreach ($models as $talent) {
printf('<img src="%s" alt="%s">', $talent['thumb_url'], $talent['display_name']);
}
# Python
for talent in client.talents(height_min=175, per_page=24):
print(talent["display_name"], talent["height"])PHPallTalents(array $filters = []): Generator
PY iter_talents(**filters) -> Iterator[dict]
Your whole talent base, page by page — for nightly exports or a static site build.
ReturnsA generator/iterator of talent summaries.
foreach ($client->allTalents() as $talent) {
$index->add($talent['id'], $talent['display_name']);
}
# Python
everyone = list(client.iter_talents())PHPtalent(int $id, bool $withContacts = false): array
PY talent(talent_id, with_contacts=False) -> dict
A full talent card. Contacts stay out of the response unless you ask for them — leave them off on a public page.
Parameters
| id | int | Talent profile id from your own database. |
| withContacts / with_contacts | bool | Include phone, email, messengers and social links. |
ReturnsMeasurements, appearance, bio, skills, experience, education, photos[] and videos[] (converted uploads and YouTube links).
RaisesCastingsNotFoundException / CastingsNotFoundError
$talent = $client->talent(1441, withContacts: true);
echo $talent['display_name'], ', ', $talent['age'];
echo $talent['contacts']['phone'];
echo count($talent['photos']), ' photos, ', count($talent['videos']), ' videos';
# Python — public page, no contacts
talent = client.talent(1441)
gallery = [p["thumb_url"] for p in talent["photos"]]
The page object
Every list method returns one page. In PHP it is a CastingsPage you can foreach and count(); in Python it is a Page, a real list subclass — indexing, slicing and len() all work.
| PHP | Python | Meaning |
|---|
| foreach ($page as $item) | for item in page | The records on this page. |
| count($page) | len(page) | How many records this page holds. |
| $page->items | page[0], page[:5] | Direct access to the records. |
| $page->total() | page.total | Records across all pages. |
| $page->page() | page.page | Current page number. |
| $page->lastPage() | page.last_page | Number of the last page. |
| $page->hasMore() | page.has_more | Whether another page follows. |
| $page->meta | page.meta | The raw meta block from the API. |