Agent quickstart
Guide for AI agents building syncs and Live Exports with the Whalesync API: get a key, relay human steps, write mappings, and monitor the result.
This page is for AI agents building syncs and Live Exports with the Whalesync API on someone's behalf, and for the people setting them up. Full endpoint documentation is in the API reference and the Live Export API reference. Every error code is in the Error reference.
If your runtime supports MCP, prefer the MCP server. It wraps the same API, signs in through the browser instead of an API key, and helps guide you through the right steps.
Connection details
Section titled “Connection details”Base URL: https://api.whalesync.com/v1Spec: https://api.whalesync.com/v1/openapi.json (describes every endpoint)Auth: Authorization: Bearer ws_tok_…Scopes: read (monitor) · operate (pause, activate, retry, trigger and cancel Live Export runs) · readwrite (build and change)Fetch the OpenAPI spec first. Everything else can be read from it. Paths in this guide are relative to the base URL: GET /sync/connectors means GET https://api.whalesync.com/v1/sync/connectors.
Ask a human for an API key
Section titled “Ask a human for an API key”There is no endpoint that creates an API key. API keys are created manually by a human in the Whalesync app. Do not probe for POST /keys or similar; no such endpoint exists.
A request without a usable key returns a 401 with a required_action containing the key-creation link to give to your user. You can also ask before starting:
To set this up I need a Whalesync API key. I can't create one myself.
- Open https://app.whalesync.com/settings/api-keys
- Click Create key
- Choose Read & write so I can build the sync. Read & operate is enough if you only want me to pause, resume, and retry it, and Read only if you just want me to watch it.
- If I'm only looking after one existing sync, choose One sync and pick it.
- Copy the key and paste it here. It's shown only once.
Treat it like a password: it can read and change your syncs, including the contents of synced records.
Ask for the weakest scope that covers the job. From weakest to strongest:
read(Read only): everyGETandPOST …/validate.operate(Read & operate): everythingreadallows, plus pausing and activating syncs, retrying issues, refetching records, and triggering and canceling Live Export runs. Activating never starts adraftsync.readwrite(Read & write): everything, including creating, editing, and deleting syncs, mappings, and Live Exports.
Scope is fixed at creation. A key can also be limited to one sync. It then sees only that sync, gets 404 not_found for every other sync, and can't create syncs or use Live Export endpoints (403 sync_restricted_key). If you only need to monitor or run one existing sync, ask for a read or operate key limited to that sync. See Scopes and Keys limited to one sync.
Keys can be revoked at any time from the same page. Revocation is immediate.
Steps requiring human intervention
Section titled “Steps requiring human intervention”The API can create a sync, read the tables and fields on both sides, create tables and fields on a side where they don't exist yet, write and validate mappings, pause, activate, and delete syncs, read the operations log and open issues, look up a single record's sync state, and list the deletes waiting for review.
For a sync, three steps require a human. All are deliberate design decisions, so there is no API path around them.
Connecting a side in the browser. A person connects a side whenever its credentials aren't sent inline. That is always the case for apps that sign in through a browser — HubSpot, Salesforce, and similar OAuth connectors, whose credentials cannot be sent over the API at all. Apps that also take a pasted token, such as Airtable, Webflow, and Notion, take it inline (for Airtable, {"connector": "airtable", "auth": {"apiKey": "pat..."}, "base": "app..."}) or can be left for a person like any other side; GET /sync/connectors lists them as api_key. It is also a choice for connectors that take credentials inline (Postgres, Supabase, and others): if your user would rather not paste an app's credentials — a database password, a service API key — into the conversation, you can defer that side to a person too. Either way, declare that side with connector only and omit auth and base. The created sync has that side null and a pending_actions entry with a link for a human, who signs in, connects the app, and picks its base.
Starting a sync. A sync only runs under mappings a human has reviewed and started in the app. Build the sync, then hand over its review_url. Any later mappings edit returns the sync to draft, which requires a new review.
Deciding a pending delete. On a sync with delete_approval: "review_required", a record that goes missing on one side waits for a person before the delete reaches the other side. Approving or ignoring one is irreversible, so no endpoint does it. GET /sync/pending-deletes?sync=… lists what's waiting; relay each entry's review_url.
Live Export. The only human step is connecting a side in the browser, where the person also picks where the destination tables go. There is no review step before an export runs, so you are responsible for confirming with your user that the destination tables can be overwritten.
How human steps appear in the API
Section titled “How human steps appear in the API”The same object appears on the sync as pending_actions and on a blocked call's 409 as required_action:
{"type": "user_authorization", "audience": "end_user", "action": "open_in_browser", "side": "right", "connector": "airtable", "url": "https://app.whalesync.com/syncs/edit/9f2c…/connect/right?connector=airtable", "instruction": "Give this link to a person. They sign in to Whalesync and connect Airtable in the browser — credentials entered there never pass through the API or an agent. Agents cannot complete this step."}Relay instruction and url to your user. Do not fetch the URL; it is a login-protected page for a human. Poll the sync until the step is complete.
Typical sync creation flow
Section titled “Typical sync creation flow”GET /sync/connectorsto find your two apps and how each authenticates.POST /sync/syncswith credentials inline for the app that takes them, and connector alone for the browser one — or connector alone for any app whose credentials the user prefers to enter themselves.- Relay the sync's
user_authorizationpending action. Poll until both sides are non-null. - Follow
tables_url, then each table'sfields_url, to read the real tables and fields on both sides. Present these for your user to pick from; don't ask them to list tables and fields up front. Once a side is connected you can read its whole schema yourself. POST …/validateyour mappings document, fix issues bycodeandpath, thenPUT …/mappings. Map to existing tables and fields, or have Whalesync create them on a side with{"create": {"name": "…"}}(see Creating tables and fields).- Relay the sync's
user_confirmationpending action. Poll untilstatusisactive. - Monitor with
GET …/status,/sync/operations?sync=…, and/sync/issues?sync=…. Issues carryremediationwritten to be acted on;POST /sync/issues/{id}/retryonce the cause is fixed. - For a question about one record ("why hasn't this contact arrived?"),
GET /sync/records?sync=…&query=…to find it, then follow the hit'surlfor its status: both sides, its open issues, what's queued for it, and whether a delete is waiting on a person.
Typical Live Export flow
Section titled “Typical Live Export flow”A live export copies tables from a source app into new tables in a destination app, one way. Runs overwrite the tables the export manages. Endpoint details are in the Live Export API reference.
GET /live-export/connectors. Pick a source whoseroles.sourceistrueand a destination whoseroles.destinationistrue.POST /live-export/live-exportswith connector names only, unless credentials were handed to you programmatically.- Relay each
pending_actionsentry. Poll until both sides are non-null. Ifdestination.locationis stillnullandGET …/destination/locationsreturns entries, set one withPATCH. GET …/source/tables, then…/fieldsfor each chosen table. Present them for your user to pick from.POST …/mappings/validate, thenPUT …/mappingswithIf-Match.POST …/save, thenGET …/save?wait=50untilstateissucceeded. Check that the export showsunsaved_changes: false.- Confirm with your user that the destination can be overwritten. Then
PATCH …/schedule {"enabled": true, "cadence": "<one of allowed_cadences>"}, orPOST …/triggerto run once. - Monitor with
GET …/statusand/live-export/runs?live_export=….
Creating tables and fields
Section titled “Creating tables and fields”You don't need a human to build the destination table or columns first. Whalesync can create them for you as part of writing mappings. In the mappings document, reference an existing object by string (a remote_id, a Whalesync id, or an exact name when it's unique on that side), or use {"create": {"name": "…"}} to have Whalesync create it on that side, typed from its mapped counterpart:
{"tables": [{ "left_table": "tblContacts", "right_table": {"create": {"name": "contacts"}}, "direction": "left_to_right", "fields": [ {"left_field": "fldName", "right_field": {"create": {"name": "name"}}}, {"left_field": "fldEmail", "right_field": {"create": {"name": "email"}}} ]}]}Creation happens when you PUT …/mappings: the response returns the document with real ids filled in, and retries safely adopt already-created objects. Only one side of a pair may be a create — the other must be an existing table or field to copy the type from.
Best practices
Section titled “Best practices”- Follow the
*_urlfields in responses instead of constructing URLs. They point at the next valid steps for the resource's current state. - Read the schema; don't ask your user for it. Once a side is connected you can list its tables and fields yourself and present them to pick from. A destination table or field that doesn't exist yet doesn't have to be built by hand either — create it with
{"create": {"name": "…"}}. - Send
Idempotency-KeyonPOST /sync/syncs, the sync mappingsPUT, andPOST /live-export/live-exports, the calls that create objects, so retries are safe. - Use
If-Matchon the mappingsPUTwith the revision you read, so a concurrent edit made in the app fails with412 revision_mismatchinstead of being overwritten. - Prefer
remote_idover names when referencing bases, tables, and fields. Names are not unique and can be renamed. - Don't ask the user to paste an app's credentials into the conversation unless they offer — create that side without
authand hand over the connect link. - Branch on
code, notmessage. Messages are written for people and may change.