# Send call sheets into Callsheet Pro: the developer API

Your own tools can push a shoot day straight into a Callsheet Pro account. No PDF, no import step: send the data, and the day is on the phone within seconds.

This is the plain-text version of https://developer.callsheetpro.app.

## Three ways in. Pick yours.

- **A PDF on your phone:** tap Share on the PDF and pick Callsheet Pro. Built into the iOS and Android apps. No key, no API.
- **A call sheet by email:** forward it to your personal address from Settings, Email Forwarding. The attachment is read automatically.
- **A tool that holds the data:** scheduling software, a production office system, a spreadsheet script, a Shortcut. That is what this page is for.

## Get your key in 3 steps

Every Callsheet Pro account can make a key, Free or Pro. You never need to email anyone.

1. Open Callsheet Pro and go to Settings (the gear at the top right on the phone; Settings in the sidebar on the web app).
2. Scroll to **API key** and tap **Create API key**. It sits right under Email Forwarding.
3. Tap the copy button and paste the key where your tool wants it. The key starts with `csp_`. For safety it is shown only this once; if you lose it, tap **Make a new key**.

The key writes into your own account only. Treat it like a password: anyone holding it can add and change call sheet days in your account, and nothing else.

## Try it in one minute

Put your key in place of `csp_YOUR_KEY` and run:

```bash
curl -X POST "https://us-central1-callsheet-pro.cloudfunctions.net/api/v1/days" \
  -H "Authorization: Bearer csp_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "externalId": "my-first-test",
    "day": {
      "productionTitle": "API test",
      "dateISO": "2026-10-20",
      "generalCallTime": "07:00",
      "locations": [{ "name": "Studio A", "address": "Kleine Weg 1, Amsterdam", "callTime": "07:00" }]
    }
  }'
```

You get back `{"result":"created", ...}`. Open Callsheet Pro: there is a production called **API test** with one day.

Now change the call time to 08:00 and send the same body again. The answer says `"result":"updated"`, and the same day in the app now reads 08:00 instead of a second copy appearing. The `externalId` is what makes that happen: same id, same day.

Done testing? In Callsheet Pro, open All Days and swipe the day to the left to delete it (or tap Edit list, then the trash icon next to it). On the web app the trash icon sits on each row of All Days; tap it twice.

- Got a 401? The key is wrong or was replaced. Make a new one in Settings, API key, and paste it again.
- Got a 422? The `issues` list in the answer names exactly which field to fix.

## Build an iOS Shortcut

A Shortcut turns any text on your iPhone into a shoot day: a call sheet pasted into a WhatsApp message, the body of an email, a note. Select the text, tap Share, pick the Shortcut, and the day appears in Callsheet Pro. Each send reads the text with the same parser as a PDF upload and spends one upload from your monthly allowance.

Have the call sheet as a PDF? You do not need a Shortcut: tap Share on the PDF and pick Callsheet Pro.

1. Open the Shortcuts app and tap the plus at the top right. Tap the name at the top, call it **Send to Callsheet Pro**.
2. Add the action **Get Contents of URL**: tap **Search Actions** at the bottom, type the name, tap it. Then tap the blue **URL** word and paste this: `https://us-central1-callsheet-pro.cloudfunctions.net/api/v1/days`
3. Expand the action (tap the small arrow) and fill it in:
   - Method: POST
   - Headers: add `Authorization` with the value `Bearer csp_YOUR_KEY` (the word Bearer, a space, then your key), and add `Content-Type` with the value `application/json`
   - Request Body: JSON. Tap **Add new field**, choose **Text**, type `input` as the key, then tap the value, tap **Select Variable** and pick **Shortcut Input**
4. Set up the input. Picking Shortcut Input adds a **Receive ... from ...** step at the top. Tap **Continue** under **If there's no input** and choose **Get Clipboard**, so a copied call sheet works too. To get the Shortcut into the Share menu, tap the **i** (Details) at the bottom and switch on **Show in Share Sheet**.
5. Add the action **Show Content** (on older iOS it is called **Show Result**). It shows the answer: `created` on success, or the message that says what went wrong.
6. Try it: copy a call sheet text and tap the play button at the bottom right. The first time, iOS asks whether the Shortcut may send text to `us-central1-callsheet-pro.cloudfunctions.net`: tap **Always Allow**. After a few seconds the answer appears, and the day is in Callsheet Pro. From then on: select text in a message or email, tap Share, pick **Send to Callsheet Pro**.

One send per 15 seconds, and each send spends one upload from your monthly allowance, like scanning a PDF in the app. If the text is not a call sheet at all, the answer says `PARSE_FAILED` and nothing is added.

