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

Error reference

Handling failures from the Whalesync API

Every error response has the same shape:

{"error": {"type": "invalid_request_error", "code": "base_not_found",
           "message": "…", "doc_url": "https://docs.whalesync.com/api/errors#base_not_found"}}

Branch on code. Messages are written for people and may change.

type says how to handle the failure, independent of the status:

type

Meaning

authentication_error

No usable API key. Don't retry with the same one.

permission_error

Authenticated, but not allowed to do this.

invalid_request_error

Something about the request is wrong, or the resource isn't in a state that allows it.

requires_action

A person has to do something first. Always carries required_action.

rate_limit_error

Over the per-key budget. Wait for Retry-After.

api_error

A problem on our side. Retry.

Two fields appear on some errors. required_action is the step a person must take, with an instruction written to be relayed word for word; it appears on every requires_action error and on the two authentication codes. details carries machine-readable specifics for the codes that have them.

Authentication and access

missing_api_key

401 · No Authorization: Bearer ws_tok_… header. The required_action links a person to key creation and names the smallest scope this endpoint needs. There is no endpoint that creates a key; API keys are created manually by a human in Whalesync → Settings → API keys.

invalid_api_key

401 · The key is unrecognized, malformed, or revoked. Deliberately the same response for all three: the API never confirms whether a key ever existed. Carries the same required_action as above.

insufficient_scope

403 · A read key was used for something that changes state. read keys may call every GET and POST …/validate, which writes nothing. Ask a person for a readwrite key; scope can't be changed after creation.

public_api_not_enabled

403 · The key is valid but the account isn't in the API's launch yet. This is not something the account owner can turn on. They should ask for API access at support@whalesync.com. Existing keys keep working once it's enabled; a new key won't help. Deliberately not a requires_action, because no browser step resolves it.

rate_limit_exceeded

429 · Over the per-key budget. Wait Retry-After seconds. RateLimit-* headers on every response show the budget before you hit it.

subscription_required

403 · The account has no subscription that permits creating or changing syncs. A person resolves it in billing.

Request shape

invalid_request

400 · The request body or parameters didn't validate. The message names the problem.

invalid_limit

400 · limit is outside the allowed range.

invalid_cursor

400 · cursor isn't one this endpoint issued. Pass back the previous page's next_cursor unchanged. Cursors are opaque and not portable between endpoints.

missing_sync

400 · /sync/operations, /sync/issues, /sync/records, and /sync/pending-deletes require ?sync=. All are read one sync at a time. Every sync carries pre-filtered operations_url, issues_url, and pending_deletes_url. /sync/records/{record_id} needs ?sync= too when the id is a connected-app id rather than a rec_ id.

invalid_sync

400 · The sync filter value isn't a sync id.

invalid_table

400 · The table filter value isn't a table id.

invalid_side

400 · A side value wasn't left or right.

invalid_since

400 · A since value wasn't an ISO 8601 timestamp.

invalid_type

400 · A type filter value was outside the issue types.

not_found

404 · No such resource, or it belongs to someone else. The two are indistinguishable on purpose, so the API never reveals that an id exists.

invalid_idempotency_key

400 · The Idempotency-Key header was malformed or too long.

idempotency_key_reused

400 · The same key arrived with a different body, which means your retry logic is sending new work under an old key. Keys last 24 hours.

idempotency_key_in_use

409 · A duplicate landed while the first request was still running. Retry after it finishes.

Building a sync

unknown_connector

400 · No such connector, or it isn't available to this account. List them with GET /sync/connectors. Connectors above the account's plan appear there with available: false and the plan that unlocks them.

connector_pair_not_allowed

400 · This connector can't sync to itself.

missing_auth

