Skip to content

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.

Base URL: https://api.whalesync.com/v1
Spec: 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.

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.

  1. Open https://app.whalesync.com/settings/api-keys
  2. Click Create key
  3. 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.
  4. If I'm only looking after one existing sync, choose One sync and pick it.
  5. 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): every GET and POST …/validate.
  • operate (Read & operate): everything read allows, plus pausing and activating syncs, retrying issues, refetching records, and triggering and canceling Live Export runs. Activating never starts a draft sync.
  • 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.

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.

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.

  1. GET /sync/connectors to find your two apps and how each authenticates.
  2. POST /sync/syncs with 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.
  3. Relay the sync's user_authorization pending action. Poll until both sides are non-null.
  4. Follow tables_url, then each table's fields_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.
  5. POST …/validate your mappings document, fix issues by code and path, then PUT …/mappings. Map to existing tables and fields, or have Whalesync create them on a side with {"create": {"name": "…"}} (see Creating tables and fields).
  6. Relay the sync's user_confirmation pending action. Poll until status is active.
  7. Monitor with GET …/status, /sync/operations?sync=…, and /sync/issues?sync=…. Issues carry remediation written to be acted on; POST /sync/issues/{id}/retry once the cause is fixed.
  8. For a question about one record ("why hasn't this contact arrived?"), GET /sync/records?sync=…&query=… to find it, then follow the hit's url for its status: both sides, its open issues, what's queued for it, and whether a delete is waiting on a person.

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.

  1. GET /live-export/connectors. Pick a source whose roles.source is true and a destination whose roles.destination is true.
  2. POST /live-export/live-exports with connector names only, unless credentials were handed to you programmatically.
  3. Relay each pending_actions entry. Poll until both sides are non-null. If destination.location is still null and GET …/destination/locations returns entries, set one with PATCH.
  4. GET …/source/tables, then …/fields for each chosen table. Present them for your user to pick from.
  5. POST …/mappings/validate, then PUT …/mappings with If-Match.
  6. POST …/save, then GET …/save?wait=50 until state is succeeded. Check that the export shows unsaved_changes: false.
  7. Confirm with your user that the destination can be overwritten. Then PATCH …/schedule {"enabled": true, "cadence": "<one of allowed_cadences>"}, or POST …/trigger to run once.
  8. Monitor with GET …/status and /live-export/runs?live_export=….

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.

  • Follow the *_url fields 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-Key on POST /sync/syncs, the sync mappings PUT, and POST /live-export/live-exports, the calls that create objects, so retries are safe.
  • Use If-Match on the mappings PUT with the revision you read, so a concurrent edit made in the app fails with 412 revision_mismatch instead of being overwritten.
  • Prefer remote_id over 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 auth and hand over the connect link.
  • Branch on code, not message. Messages are written for people and may change.