Errors
Stable, machine-readable error codes with a self-serve fix walkthrough for every one.
TL;DR
Every error returns a JSON envelope with a stable code field. Switch on code, not on the HTTP status — the status can change between releases, the code cannot.
The error envelope
Every non-2xx response from api.repull.dev follows the same shape. Optional fields are only set when relevant.
{
"error": {
"code": "invalid_params",
"message": "limit must be between 1 and 100",
"docs_url": "https://repull.dev/docs/errors/invalid_params",
"example": "curl https://api.repull.dev/v1/listings?limit=50 ..."
}
}code— stable identifier you can switch on. Listed in the table below.message— human-readable explanation. Includes the offending field name when relevant.docs_url— direct link to the per-code page below. Surface this in your UI for fast self-serve resolution.example— a corrected example you can copy-paste. Set on a subset of codes today; expanding over time.
Reference
Click any code for a fix walkthrough with curl + TypeScript examples and a section for AI agents.
| Status | Code | Description |
|---|---|---|
| 400 | invalid_action | The `action` field is not one of the values this endpoint accepts. |
| 400 | invalid_body | A field in the request body failed validation. |
| 400 | invalid_json | The request body is not valid JSON. Nothing was done; serialize the body with a JSON library and send it again. |
| 400 | invalid_param | A single path parameter (typically an id) failed validation. |
| 400 | invalid_params | A query string parameter failed validation. |
| 400 | invalid_provider | The provider in the URL or session does not match the request. |
| 400 | invalid_type | The `type` field is not one of the values this endpoint accepts. |
| 400 | unknown_params | The request included a parameter that is not in the allowlist for this endpoint. |
| 401 | invalid_credentials | The credentials supplied to a Connect call were rejected by the upstream channel. |
| 401 | unauthorized | Missing or invalid API key. |
| 402 | listings_limit_exceeded | The workspace has more active listings than its plan allows, or an activation would put it over. Deactivate listings or upgrade. |
| 403 | connection_reauth_required | The channel or PMS no longer accepts this connection for the listing or the action — revoked, or granted without write access. Reconnect it; retrying will not help. |
| 403 | forbidden | Authenticated, but the workspace does not have access to this resource or feature. |
| 403 | insufficient_access | The Airbnb account is connected with read-only or messaging access, and this action needs full access. Reconnect with full access. |
| 403 | listing_inactive | The listing this request addresses is inactive. Activate it to read or change it through the API. |
| 403 | listing_not_api_connected | API sync is switched off for this listing on Airbnb. Turn it on for the listing itself; reconnecting the account will not help. |
| 404 | no_connection | No active connection for the provider in this workspace. A reservation write on a listing managed in a PMS you are no longer connected to answers 409 with this code. |
| 404 | not_found | The id does not exist, or it belongs to a different workspace. |
| 409 | conversation_already_booked | The guest on this conversation has already booked, so there is nothing to pre-approve. |
| 409 | delivery_already_succeeded | The endpoint already accepted this delivery. Replaying it would send a duplicate; pass `force` if you need it anyway. |
| 409 | idempotency_key_in_use | The first request with this `Idempotency-Key` is still running. Wait, then retry with the same key. |
| 409 | inquiry_expired | The inquiry expired on Airbnb. Message the guest, or send a special offer if they ask again. |
| 409 | inquiry_no_longer_open | Airbnb says the inquiry or offer already moved on (booked, declined, pre-approved or closed). Re-read the conversation; do not retry. |
| 409 | invalid_state | The resource is not in a state that allows this operation. |
| 409 | listing_not_on_booking | Publishing to Booking.com was asked for a listing no Booking.com room is linked to. Link a room, or create a property for it. |
| 409 | pms_duplicate | The PMS already holds a booking made with this `Idempotency-Key`. The first request went through; `existing` names the booking. |
| 409 | pms_group_booking | The stay is part of a group booking in the PMS. Change or cancel group bookings in the PMS. |
| 409 | pms_unavailable | The PMS says the dates or the unit are not available. Nothing was booked. Pick other dates, or quote first. |
| 409 | pms_writes_off | The PMS connection's write policy turns off bookings through the API. Nothing was sent to the PMS. Turn on `reservations.api`, or make the change in the PMS. |
| 409 | provider_unavailable | The provider is in the catalog but not yet available for new connections. |
| 409 | replay_limit_reached | This delivery has been replayed 3 times in the last hour. Wait for `next_replay_allowed_at` — and fix the receiving endpoint, which is what keeps failing. |
| 409 | request_expired | Airbnb expired the booking request (hosts have 24 hours to answer). The guest has to request again. |
| 409 | request_no_longer_pending | Airbnb says the booking request was already accepted, declined, withdrawn or cancelled. Re-read it; do not retry. |
| 409 | reservation_not_pending | Only a pending booking request can be accepted or declined. `currentStatus` says what the reservation is; do not retry. |
| 409 | reservation_owned_by_channel | The booking belongs to a channel (Airbnb, Booking.com, Vrbo), including one that came in through a PMS. Change or cancel it on the channel. |
| 409 | session_terminal | The Connect session is already completed, errored, or cancelled. |
| 410 | session_expired | The Connect session lifetime has elapsed. Start a new session. |
| 422 | airbnb_link_missing | The conversation or reservation has no Airbnb thread, confirmation code or host on record. Reconnect Airbnb so it re-syncs. |
| 422 | airbnb_rejected | Airbnb refused the change as sent. `message` carries Airbnb's reason. |
| 422 | attachment_requires_message | Booking.com does not deliver a file without message text. Nothing was sent; add a `message`. |
| 422 | attachment_too_large | A file is over the 10 MB per-file limit. Nothing was sent; compress or resize it. |
| 422 | attachment_type_not_supported | A file's real type (read from its bytes) is not one the channel accepts. Nothing was sent. |
| 422 | attachment_unreachable | An attachment URL could not be downloaded: an error status, an empty file, or no answer within 20 seconds. Nothing was sent. |
| 422 | attachment_url_not_allowed | An attachment URL is not a public https:// address Repull can fetch (private address, credentials in the URL, …). Nothing was sent. |
| 422 | attachments_not_supported | The conversation's channel cannot carry files (SMS, email, direct-booking site chat), or the endpoint sends text only. Nothing was sent. |
| 422 | booking_rejected | Booking.com refused the change as sent. `message` carries their reason and `booking_ruid` identifies the call to their support. |
| 422 | channel_not_supported | Pre-approvals, special offers and accepting or declining requests exist only for Airbnb listings connected directly. Nothing was sent. |
| 422 | idempotency_key_reused | The `Idempotency-Key` was already used for a different request. Use a new key per distinct request. |
| 422 | inventory_not_in_rate_update | A Booking.com rates update carried `roomsToSell`. Inventory belongs to the availability write. |
| 422 | listing_not_on_airbnb | The listing named in a special offer is not linked to Airbnb in this workspace. |
| 422 | message_not_sent | The channel refused the message and nothing reached the guest. `statusReason` carries the channel's own words. |
| 422 | message_partially_sent | Some of the channel messages a send produced reached the guest and some did not. `parts` says which; resend only the failed ones. |
| 422 | pms_not_linked | The listing is not managed in a PMS, so there is no PMS quote. Book it directly, or price it with `GET /v1/quotes`. |
| 422 | pms_rejected | The PMS refused the request as sent; its reason is in `message`. Nothing was changed. Correct it and retry with a new `Idempotency-Key`. |
| 422 | pms_write_unsupported | The PMS that owns the record cannot do this through its API (for example cancel on OwnerRez, reply to a review on Hostaway, or send attachments through Guesty). Do it in the PMS; the change arrives with the next sync. `capabilities.pms` says beforehand. |
| 422 | reservation_not_created | A direct booking was declined for a booking reason, for example dates that cannot be booked. `message` says why. |
| 422 | reservation_not_modifiable | This reservation cannot be changed through the API (for example it has no confirmation code). Change it where it was made. |
| 422 | reservation_not_modified | The change was refused for a booking reason, for example a move to a listing that cannot take it. Nothing was changed; `message` says why. |
| 422 | restriction_not_supported | The channel has no way to receive this restriction. It is refused rather than dropped; set it in the channel's extranet. |
| 422 | too_many_attachments | More files than the channel takes in one request (5). Nothing was sent; split them across requests. |
| 422 | unsupported_field | A field this listing or endpoint cannot take. Refused by name, never silently dropped — `fields` (or `field`) names it. |
| 429 | airbnb_rate_limited | Airbnb is rate-limiting writes for this host. Back off and batch dates. |
| 429 | booking_rate_limited | Booking.com is rate-limiting this property. Back off and put more dates into one request. |
| 429 | daily_limit_exceeded | The workspace made more requests in one UTC day than its plan allows. A circuit breaker against runaway client loops — distinct from the per-minute limiter and the monthly quota. |
| 429 | rate_limited | Too many requests. Back off and retry using the Retry-After header. |
| 500 | internal_error | Something went wrong on our side. Safe to retry with backoff. |
| 500 | service_misconfigured | A fault on our side stopped the action before anything was sent. `retryable` is `false`: do not resend in a loop; report the `request_id`. |
| 501 | not_implemented | The endpoint exists but the requested capability is not yet available. |
| 502 | airbnb_error | Airbnb had an outage or timed out. The request is fine; retry with backoff. |
| 502 | booking_error | Booking.com had an outage or timed out. The request is fine; retry with backoff. |
| 502 | message_send_failed | A message send failed for a reason Repull could not classify, usually the channel being unavailable. Retry with the same `Idempotency-Key`. |
| 502 | offer_action_failed | A pre-approval, special offer or request response failed for a reason Repull could not classify. Retry with the same `Idempotency-Key`. |
| 502 | pms_error | The PMS could not be reached or failed. Nothing was confirmed. Retry with the SAME `Idempotency-Key`. |
| 502 | quote_failed | The PMS quote failed for a reason not covered by a more specific code. Nothing was booked; retry. |
| 502 | reservation_create_failed | Creating the reservation failed for a reason that is not about the request. Retry with the same `Idempotency-Key`. |
| 502 | reservation_created_in_pms_only | The booking exists in the PMS but is not recorded in Repull yet. Do NOT retry: it arrives with the next sync, and the same key replays this answer. |
| 502 | reservation_update_failed | Changing the reservation failed for a reason that is not about the request. Re-read the reservation, then retry. |
| 503 | attachment_storage_failed | A file could not be stored for delivery. Transient and on our side: nothing was sent, retry with the same `Idempotency-Key`. |
How to handle errors
A typical error handler covers four buckets. Per-code pages give you the specifics.
import { Repull } from '@repull/sdk'
const repull = new Repull({ apiKey: process.env.REPULL_KEY! })
async function safeCall<T>(fn: () => Promise<T>): Promise<T | null> {
try {
return await fn()
} catch (err: any) {
switch (err.code) {
// 1. Bad input — fix the request, do not retry verbatim
case 'invalid_params':
case 'invalid_body':
case 'invalid_param':
case 'invalid_action':
case 'invalid_type':
case 'unknown_params':
case 'invalid_provider':
case 'airbnb_rejected': // Airbnb refused the change; error.message has its reason
console.error('Bad request:', err.message)
return null
// 2. Auth / permissions — surface to operator, do not retry
case 'unauthorized':
case 'forbidden':
case 'invalid_credentials':
case 'connection_reauth_required': // reconnect the channel, then retry
case 'listing_not_api_connected': // the host must turn on API sync for THIS listing in Airbnb
console.error('Auth failure:', err.message)
return null
// 3. Resource state — re-fetch and decide
case 'not_found':
case 'invalid_state':
case 'no_connection':
case 'session_expired':
case 'session_terminal':
case 'provider_unavailable':
return null
// 4. Transient — back off and retry
case 'rate_limited':
case 'rate_limit_exceeded':
case 'airbnb_rate_limited':
case 'airbnb_error': // Airbnb outage or timeout
case 'internal_error':
await new Promise(r => setTimeout(r, 1000))
return await fn()
default:
throw err
}
}
}For AI agents
Every code page ends with an If you're an AI agent block — a 1-2 sentence instruction for what to change in the next call. The docs_url field in the envelope deep-links straight to it. See Using Repull from AI agents for the full integration guide.