Agent quickstart
Guide for AI agents building syncs with the Whalesync API
This page is for AI agents driving the Whalesync API on someone's behalf, and for the people setting them up. Full endpoint documentation is in the 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
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) · 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
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 and write so I can build the sync (Read only is enough if you just want me to watch 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.
Request the read scope if you only need monitoring. A read key can call every GET and POST …/validate; anything that changes state needs readwrite. Scope is fixed at creation.
Keys can be revoked at any time from the same page. Revocation is immediate.
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.
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 — Airtable, HubSpot, Salesforce, Webflow, and similar OAuth connectors, whose credentials cannot be sent over the API at all. 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.
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:
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
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.
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:
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
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/syncsand the mappingsPUT, the two 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.
Last updated
Was this helpful?

