For the complete documentation index, see llms.txt. This page is also available as Markdown.

API reference

Paths, authentication, and conventions for the Whalesync API

The Whalesync API creates and monitors syncs programmatically. It is a REST API with JSON request and response bodies, API-key authentication, cursor pagination, and stable machine-readable error codes. Responses include URLs for the next available operations, and anything that requires a human comes back as a structured action to relay.

  • Base URL: https://api.whalesync.com/v1. Paths in this reference are relative to it: GET /sync/connectors means GET https://api.whalesync.com/v1/sync/connectors.

  • Prefix: Everything under /sync/ belongs to the sync feature. Other Whalesync features will get their own prefix under the same base URL.

  • Spec: OpenAPI 3.1 at https://api.whalesync.com/v1/openapi.json

  • Discovery: https://api.whalesync.com/llms.txt

  • Companion pages: Agent quickstart walks through the typical sync creation flow · Error reference documents every error code

All request and response fields are snake_case. The one exception is the keys inside a side's auth: those are the credential field ids each connector declares (for example Postgres's connectionString), passed through verbatim, so GET /sync/connectors and auth always agree and nothing is translated.

Authentication

Authenticate every request with an API key in the Authorization header:

Authorization: Bearer ws_tok_XXXXXXXXXXXXXXXXXXXXXXXX

Create keys in Settings → API keys in the Whalesync app. You can have multiple named keys; each key's secret is shown once at creation. Keys can be revoked at any time in Settings, revocation is immediate, and each key shows its last_used_at.

Keys have one of two scopes:

  • readwrite: full access.

  • read: monitoring only. Anything that changes something fails with 403 insufficient_scope. The one POST a read key may call is …/validate, which only checks a document and never writes. Note that read exposes the contents of synced records (before/after values in the operations log).

There is no OAuth for the REST API itself. Browser sign-in without an API key is available through the MCP server.

Conventions

Error responses

Every error has the same shape:

code values are stable. Branch on them, not on message. Some errors add a details object with machine-readable specifics, and requires_action errors add a required_action object for a human. Every code, and the type values that classify them, are documented in the Error reference.

Pending actions

Some steps require a human signed in to Whalesync (connecting an app, starting a sync). A sync lists them in pending_actions:

Calling an endpoint one of them is blocking returns a 409 with the same object as required_action:

type is user_authorization (connect an app), user_confirmation (review and start the sync), or user_api_key (create the key itself, which only ever appears on a 401). Relay instruction and url to your user; don't fetch the URL.

Pagination

List endpoints take limit and cursor, and return {"data": […], "has_more": true, "next_cursor": "…"}.

IDs

Whalesync IDs are prefixed (sync_…, table_…, field_…). Schema objects also carry the remote_id from the connected app (Airtable tbl…/fld…, Postgres table names, and so on), and anywhere you reference a table or field you may use either form.

Responses include *_url fields (tables_url, fields_url, mappings_url, and more) pointing at the next valid steps for that resource's current state. Follow them instead of constructing URLs.

Idempotency

Send an Idempotency-Key header on POST /sync/syncs and on the mappings PUT, the two calls that create objects, to make retries safe. The other writes are already safe to repeat: pause, activate, and issue retry converge on the same state.

A retry with the same key and body replays the original response (marked with an Idempotent-Replayed: true header). The same key with a different body is a 400 idempotency_key_reused, and a retry that lands while the first request is still running is a 409 idempotency_key_in_use. Keys expire after 24 hours; a failed request releases its key so the retry runs fresh.

Rate limits

Per-key limits; 429 with Retry-After when exceeded. RateLimit-* headers on every response show the budget.

Credentials are not returned

Credentials you send (connection strings, connector API keys) are never included in any response.

Connectors