400 · An API-key side sent base without auth. On an API-key side the two travel together: send both, or omit both and a person connects it in the browser, picking the base there. Send both and the side is built and its credentials validated live; omit both and the side comes back null with a user_authorization pending action, exactly like an OAuth side. (This also fires when a PATCH changes a side's connector without supplying the full auth and base the new connector needs.) Field ids for auth come from GET /sync/connectors; send them exactly as given (they're the connector's own, for example connectionString).

missing_base

400 · The mirror of missing_auth: an API-key side sent auth without base. Send both, or omit both and a person connects it in the browser, picking the base there.

auth_not_supported

400 · This side's app signs in through a browser, so it takes no auth. Send only {"connector": "…"} for it. The sync comes back with that side null and a pending action linking a person to the step that connects it, where they also choose its base. Deferring a side to a person this way now works for every connector, not only browser sign-in apps: an API-key side declared by connector alone is deferred the same way (a choice there, rather than the only option — see missing_auth).

base_not_supported

400 · Same as auth_not_supported: a browser sign-in side takes no base either. The person who connects the app picks its base.

oauth_connector_not_supported

400 · Credentials for a browser sign-in app can't be sent over the API, so they can't be set or rotated here. This does not mean such syncs must be built in the app: creating one works, per auth_not_supported above. Reconnecting an existing one is done by a person in Whalesync.

invalid_auth

400 · The credentials were rejected by the app they're for. The message carries what that app said.

connection_error

400 · The app couldn't be reached with those credentials: wrong host, network failure, or a database that isn't accepting connections.

base_not_found

400 · The base couldn't be resolved. details.bases lists what the credentials can reach; pick one and retry. Nothing is created by a failed create. Prefer the base's remote_id: names aren't unique.

base_ambiguous

400 · The base matched more than one by name. details.bases lists the candidates; pick one by remote_id and retry. Base names are not unique.

base_conflict

400 · Both sides point at the same base with the same credentials. A base can't sync to itself.

sync_not_draft

409 · A side's connector or base can only be changed while the sync is a draft. Credentials (auth) can be rotated at any time.

Mappings and schema

sync_active

409 · Mappings can't be edited, and schema can't be force-refreshed, while a sync is running. An active sync's schema is kept fresh in the background and a manual fetch would race it. Pause first. Plain reads always work.

invalid_mappings

400 · The document is malformed or the write path rejected it. details.issues carries the specifics with a JSON pointer into your document. Run POST …/validate first to get the same list without attempting a write.

revision_mismatch

412 · The If-Match revision is stale. The mappings changed since you read them, probably edited in the app. Fetch the document again and reapply your edit.

create_failed

400 · A {"create": …} placeholder couldn't be created in the destination app. The message carries the app's reason and the document path. Re-sending a create that matches an object of the same name adopts it, so retrying after a partial failure is safe.

table_setup_failed

400 · A mapped table couldn't be prepared for syncing. Some connectors add a Whalesync ID column to a table before it can sync, and that step failed. The message carries the app's reason.

table_ambiguous

400 · A bare remote_id matched tables on both sides. Use the prefixed table_… id.

auth_required

409 · requires_action · A side still has no connection, so there's nothing to map or list schema for. The required_action links a person to the step that connects it. Poll the sync until the side stops being null.

confirmation_required

409 · requires_action · The sync is a draft: no one has reviewed and started it under its current mappings. The API can't start a sync a person hasn't approved. Hand over the required_action (the same page as the sync's review_url), then poll until status is active. Any mappings edit returns a sync to draft, so this recurs after every change.

Validation issues

These are not errors. POST …/validate returns 200 with an issues array, and the mappings PUT repeats the same objects in details.issues when it refuses. Each carries a path pointing into your document. severity: "error" means a person can't start the sync until it's fixed; warning never blocks.

incompatible_field_types

The two fields can't carry each other's values in the direction they're mapped. Evaluated per direction; a pair can be fine one way and not the other.

required_field_unmapped

The destination requires this field, so a record can't be written without it.

foreign_key_target_unmapped

A linked-record field points at a table that isn't in the mappings. Map the target table too.

foreign_key_target_mismatch

The two sides' link fields point at tables that aren't mapped to each other.

unknown_table

A table reference doesn't resolve on that side. References may be a remote_id, a Whalesync id, or an exact unique name; resolution is per side.

unknown_field

A field reference doesn't resolve on that side. Same resolution rules as unknown_table.

orphaned_table

The reference resolves to a table the app no longer has. It was deleted or renamed since the schema was last fetched. Refresh the schema with ?refresh=true.

orphaned_field

The reference resolves to a field the app no longer has. Refresh the schema with ?refresh=true.

view_required

A table synced through views needs a view on its side. Read the legal values from the table's views.options.

view_not_found

The view value isn't one that table offers. Note that view is part of the full-replace document, so omitting it in a later PUT clears the previous choice and fails with view_required.

view_not_supported

A view was given for a table that doesn't sync through views (views is null).

create_unsupported

The connector can't create tables or fields, so a {"create": …} placeholder can't be honored on that side. Severity error.

create_name_collision

Something with that name already exists, and the create will adopt it rather than make a new one. Severity warning; it never blocks.

Records and deletes

ambiguous_record

400 · A record id from a connected app matched records in more than one table. Add ?table= to pick one. A rec_ id is never ambiguous.

delete_approval_disabled

409 · /sync/pending-deletes was called on a sync that auto-approves deletes, so it has no queue. A sync's delete_approval field says which mode it is in before you call.

Server

internal_error

500 · An unexpected error on Whalesync's side. Retry; if it persists, contact support@whalesync.com. The response deliberately carries no internal detail.

Last updated

Was this helpful?