Errors

The error body, how to handle refusals, request ids for support, and every error reason grouped by HTTP status.

Every refusal answers a JSON error body with a stable reason code. Branch on the reason, log the request id, and use the table at the end to see every reason a /v1 call can answer.

The error body

An error answer
HTTP/1.1 400 Bad Request
content-type: application/json
x-request-id: 5c1f0d2e-8a4b-4f7e-9c33-0e6b2d1a9f40

{
  "defined": true,
  "code": "invalid_input",
  "status": 400,
  "message": "Input validation failed",
  "data": {
    "issues": [{ "path": ["to", 0, "address"], "message": "Invalid email address" }]
  }
}
FieldMeaning
codeThe error's stable reason, in snake_case. Branch on it.
statusThe HTTP status. A reason always answers with the same status.
messageFor people. It may change; never parse it.
dataStructured details, present only for the reasons below.
definedtrue for a reason the API declares.

Reasons that carry data:

Reasondata
invalid_inputissues: each problem's path in the input (empty for the input as a whole) and message.
unknown_account, unknown_mailboxids: the ids the request named that the tenant does not have.
invalid_template, template_render_failedfield: the template field that did not compile or render.
sync_kinds_disabledkinds: the sync kinds the request named that the account does not run.

Each operation in the API reference lists the reasons it declares, each with the condition it is answered on.

Handle errors

  • 4xx: the request was refused. Fix it, or act on the reason (reconnect an account on account_reauth_required, wait for export_not_ready).
  • 429 and 503: slow down and retry after Retry-After. See Rate limits.
  • 500 and 502: the outcome of a write may be unknown. Retry a POST with the same Idempotency-Key; 5xx answers are not stored, so the retry runs the request again. See Idempotency.

The SDK throws every API error as an ORPCError whose code is the reason:

TypeScript
import { isApiError, isDefinedError, safe } from "@workwae/sdk";

// safe() returns [error, data]; isDefinedError() narrows to the reasons this operation declares.
const [error, entry] = await safe(workwae.outbox.get({ accountId, outboxId }));
if (isDefinedError(error) && error.code === "send_not_found") {
  // ...
} else if (error) throw error;

// isApiError() checks for one reason anywhere, and narrows data to that reason's.
try {
  await workwae.outbox.create({ accountId, to: [], text: "hi" });
} catch (error) {
  if (isApiError(error, "invalid_input")) console.log(error.data.issues);
  else if (isApiError(error)) console.log(error.code, error.status, error.requestId);
  else throw error;
}

Request ids

Every answer carries an X-Request-Id header. Quote it when you contact support: it finds the request in Workwae's logs, and it is recorded on the request's audit log entries.

To correlate with your own logs, send your own id: 1 to 128 characters of A-Z a-z 0-9 . _ : -. It is echoed back. Without one, or when yours does not fit, the API generates one.

curl -i "https://api.workwae.com/v1/accounts" \
  -H "Authorization: Bearer $WORKWAE_API_KEY" \
  -H "X-Request-Id: checkout-7f3a91"

Reasons every operation can answer

Besides its own reasons, any operation can answer:

StatusReasons
400invalid_input
401unauthenticated (no credential, or one that is malformed, unknown, revoked or an expired account token), api_key_expired (past its expiry, or past the grace of the rotation that replaced it)
403missing_scope, role_forbidden, account_token_forbidden, account_scope_forbidden, mcp_only_key
413payload_too_large
429rate_limited, too_many_requests
500internal_error
503overloaded

POST operations can also answer invalid_idempotency_key (400), idempotency_key_in_progress (409) and idempotency_key_reused (422). See Idempotency.

Reasons by status

Every reason a /v1 operation declares, grouped by the one HTTP status it always answers with.

StatusMeaningReasons
400The request itself is wrong.access_token_rejected ai_disabled ai_refused attachments_too_large cloud_required contact_unidentifiable credential_conflict credential_mismatch credential_missing dav_discovery_failed dav_unsupported directory_id_not_allowed from_not_allowed gmail_push_unsupported google_imap_requires_own_app honeypot_needs_click_tracking https_required identities_managed_by_provider identity_failed identity_not_custom identity_unavailable invalid_certificate invalid_client invalid_cursor invalid_grant invalid_idempotency_key invalid_input invalid_range invalid_recurrence invalid_service_account_key invalid_template invalid_time_zone invalid_working_hours list_send_recipients list_unsubscribe_unavailable login_failed mailbox_not_label mailbox_outside_accounts mailbox_parent meeting_app_mismatch meeting_app_secret_unreadable meeting_recurrence_unsupported meeting_service_rejected mixed_event_times multiple_meeting_options no_recipients not_a_draft not_dav_server not_imap_account not_microsoft not_testable oauth_app_mismatch oauth_app_secret_unreadable oauth_servers_fixed org_connection_account params_too_large password_not_stored password_required password_unsupported plaintext_not_allowed provider_not_allowed provider_not_configured provider_rejected provider_unsupported push_unavailable recurrence_on_instance redirect_uri_not_allowed replay_range_too_large scope_denied scope_missing server_not_allowed session_expired shared_mailbox_account shared_mailbox_unsupported storage_unavailable suppressed template_account_mismatch template_and_inline_fields template_render_failed token_exchange_failed tracking_unsupported user_required
401No usable credential.api_key_expired unauthenticated
402Payment is past due.billing_past_due
403The caller or the tenant's plan may not.account_scope_forbidden account_token_forbidden calendar_read_only mcp_only_key missing_scope plan_limit_accounts plan_limit_sends plan_limit_storage plan_limit_webhooks plan_module_unavailable provider_forbidden role_forbidden session_required
404A resource the path names does not exist.account_not_found booking_not_found calendar_event_not_found calendar_not_found connect_session_not_found contact_not_found draft_not_found event_not_found export_not_found identity_not_found mcp_key_not_found meeting_app_not_found meeting_connect_session_not_found meeting_connection_not_found message_not_found oauth_app_not_found org_connection_not_found photo_not_found provider_not_found scheduling_link_not_found send_not_found suppression_entry_not_found suppression_list_not_found suppression_review_not_found template_not_found thread_not_found webhook_endpoint_not_found
409The resource's state forbids it.account_disabled account_exists account_misconfigured account_mismatch account_paused account_reauth_required booking_cancelled calendar_disabled calendar_primary connection_in_use consent_required contacts_disabled directory_unavailable endpoint_disabled export_failed export_in_progress export_not_ready export_running idempotency_key_in_progress identity_exists mailbox_protected mcp_key_limit meeting_app_exists meeting_app_in_use meeting_deleted meeting_host_missing meeting_service_not_connected meeting_service_reauth_required oauth_app_in_use organizer_cannot_respond provider_conflict send_not_cancellable send_not_reschedulable session_used slot_taken slot_unavailable suppression_list_exists suppression_list_not_empty sync_kinds_disabled template_name_taken
410The resource is gone for good.export_expired
413The body is too large.payload_too_large
422The body or query names something that does not exist.autodiscovery_failed idempotency_key_reused shared_mailbox_inaccessible unknown_account unknown_calendar unknown_contact_folder unknown_mailbox unknown_meeting_app unknown_message unknown_oauth_app unknown_reply_target unknown_template
429Slow down: retry after Retry-After.provider_throttled rate_limited too_many_requests
500A failure on Workwae's side.export_corrupt internal_error
502An upstream (mail provider, billing, AI gateway) failed.ai_suspended ai_unavailable draft_lost meeting_service_unavailable provider_unavailable
503The API is shedding load: retry after Retry-After.overloaded

On this page