GET /sync/connectors tells you how each connector authenticates: "auth": {"method": "oauth"} or "auth": {"method": "api_key", "fields": [{"id": "connectionString", "label": "Postgres connection string", …}]}. Each field's id is the key to send under a side's auth; send it exactly as given. Connectors your plan doesn't include are still listed, annotated with "available": false and the required plan.

Credentials

Each sync side holds its own credentials: inline auth for API-key connectors, given when the sync is created, or a browser sign-in for OAuth connectors, done by a human in Whalesync. Credentials are scoped to their sync. There are no connection endpoints, nothing to reuse across syncs, and nothing to clean up.

  • Anything the API exposes about a credential (auth status, error codes) appears nested on the side object in sync responses, for example "left": {"auth_status": "error", "auth_error": "invalid_credentials", …}.

  • A broken credential also opens an issue (raising the sync's open_issues count) whose remediation explains how to fix it.

  • To fix or rotate an API-key credential, PATCH the side with a new auth, same shape as at creation: PATCH /sync/syncs/{sync_id} {"right": {"auth": {"connectionString": "…"}}}.

  • To finish an API-key side that was deferred at creation (declared by connector alone, still null), PATCH it with connector, auth, and base together while the sync is a draft. This fills the unconnected side over the API instead of waiting for a person — the alternative to relaying its connect link. OAuth sides can't be filled this way; they are always connected in the browser.

  • OAuth credentials can only be reconnected in the Whalesync app; the issue's remediation sends your user there.

  • To see which remote bases or workspaces a side's credentials can reach (for example after an ambiguous base name):

Syncs

A sync connects two apps (its left and right sides) and keeps mapped tables in sync.

Creating a sync

An API-key side takes a connector and, optionally, auth (inline credentials) and a base. On such a side auth and base travel together: send both, or neither.

  • Both. The side is built during the create and its credentials are validated live.

  • Neither. The side is deferred to a person, exactly like an OAuth side. It comes back null and the sync carries a user_authorization pending action whose link sends someone to a Whalesync page where they enter the credentials in the browser and pick the base. This is not only for OAuth apps — any API-key side can be left for a person, so your user need not paste an app's credentials into an agent. Unlike an OAuth side, a deferred API-key side can still be finished over the API instead (see Credentials).

  • One without the other. base without auth fails with 400 missing_auth; auth without base fails with 400 missing_base.

base is the ID of the base/workspace/schema in the connected app: an Airtable app… ID, a Postgres schema name, and so on. An exact display name is accepted as a fallback when it's unambiguous. It resolves during the create itself: if it can't be found (or matches more than one base) the create fails with base_not_found / base_ambiguous, and the error's details.bases lists what the credentials can reach. Fix the request and retry.

An OAuth side takes only its connector: no auth, no base. Its browser sign-in requires a human, so the API leaves that side unbuilt. It comes back null and the sync carries a user_authorization pending action naming the app you asked for. Relay it to your user; the link opens that app's sign-in directly, they pick the base, and you poll the sync until the side appears.

  • That page requires a normal Whalesync login as the account's owner. It is not a capability link, so it can't be used to attach someone else's account to your sync.

  • Until both sides exist, mappings, schema, and activate calls answer 409 requires_action (auth_required) carrying the same action.

  • When only one side is built, it takes the left slot; the side your user connects becomes the right. Read the sync back once both exist and write mappings against that shape.

Any side can be left for a person to connect: declare it by connector alone, omitting auth and base, and a human completes the connection. For OAuth apps this is the only way; for API-key apps it is a choice, so credentials for the connected app need never pass through the API or an agent.

Sync statuses: draft → active ⇄ paused. A sync is draft until your user starts it in Whalesync, and any mappings edit returns it to draft. There is no separate health field. A sync with problems has a nonzero open_issues count (and auth_status: "error" on the affected side); read /sync/issues for the details.

A sync also says how it handles deletes. delete_approval is review_required, where a record that goes missing on one side waits for a person before the delete is applied to the other side, or auto_approve, where deletes are applied as they're detected. Under review_required, pending_deletes counts what is waiting right now and pending_deletes_url links to the queue. See Pending deletes.

Schema discovery

Table and field listings are fetched from the connected app on demand and cached. Each listing carries a top-level fetched_at saying when its cache was filled, and each table in the table listing also carries fields_fetched_at, null until you ask for that table's fields. Pass ?refresh=true to refetch; like in the app, refreshing needs the sync to be off, since an active sync's schema is kept fresh automatically. A first fetch can take a few seconds; if another request is already loading the same schema the endpoint returns 202 with {"loading": true} and a Retry-After header. Retry the same GET.

Fields look like:

type_details carries whatever extra the connector says about that field type, and is null when there's nothing to add.

type is the connector's own field type. Every connector has its own set, so there is no global type enum. Use the capability flags (read, write, required, primary) to plan mappings, and POST …/validate to check compatibility: it is the authoritative check, and incompatible pairs come back with a specific code and message explaining why.

Views

Some connectors sync a table through one of its views (Airtable, Salesforce). Those tables come back with a views object saying both whether you have to pick one and what the choices are:

Put the value in that side's view in the mappings document. views is null when the connector has no such concept, in which case view must be omitted. Both POST …/validate and the mappings PUT check this and name the legal values back to you: view_required when one is missing, view_not_found for a value the table doesn't have, view_not_supported for a view on a table with none. The PUT rejects before writing anything and repeats the detail in details.issues. Note that view is part of the full-replace document: leave it out of a later PUT and the previous selection is cleared, which fails the same way.

Mappings

The mappings document declares which tables and fields sync, and in which direction. It is read and written as a whole:

  • Table and field references are strings for existing objects (a remote_id, a Whalesync id, or an exact name when it's unique on that side) or {"create": {"name": "…"}} to have Whalesync create the table or field on that side, typed from its mapped counterpart. Creation happens when you PUT; 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.

  • direction per table: left_to_right | right_to_left | two_way. Fields default to their table's direction. A read can additionally show "invalid" for a pair whose allowed directions have all been disabled by schema drift; fix the mapping and PUT back a real direction.

  • A side's record_delete_behavior says what a delete on the other side does to this table's records: sync (the default) or do_nothing, which is Delete protection for that table.

  • Full-replace semantics: anything omitted is unmapped, and omitted settings revert to defaults. Fetch-modify-put is the intended editing pattern.

  • validate returns issues you can fix mechanically:

Errors block activation (not saving a draft); warnings never block. There are two failure channels. A structurally malformed document (wrong types, an invalid direction, a create placeholder on both sides of a pair) is a 400 invalid_mappings naming the path. Everything semantic about a well-formed document (unknown or orphaned references, incompatibilities, unmapped required fields) comes back as a 200 with issues.

A PUT can also fail on a table the connector couldn't prepare for syncing — some connectors add a Whalesync ID column to a table first — which is a 400 table_setup_failed.

Read-only table settings

A table pair carries two settings that are configured in the app and returned for reading only. Both are absent when there is nothing to report, and both are ignored on a PUT.

  • filter is the sync filter on the pair. Records that fail it sync in neither direction, and show is_filtered on their record status. It is a structured object: match (all or any) over conditions, which may nest further groups, plus a summary sentence safe to relay to a person. Filters are edited in the app, before the first sync. See Filters.

  • advanced_settings lists the pair's advanced settings that are off their default: delay_before_syncing (changes wait out a quiet period), row_level_sync (only records whose sync-enabled field is true sync out of that table), and connector_option (a per-table choice the connector asks for). Each entry names the side it's on, its value, and a one-sentence description safe to relay.

Starting and stopping

A sync only runs under a configuration a human has started in the Whalesync app. While a sync is draft (newly created, or edited since it last ran) there is nothing for the API to activate: the sync carries a user_confirmation pending action (and review_url, the same page). Relay it to your user; they review what was built (including record matching for tables that have data on both sides) and start the sync with a click. Poll the sync until status is active.

Calling activate on a draft sync returns the standard prerequisite error:

Once started, pause and activate toggle the sync freely from the API, until the mappings change, which returns it to draft.

Monitoring

Operations and issues are their own collections rather than sub-resources of a sync, filtered by ?sync=, with the filter param named after the resource. The sync filter is required: both collections are always read one sync at a time. Every sync response links to its own slices via operations_url and issues_url.

An operation, as returned by GET /sync/operations/{id}:

trigger is push (Whalesync wrote the change) or poll (Whalesync detected it). The list returns the same object without field_changes and with a null record.url; both need the full stored log for each row, which is too expensive per page. Fetch an operation on its own for them. The operation's table carries no remote_id: the log is stored denormalized, so only the Whalesync id and the display name at the time are on the row.

An issue (note remediation, written to be actionable by an agent):

table and record are null on issues that aren't about one table or one record. A record-level issue carries that record's id, remote_id, name, and a link into the connected app, and the same issue also appears on the record's status document.

code is a broad category (authentication_error, connection_error, record_error, webhook_error, validation_error, sync_error), deliberately coarse: it does not reveal more about your credentials than that they failed. The specifics you should act on are in remediation and message. type is the area the issue is in (connection, record, webhook, sync_preview, or other) and is what ?type= filters on.

Records

These answer questions about one record: why it hasn't arrived, whether something is blocking it, whether it's waiting on a delete review. They report state only. Nothing in this API creates, edits, or deletes record data in a connected app — writing records is what the sync itself does.

Search is capped rather than paginated. limit is at most 25, next_cursor is always null, and has_more: true means more matched than came back, so narrow the query. Exact id matches come first, and table restricts the search to one table. Each hit carries a url to that record's status document and a browser_url that opens the record in the connected app.

The status document gathers everything about one record:

  • left and right, each null when that side has no copy of the record, with is_deleted, is_filtered (a sync filter currently excludes it), is_sync_disabled (turned off through a sync-enabled field), and queued_operations.

  • issues, the record's open issues. An issue here usually blocks it from syncing.

  • pending_delete, a delete awaiting review, or null, plus pending_delete_events as history (detected, approved, ignored).

  • operations, the most recent operations on the record with both sides merged, plus operations_has_more and operations_url for the rest.

queued_operations distinguishes empty from unknown: [] means nothing is queued, null means the queue couldn't be consulted just then. Each entry has a type (push writes to the app, refetch re-reads the record, verify_delete checks that a record detected as missing is really gone) and its position in the queue.

record_id may be a rec_ id or the record's id in the connected app. For the latter, ?sync= is required, and ?table= picks a table when the same id exists in more than one; without it the call fails with 400 ambiguous_record.

Pending deletes

When a record goes missing on one side of a sync with delete_approval: "review_required", Whalesync holds the delete rather than applying it to the other side, so an outage, a pagination bug, or a mistaken write can't destroy data downstream. Delete approval queue describes the feature itself.

state defaults to awaiting_review; pass ignored for the deletes a person chose to keep the record for. Neither state is resolved — an ignored delete is dormant and stops being raised, and can be restored in the app. Standard limit and cursor pagination.

Each entry names the missing_side whose record disappeared, the delete_from_side the record would be deleted from, pending_since, ignored_at, and the record itself (null when the other side no longer holds a copy of it). record_url is that record's status document; review_url is the page where a person decides.

A sync that auto-approves deletes has no queue, and this endpoint answers 409 delete_approval_disabled. The sync's delete_approval field tells you which mode it's in before you call.

Planned additions

Not yet in the API: webhooks, both per-sync record-change deliveries (webhook_url) and API-level events (/webhook_endpoints) · org-scoped API keys. Filters and advanced settings can be read on a mapped table pair but only changed in the app, and deciding a pending delete is deliberately app-only.

Last updated

Was this helpful?