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
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" }]
}
}| Field | Meaning |
|---|---|
code | The error's stable reason, in snake_case. Branch on it. |
status | The HTTP status. A reason always answers with the same status. |
message | For people. It may change; never parse it. |
data | Structured details, present only for the reasons below. |
defined | true for a reason the API declares. |
Reasons that carry data:
| Reason | data |
|---|---|
invalid_input | issues: each problem's path in the input (empty for the input as a whole) and message. |
unknown_account, unknown_mailbox | ids: the ids the request named that the tenant does not have. |
invalid_template, template_render_failed | field: the template field that did not compile or render. |
sync_kinds_disabled | kinds: 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 forexport_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
POSTwith the sameIdempotency-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:
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:
| Status | Reasons |
|---|---|
| 400 | invalid_input |
| 401 | unauthenticated (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) |
| 403 | missing_scope, role_forbidden, account_token_forbidden, account_scope_forbidden, mcp_only_key |
| 413 | payload_too_large |
| 429 | rate_limited, too_many_requests |
| 500 | internal_error |
| 503 | overloaded |
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.
| Status | Meaning | Reasons |
|---|---|---|
400 | The 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 |
401 | No usable credential. | api_key_expired unauthenticated |
402 | Payment is past due. | billing_past_due |
403 | The 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 |
404 | A 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 |
409 | The 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 |
410 | The resource is gone for good. | export_expired |
413 | The body is too large. | payload_too_large |
422 | The 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 |
429 | Slow down: retry after Retry-After. | provider_throttled rate_limited too_many_requests |
500 | A failure on Workwae's side. | export_corrupt internal_error |
502 | An upstream (mail provider, billing, AI gateway) failed. | ai_suspended ai_unavailable draft_lost meeting_service_unavailable provider_unavailable |
503 | The API is shedding load: retry after Retry-After. | overloaded |