## Endpoint

```
POST https://us-central1-callsheet-pro.cloudfunctions.net/api/v1/days
Authorization: Bearer csp_YOUR_KEY
Content-Type: application/json
```

One call sends one shoot day. The response tells you whether it was created or updated. This is the only route; there is no way to read, list or delete days through the API.

## Structured mode (recommended)

Send the day as JSON in a `day` object. Instant, exact, and free of upload credits.

```json
{
  "externalId": "your-stable-id-for-this-sheet",
  "day": {
    "productionTitle": "Night Shift",
    "dateISO": "2026-07-14",
    "generalCallTime": "07:00",
    "wrapTime": "19:00",
    "locations": [
      { "name": "Studio A", "address": "Kleine Weg 1, Amsterdam", "callTime": "07:00", "parking": "Lot B" }
    ],
    "crew": [
      { "role": "Gaffer", "name": "A. Person", "phone": "+31600000000", "callTime": "07:00" }
    ],
    "schedule": [
      { "time": "08:00", "description": "First setup", "teams": [] }
    ]
  }
}
```

Required: `productionTitle`, and a date (`dateISO` as `YYYY-MM-DD`, preferred, or `date` as free text). Everything else is optional.

### Updates (upsert by externalId)

`externalId` is your tool's own stable id for the sheet (max 128 characters). Re-send the same `externalId` and the existing day in the app is updated in place instead of duplicated. This is how live changes work: when the sheet changes in your tool, just send it again.

- Always send the full day, every time. Fields you omit are cleared. An omitted `crew` list empties the crew section.
- The API owns the call sheet content (title, times, locations, crew, schedule, scenes, transport, cast, and so on). An update overwrites those with what you send.
- The user's personal data in the app always survives updates: private notes, My Wrap, attached documents, pinned notes, custom crew order, and how the day is grouped into a production.

Without `externalId`, every call creates a new day.

## Loose mode

If you cannot produce our JSON, send any text instead and our parser turns it into a structured day. Costs the account owner one upload credit per call (same as scanning a PDF in the app) and is limited to one call per 15 seconds.

```json
{
  "externalId": "your-stable-id-for-this-sheet",
  "input": "CALL SHEET - Night Shift - Tuesday, July 14, 2026\nGeneral call 07:00 ... (any format, max 100,000 characters)"
}
```

Send exactly one of `day` or `input`, never both. The API accepts no files: a PDF has to become text before it is sent, or go through Share in the app.

## Field reference (day object)

| Field | Type | Notes |
|---|---|---|
| productionTitle | string | Required. Days with the same title group into one production in the app. |
| seriesName | string | Optional series name. |
| episodeInfo | string | For example "S2E05". |
| dayNumber | number | Shoot day number. |
| dateISO | string | `YYYY-MM-DD`. Preferred date field. |
| date | string | Human-readable date, fallback when dateISO is absent. |
| country | string | ISO country code of the shoot, used for phone number formatting. |
| generalCallTime | string | For example "07:00". |
| wrapTime | string | Expected wrap. |
| weather | string | Free text. |
| sunrise / sunset | string | For example "05:42". |
| warnings | string[] | Safety or general warnings shown prominently. |
| locations | Location[] | See below. |
| crew | CrewMember[] | See below. |
| schedule | ScheduleItem[] | See below. |
| scenes | Scene[] | See below. |
| transport | TransportRow[] | See below. |
| cast | CastMember[] | See below. |
| emergency | object | `hospitalName`, `hospitalAddress`, `hospitalPhone`, `firstAid`, `counselor`. |
| invoiceEmail / invoiceCompany / invoiceDetails / projectNumber | string | Billing info for crew invoices. |
| gearAndSettings | object | `gear`: string[], `settings`: [{ label, value }], `warning`: string, `groups`: [{ title, gear, settings }]. Send `groups` when the technical block is printed under two or more subheadings (broadcast/OB sheets: TECHNIEK, AUDIO, EVS, VIDEOSIGNALEN); `gear` and `settings` are then derived from it and anything you put in them is ignored. A single group is folded into the flat arrays. |

Object shapes:

- Location: `name`, `address`, `callTime`, `parking`, `contactName`, `contactPhone`
- CrewMember: `role`, `name`, `phone` (E.164 preferred), `email`, `callTime`, `callLocation`, `team`
- ScheduleItem: `time`, `endTime`, `description`, `teams` (string[]), `sceneNumber`
- Scene: `number`, `description`, `cast` (string[]), `scheduledTime`, `locationName` (must match a locations[].name to link)
- TransportRow: `kind`, `label`, `driver`, `driverPhone`, `passengers` (string[]), `time`, `from`, `to`, `ref`
- CastMember: `role`, `name`, `phone`, `pickupTime`, `makeupTime`, `wardrobeTime`, `callTime`, `onSetTime`, `note`

