Developer documentation

REST API

Embed your castings, application forms and talent database into your own website. The API is read-mostly, JSON-only and scoped to a single agency: the token you send decides whose data you get back.

Base URL
https://castings.com.ua/api/v1
Authentication
Authorization: Bearer <token>

Authentication

Every request carries your agency API token. Generate it in Agency → Settings → API; only the agency owner can issue or revoke it. Regenerating invalidates the previous token immediately.

curl -H "Authorization: Bearer $CASTINGS_TOKEN" \
     -H "Accept: application/json" \
     https://castings.com.ua/api/v1/agency

If your HTTP client cannot set an Authorization header, send the token as X-Agency-Token instead.

The token is a secret with read access to your castings, applications and talent database. Keep it on your server and proxy browser requests through your own backend — never ship it in front-end JavaScript.

Conventions

Response shape

A single record comes back as data; a list adds a meta block with the pagination state.

{
  "data": [ ... ],
  "meta": { "page": 1, "per_page": 20, "total": 57, "last_page": 3 }
}

Pagination

List endpoints accept page (default 1) and per_page (default 20, maximum 100).

Errors

Errors always answer with a message, and validation failures add a per-field errors map.

{
  "message": "The email field must be a valid email address.",
  "errors": {
    "email": ["The email field must be a valid email address."]
  }
}
StatusMeaning
200OK.
201Application created.
401Missing or invalid API token.
403Your plan does not include this feature (e.g. web intake).
404Unknown endpoint, or the record does not belong to your agency.
409This applicant already applied — resend with update_existing=1.
422Validation failed; see the errors map.
429Rate limit or monthly application quota reached.

Rate limit

300 requests per minute per token. Over the limit the API answers 429 with a Retry-After header telling you how long to wait.

Language

Human-readable labels (field labels, validation messages) follow the request locale. Add ?lang=en — supported: en, uk, ru, pl, fr, it, de, es — or send an Accept-Language header. Machine values (field keys, statuses, gender) are always English and stable.

CORS

Cross-origin requests are allowed from any origin, so a browser can call the API directly — but that would expose your token, so prefer a server-side call.

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

statusstringopen (default), closed or all.
genderstringany, male or female. A casting open to "any" always matches.
ageintegerKeep only castings whose age range covers this age.
searchstringSubstring match on the role name.
pageintegerPage number, default 1.
per_pageintegerItems 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>mixedThe casting's fields, exactly as returned by /fields.
photos[]fileRepeat the parameter once per photo (jpeg, png, webp, gif).
video_demo_filefilemp4, mov, webm, avi, mkv, ogv or 3gp.
update_existingbooleanConfirm 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/applications

Response

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_idintegerRestrict to one casting.
statusstringpending, reviewed, shortlisted, approved or rejected.
sourcestringweb, email, telegram or director.
sincedateOnly applications submitted on or after this date/time.
page, per_pageintegerPagination, 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

searchstringSubstring match on first or last name.
genderstringmale, female or other.
age_min, age_maxintegerAge range (profiles without a birth date are excluded).
height_min, height_maxintegerHeight range in cm.
includestringPass contacts to include phone, email and messengers.
page, per_pageintegerPagination, 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.

StatusPHPPythonExtras
401CastingsAuthExceptionCastingsAuthError—
403CastingsForbiddenExceptionCastingsForbiddenError—
404CastingsNotFoundExceptionCastingsNotFoundError—
409CastingsDuplicateExceptionCastingsDuplicateErrorapplicationId() / .application_id
422CastingsValidationExceptionCastingsValidationErrorerrors(), firstError() / .errors, first_error()
429CastingsRateLimitExceptionCastingsRateLimitErrorretryAfter() / .retry_after
—CastingsTransportExceptionCastingsTransportErrornetwork 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)
tokenstringAgency API token. Required.
baseUrl / base_urlstringAPI root without a trailing slash. Defaults to https://castings.com.ua/api/v1.
timeoutintPer-request timeout in seconds (default 30). Raise it for large video uploads.
languagestring|nullForces 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

