Whalesync API reference for Live Export: create exports, map source tables, save, schedule, trigger runs, and monitor them.
A live export copies tables from a source app into tables it creates in a destination app. It runs one way, on demand or on a schedule, and never writes to the source. Everything for Live Export lives under /live-export/. Live Export describes the feature itself, and Live Export vs. sync compares it with a sync.
Paths are relative to https://api.whalesync.com/v1, as on the API reference. Authentication, scopes, error responses, pending actions, pagination, and rate limits work as described there. This page covers what is specific to Live Export.
Live Export is available on plans that include it. Reads never explain a missing feature: when Live Export isn't available to the account, GET /live-export/live-exports returns an empty list and any lex_… id returns 404.
GET /live-export/connectors returns [] when the feature isn't available. When the account is on a plan that doesn't include a connector, it lists the connector with available: false and required_plan.
Writes (create, update, delete, mappings PUT, save, schedule PATCH, trigger, cancel) return 403 live_export_unavailable, naming the plan when the plan is what's missing.
Every Live Export endpoint refuses API keys limited to one sync with 403 sync_restricted_key. See Keys limited to one sync.
Live exports are lex_… and runs are run_…. Both are opaque; only the prefix is guaranteed. Run ids contain a . (for example run_5JjKXzKZqY.wMpkUYLMV0) because a run id also identifies its export.
Source table ids can contain commas (for example transcripts,1299375510811165803). Pass every id back exactly as you received it.
Never saved. No runnable export exists yet. Connect both sides, map, and save.
active
Saved, and the schedule is on. Runs fire on the cadence.
manual_only
Saved, but the schedule is off or was never created. The export only runs when triggered. Nothing is stopped or broken.
There are no pause or activate endpoints. PATCH …/schedule {"enabled": …} turns the schedule on and off, and trigger works in every saved state.
unsaved_changes: true means the mappings were edited over the API after the last save. A run still uses the last-saved mappings. Saving clears it, whether the save is made over the API or in the app.
GET /live-export/connectors is a separate registry from GET /sync/connectors, because the same app can authenticate differently for each product. The type slugs share one namespace, so hubspot means HubSpot everywhere. The response is {"data": […]}, sorted by name and not paginated.
roles says whether a connector can be a source, a destination, or both.
auth.method is oauth or api_key. For api_key, fields lists the credential ids to send under auth. Unlike the sync registry, Live Export credential fields have no help or options.
Which connectors appear depends on the account. Read the list rather than assuming a fixed set.
The body is name (optional), source, and destination. Each side takes a connector and, for an api_key connector, optionally auth. name defaults to " to ".
Inline auth is validated during the create. If the app rejects it, nothing is kept and the call returns 400 connection_failed with the app's message.
Leave auth out to defer a side to a person. The side comes back null with a user_authorization pending action. This works for any side. OAuth sides can only be connected this way, and sending auth for one is 400 oauth_auth_not_allowed.
Pending actions have the same shape as on a sync, except that side is source or destination. The URL opens https://app.whalesync.com/exports/…/connect/{source|destination}?connector=…. On the destination, the page also asks the person to pick where created tables will live.
Relay instruction and url to your user word for word, then poll GET /live-export/live-exports/{id} until the side is non-null. A call that needs a side that isn't connected yet returns 409 auth_required (requires_action) with the same object as required_action.
PATCH /live-export/live-exports/{id} {"source": {"auth": {…}}} finishes an API-key side that was created without credentials. A side that is already connected returns 409 side_already_connected. Reconnecting a side is done in the app.
location is where the export creates its tables: an Airtable base, a Notion parent page, a Supabase or Postgres schema, or a Google Sheets spreadsheet. The person usually picks it on the connect page. If it is still unset, list the options with GET …/destination/locations (add ?search= when has_more is true), then PATCH {"destination": {"location": "<id>"}}.
The locations response is {"data": [{"id", "name"}], "has_more": bool}. With ?search=, has_more is always false.
A location that isn't in the list returns 400 invalid_location. So does sending location on the source.
On the export, destination.location is {id, name} or null. null means no pick was recorded and nothing has been created yet, or the destination uses its own default. It does not mean the destination is unconnected; an unconnected destination is destination: null.
GET /live-export/live-exports/{id}/source/tables/{table_id}/fields
source/tables is read live from the source app, so it can be slow. It isn't paginated.
fields returns each exportable field with the name and type it will get in the destination. Whalesync plans the types; you can't choose them. When the source and destination are the same kind of database, type is the exact column type (text[], numeric(12,2)). Otherwise it is one of text, longText, number, boolean, date, select, multiSelect, url, email, phone, currency, or json, with list appended for multi-value fields.
suggested_primary marks the suggested title field.
source_record_id marks the field that lets reruns update records instead of duplicating them. It is always included in created tables, even if you don't map it.
Link (relationship) fields aren't offered over the API. Set those up in the app.
The export always creates its destination tables and fields. You give names, never types. Mapping onto an existing destination table or column isn't supported over the API (400 existing_field_not_supported); use the app.
Mark at most one field per table primary (400 multiple_primary_fields). If the destination needs a primary field and none is marked, the suggested one is used.
Full-replace semantics: anything omitted is unmapped. Edit by fetching the document, modifying it, and putting it back.
The response carries a revision, also sent as the ETag header. Send it as If-Match on the PUT; a stale value returns 412 revision_mismatch. The mappings PUT does not accept Idempotency-Key.
A PUT only edits the working copy. Nothing is created in the destination until you save.
Once a table has been created in the destination, GET shows it and its fields as opaque string ids. Put those back unchanged:
Here gong_record_id is the source_record_id field, added automatically. Changing the fields of a table that has already been created returns 400 applied_table_not_editable. Remove the whole table from the document, or edit it in the app.
validate returns {"valid": bool, "issues": [{"code", "message", "path"}]}, where path is tables[N] or null. This differs from sync validation: there is no severity or side, and path isn't a JSON pointer. issues[].code uses the same codes as the 400s a PUT would return, plus auth_required when a side isn't connected and internal when validation itself failed.
POST …/save creates the mapped tables and fields in the destination, then makes the new mappings the ones runs use. It runs in the background and returns 202 with the save object. Poll GET …/save, passing ?wait=<seconds> (at most 50) to hold the request until the save finishes or the wait runs out.
errors is [{name, error}], one entry per table or field that failed.
Only one save runs at a time. If a save fails, fix the problem and POST again; anything already created is skipped. succeeded is only reported once no edits remain unsaved, so treat it, together with unsaved_changes: false on the export, as the confirmation. Saves report succeeded while runs report completed.
A save with no tables mapped returns 400 empty_mappings. GET …/save on an export that was never saved returns 404.
GET returns {enabled, cadence, cron, timezone, next_run_at, last_triggered_at, allowed_cadences}.
cadence is every_10_minutes, every_30_minutes, hourly, daily, or custom. custom is read-only: it is a schedule set in the app that doesn't match the others, and it survives a PATCH that changes only enabled or timezone.
cron is read-only.
allowed_cadences depends on the plan: daily only, daily and hourly, or all four. A cadence outside it returns 403 plan_required.
The first PATCH creates the schedule. A new schedule starts off, at daily. Fields you leave out keep their current values. timezone is an IANA name such as America/New_York.
Before the first save, GET and PATCH return 409 not_provisioned. After saving but before any PATCH, GET returns 404 and the export's schedule is null.
trigger starts a run regardless of the schedule and returns 201 with the run. The body is optional.
One run at a time. Triggering while a run is in progress returns 409 run_in_progress, whose message names that run.
With unsaved edits, trigger returns 409 unsaved_changes, because the run would use the last-saved mappings. Save first, or send {"force": true} only when the person explicitly wants the last-saved version. Before the first save it returns 409 not_provisioned.
cancel stops a run in progress. A run that has already finished returns 409 run_not_active.
Here last_run is the latest finished run, unlike last_run on the export, which may still be running. next_run_at is null when the schedule is off.
GET /live-export/runs?live_export=lex_… lists runs, newest first. History covers recent runs only, so has_more: false means the end of what is kept, not every run ever. GET /live-export/runs/{id} adds steps:
Run status is pending, running, completed, failed, or cancelled. The wire value is spelled cancelled, with two l's.
Run trigger is manual (from the API or a person) or schedule.
Step action is prepare, pull, map, write_plan, or write. write is the step that changes the destination. Step name is a display label; branch on action.
Step status is pending, running, completed, failed, or skipped.
summary describes what the run is doing now, or how it ended.
Live exports have runs, not issues. A failed run's error, and the failed step's error, are the diagnostics.
The run list returns the same object without live_export_id, steps, and url.
DELETE /live-export/live-exports/{id} removes the export and its schedule and returns {"id": "lex_…", "deleted": true}. Tables already created in the destination are left in place.
These are done in the app: mapping onto existing destination tables or columns, editing the fields of tables already created, link fields, reconnecting a connected side, and setting a custom cron schedule.