All string fields accept null or can be omitted; they default to empty.

## Responses and errors

Success:

```json
{ "result": "created", "dayId": "...", "dayIds": ["..."], "externalId": "...", "warnings": [] }
```

`201` for created, `200` for updated. `warnings` lists non-fatal problems (for example a scene pointing at an unknown location). Fix them when convenient; the day was saved.

Errors produced by the API look like:

```json
{ "error": { "code": "INVALID_DAY", "message": "how to fix it", "issues": ["field: problem"], "hint": "https://developer.callsheetpro.app" } }
```

An HTTP platform or proxy can reject malformed HTTP or malformed `application/json` before the handler runs; in that case the response may be a plain `400`. Check the HTTP status and content type before decoding the JSON error envelope.

| HTTP | code | Meaning and fix |
|---|---|---|
| 400 | INVALID_JSON / INVALID_BODY | Malformed body. Send a JSON object with exactly one of `day` or `input`. |
| 401 | MISSING_AUTH / INVALID_API_KEY | Key absent, wrong, or replaced. Make a new one in Callsheet Pro under Settings, API key, and update the tool. |
| 404 | NOT_FOUND | The only route is `POST /v1/days`. |
| 405 | METHOD_NOT_ALLOWED | Send a POST. |
| 413 | PAYLOAD_TOO_LARGE | Body over 1 MB or input over 100,000 characters. Send one day per request. |
| 422 | INVALID_DAY | Day rejected; the `issues` array lists exactly what to fix. |
| 422 | PARSE_FAILED | Loose mode could not produce a usable day. Check the optional `issues` array, improve the input, or use structured mode. |
| 422 | RESPONSE_TRUNCATED | The call sheet was too long to finish parsing in one response. Split it into one day per request; no credit was charged. |
| 422 | FILE_TOO_LARGE | Input too large for the parser. Send less text, or split it per day; no credit was charged. |
| 422 | PDF_PASSWORD_PROTECTED | The PDF is password protected. Remove the password first; no credit was charged. |
| 422 | IMAGE_TOO_LARGE | The image is larger than the parser accepts. Downscale it and retry; no credit was charged. |
| 422 | PDF_NOT_VALID | Not a readable PDF. Re-export it and retry; no credit was charged. |
| 422 | PDF_TOO_MANY_PAGES | The PDF has more pages than the parser accepts. Send the call sheet pages only; no credit was charged. |
| 429 | RATE_LIMITED | Too fast. Send requests one after another and retry after a short wait. |
| 429 | DAILY_LIMIT_REACHED | 500 requests today used. Retry after midnight UTC. |
| 429 | PARSE_LIMIT_REACHED | Monthly upload allowance used (loose mode). Switch to structured mode, which is free, or wait for the monthly reset. |
| 503 | UPSTREAM_BUSY | Parser busy (loose mode only). Retry in a minute; no credit was charged. |
| 500 | INTERNAL | Our fault. Retry once; if it persists, email support@earlystudios.nl. |

## Limits

- 500 requests per account per day, counted in UTC.
- One request at a time: leave at least 300 ms between calls, and never send in parallel.
- 1 MB per request, and `input` at most 100,000 characters.
- Loose mode: one call per 15 seconds, one upload credit per call from the account's monthly allowance (3 on Free, 100 on Pro). Structured mode spends no credits.
- One key per account. Making a new one replaces the old one.

## Your key: safety and replacing it

- Single-account backend: store the key in a server-side secret or environment variable.
- Multi-user product: collect and encrypt one key per Callsheet Pro user in your backend, then use that user's key for their exports.
- User-owned desktop or mobile app: use the operating system's secure credential storage.
- Never log, commit, or embed a key in a public website bundle. Treat it like a password: anyone holding it can write call sheet days into that account.

Lost or leaked? In Callsheet Pro, Settings, API key, tap **Make a new key**. The old key stops working the same second; give the new one to the tools that need it. We store only a hash of your key, so nobody, including us, can read it back: that is why it is shown once.

Deleting your account deletes the key with it.

## Prompt for coding tools

Building the integration with a coding tool (Claude Code, Codex, Cursor, or similar)? Paste this prompt and let it work.