statusstringopen (default), closed or all.
genderstringany, male or female. A casting open to "any" always matches.
ageintKeep only castings whose age range covers this age.
searchstringSubstring match on the role name.
page / per_pageintPage 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

idintCasting 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

idintCasting id.

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_idintCasting to apply to.
fieldsarray / dictField keys from castingFields(), e.g. first_name, email, age.
filesarray / dictLocal paths: photos accepts a list, video_demo_file a single path.
updateExisting / update_existingboolRefresh 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_idintRestrict to one casting.
statusstringpending, reviewed, shortlisted, approved or rejected.
sourcestringweb, email, telegram or director.
sincestringDate or datetime; only applications submitted on or after it.
page / per_pageintPagination, 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

idintApplication 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

searchstringSubstring match on first or last name.
genderstringmale, female or other.
age_min / age_maxintAge range; profiles without a birth date are excluded.
height_min / height_maxintHeight range in cm.
includestringPass "contacts" to include phone, email and messengers.
page / per_pageintPagination, 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

idintTalent profile id from your own database.
withContacts / with_contactsboolInclude 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.

PHPPythonMeaning
foreach ($page as $item)for item in pageThe records on this page.
count($page)len(page)How many records this page holds.
$page->itemspage[0], page[:5]Direct access to the records.
$page->total()page.totalRecords across all pages.
$page->page()page.pageCurrent page number.
$page->lastPage()page.last_pageNumber of the last page.
$page->hasMore()page.has_moreWhether another page follows.
$page->metapage.metaThe raw meta block from the API.

Recipes

Render an application form

Each casting defines its own fields, so build the form from GET /castings/{id}/fields instead of hard-coding inputs. The type tells you what to render: string, integer, email, url, file and file[].

Proxy a submission from your site (PHP)

Post your visitor's form to your own endpoint and forward it with the token attached, so the secret never reaches the browser.

<?php
// apply.php on your own server — the token never leaves the backend.
$token   = getenv('CASTINGS_TOKEN');
$casting = 1474;

$payload = [
    'first_name' => $_POST['first_name'] ?? '',
    'last_name'  => $_POST['last_name'] ?? '',
    'email'      => $_POST['email'] ?? '',
];

// Attach uploaded photos, if any.
foreach ($_FILES['photos']['tmp_name'] ?? [] as $i => $tmp) {
    $payload['photos[]'] = new CURLFile(
        $tmp,
        $_FILES['photos']['type'][$i],
        $_FILES['photos']['name'][$i]
    );
}

$ch = curl_init('https://castings.com.ua/api/v1/castings/' . $casting . '/applications');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => ['Authorization: Bearer ' . $token, 'Accept: application/json'],
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_RETURNTRANSFER => true,
]);

$body   = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

http_response_code($status);
header('Content-Type: application/json');
echo $body;

Show your models gallery

GET /talents returns your agency's own talent database. Contact details are withheld unless you pass include=contacts — leave them out on a public page.

curl -H "Authorization: Bearer $CASTINGS_TOKEN" \
     "https://castings.com.ua/api/v1/talents?per_page=24" \
  | jq '.data[] | {name: .display_name, photo: .thumb_url}'

Application field keys

The full set a casting can ask for. Which ones apply is decided per casting — always read them from GET /castings/{id}/fields.

KeyTypeNotes
first_namestringGiven name.
last_namestringFamily name.
emailemailAlso used to detect a repeat application.
phonestringAny format, max 255 characters.
ageinteger1–999.
heightintegerCentimetres.
weightintegerKilograms.
bustintegerCentimetres.
waistintegerCentimetres.
hipsintegerCentimetres.
telegramstringUsername or link.
viberstringPhone or link.
whatsappstringPhone or link.
social_linkurlInstagram, portfolio, …
video_demo_linkurlYouTube/Vimeo link to a showreel.
video_demo_filefileUploaded showreel, up to the agency video limit.
photosfile[]Repeat photos[] per file; min/max counts come from the casting.

Versioning

The version lives in the path (/api/v1). New fields may be added to responses without notice, so parse defensively; anything that removes or renames a field ships as a new version.