```
Add an "Export to Callsheet Pro" feature to this project.

API: POST https://us-central1-callsheet-pro.cloudfunctions.net/api/v1/days
Auth: header "Authorization: Bearer <key>". The key starts with csp_. For a
single-account server integration, load it from an environment variable (for
example CALLSHEET_PRO_API_KEY). For a multi-user product, collect one key per
Callsheet Pro user and store it encrypted in your backend. Never hardcode,
log, commit, or ship keys in a public frontend bundle. Every Callsheet Pro
user makes their own key in the app: Settings, API key, Create API key.

Body: JSON object { "externalId"?: string, "day": object }.
- externalId: strongly recommended. Use this project's stable id for the
  call sheet (max 128 chars).
  Re-sending the same externalId updates the day in the app instead of
  duplicating it, so exports are safe to repeat after every change.
- day: the call sheet as JSON. Required: productionTitle, and dateISO
  ("YYYY-MM-DD") or date (free text). Optional fields: seriesName,
  episodeInfo, dayNumber, country, generalCallTime, wrapTime, weather,
  sunrise, sunset, warnings (string[]),
  locations [{name, address, callTime, parking, contactName, contactPhone}],
  crew [{role, name, phone, email, callTime, callLocation, team}],
  schedule [{time, endTime, description, teams (string[]), sceneNumber}],
  scenes [{number, description, cast (string[]), scheduledTime, locationName}],
  transport [{kind, label, driver, driverPhone, passengers, time, from, to, ref}],
  cast [{role, name, phone, pickupTime, makeupTime, wardrobeTime, callTime,
  onSetTime, note}], emergency {hospitalName, hospitalAddress, hospitalPhone,
  firstAid, counselor}, invoiceEmail, invoiceCompany, invoiceDetails,
  projectNumber.
  Every scene.locationName should exactly match a locations[].name.
  Always send the FULL day on every export: omitted lists are cleared.

Alternative when structured mapping is not feasible: send
{ "externalId": string, "input": "<the call sheet as plain text>" } instead of
"day". That costs the user 1 upload credit per call and allows one call per
15 seconds, so prefer "day".

Responses: 201 created / 200 updated, body { result, dayId, warnings }.
Surface warnings to the user. API-handler errors are
{ error: { code, message, issues } }; the message says how to fix. Handle:
401 invalid key (tell the user to make a new key in Callsheet Pro under
Settings, API key, and paste it again), 422 INVALID_DAY (fix the listed
issues), 429 (back off and retry sequentially, never parallel), 503 (retry
once after a minute).

If this project has a user interface, use the official "Export to Callsheet
Pro" button for the export action: ready-made HTML snippet and image assets
at https://developer.callsheetpro.app/#button

Verify the integration by sending a minimal day and confirming a 201 created
or 200 updated response. Then tell the user to open the Callsheet Pro app:
the day appears there automatically within seconds, grouped under its
production title.

Full docs: https://developer.callsheetpro.app (plain text:
https://developer.callsheetpro.app/developers.md)
```

## The official button

Put this in your tool's interface on the action that sends a sheet to Callsheet Pro. Use it as-is; do not change the colors or the wording.

- Dark: [SVG](https://developer.callsheetpro.app/assets/export-button-dark.svg), [PNG](https://developer.callsheetpro.app/assets/export-button-dark.png), [PNG 2x](https://developer.callsheetpro.app/assets/export-button-dark@2x.png)
- Yellow: [SVG](https://developer.callsheetpro.app/assets/export-button-yellow.svg), [PNG](https://developer.callsheetpro.app/assets/export-button-yellow.png), [PNG 2x](https://developer.callsheetpro.app/assets/export-button-yellow@2x.png)

Or render it natively with plain HTML (no image, no font files needed):

```html
<!-- Export to Callsheet Pro button (dark). Attach your export handler. -->
<button type="button" style="display:inline-flex; align-items:center; gap:8px;
  height:44px; padding:0 20px; background:#000; border:1px solid #46484d;
  border-radius:12px; cursor:pointer;
  font-family:Inter, system-ui, -apple-system, 'Segoe UI', sans-serif;">
  <span style="font-size:14px; color:#aaabb0;">Export to</span>
  <span style="font-size:15px; font-weight:800; letter-spacing:-0.02em;
    color:#f6f6fc;">CALLSHEET<span style="color:#facc15;">PRO</span></span>
</button>

<!-- Yellow variant: swap the three style blocks for -->
<!-- button: background:#facc15; border:none;                  -->
<!-- "Export to" span: color:rgba(0,0,0,0.78);                 -->
<!-- wordmark span: color:#000; (and drop the inner PRO span)  -->
```

Questions, or a use case the API does not cover yet? Email support@earlystudios.nl.
