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.

StatusCodeDescription
400invalid_actionThe `action` field is not one of the values this endpoint accepts.
400invalid_bodyA field in the request body failed validation.
400invalid_jsonThe request body is not valid JSON. Nothing was done; serialize the body with a JSON library and send it again.
400invalid_paramA single path parameter (typically an id) failed validation.
400invalid_paramsA query string parameter failed validation.
400invalid_providerThe provider in the URL or session does not match the request.
400invalid_typeThe `type` field is not one of the values this endpoint accepts.
400unknown_paramsThe request included a parameter that is not in the allowlist for this endpoint.
401invalid_credentialsThe credentials supplied to a Connect call were rejected by the upstream channel.
401unauthorizedMissing or invalid API key.
402listings_limit_exceededThe workspace has more active listings than its plan allows, or an activation would put it over. Deactivate listings or upgrade.
403connection_reauth_requiredThe 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.
403forbiddenAuthenticated, but the workspace does not have access to this resource or feature.
403insufficient_accessThe Airbnb account is connected with read-only or messaging access, and this action needs full access. Reconnect with full access.
403listing_inactiveThe listing this request addresses is inactive. Activate it to read or change it through the API.
403listing_not_api_connectedAPI sync is switched off for this listing on Airbnb. Turn it on for the listing itself; reconnecting the account will not help.
404no_connectionNo 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.
404not_foundThe id does not exist, or it belongs to a different workspace.
409conversation_already_bookedThe guest on this conversation has already booked, so there is nothing to pre-approve.
409delivery_already_succeededThe endpoint already accepted this delivery. Replaying it would send a duplicate; pass `force` if you need it anyway.
409idempotency_key_in_useThe first request with this `Idempotency-Key` is still running. Wait, then retry with the same key.
409inquiry_expiredThe inquiry expired on Airbnb. Message the guest, or send a special offer if they ask again.
409inquiry_no_longer_openAirbnb says the inquiry or offer already moved on (booked, declined, pre-approved or closed). Re-read the conversation; do not retry.
409invalid_stateThe resource is not in a state that allows this operation.
409listing_not_on_bookingPublishing to Booking.com was asked for a listing no Booking.com room is linked to. Link a room, or create a property for it.
409pms_duplicateThe PMS already holds a booking made with this `Idempotency-Key`. The first request went through; `existing` names the booking.
409pms_group_bookingThe stay is part of a group booking in the PMS. Change or cancel group bookings in the PMS.
409pms_unavailableThe PMS says the dates or the unit are not available. Nothing was booked. Pick other dates, or quote first.
409pms_writes_offThe 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.
409provider_unavailableThe provider is in the catalog but not yet available for new connections.
409replay_limit_reachedThis 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.
409request_expiredAirbnb expired the booking request (hosts have 24 hours to answer). The guest has to request again.
409request_no_longer_pendingAirbnb says the booking request was already accepted, declined, withdrawn or cancelled. Re-read it; do not retry.
409reservation_not_pendingOnly a pending booking request can be accepted or declined. `currentStatus` says what the reservation is; do not retry.
409reservation_owned_by_channelThe booking belongs to a channel (Airbnb, Booking.com, Vrbo), including one that came in through a PMS. Change or cancel it on the channel.
409session_terminalThe Connect session is already completed, errored, or cancelled.
410session_expiredThe Connect session lifetime has elapsed. Start a new session.
422airbnb_link_missingThe conversation or reservation has no Airbnb thread, confirmation code or host on record. Reconnect Airbnb so it re-syncs.
422airbnb_rejectedAirbnb refused the change as sent. `message` carries Airbnb's reason.
422attachment_requires_messageBooking.com does not deliver a file without message text. Nothing was sent; add a `message`.
422attachment_too_largeA file is over the 10 MB per-file limit. Nothing was sent; compress or resize it.
422attachment_type_not_supportedA file's real type (read from its bytes) is not one the channel accepts. Nothing was sent.
422attachment_unreachableAn attachment URL could not be downloaded: an error status, an empty file, or no answer within 20 seconds. Nothing was sent.
422attachment_url_not_allowedAn attachment URL is not a public https:// address Repull can fetch (private address, credentials in the URL, …). Nothing was sent.
422attachments_not_supportedThe conversation's channel cannot carry files (SMS, email, direct-booking site chat), or the endpoint sends text only. Nothing was sent.
422booking_rejectedBooking.com refused the change as sent. `message` carries their reason and `booking_ruid` identifies the call to their support.
422channel_not_supportedPre-approvals, special offers and accepting or declining requests exist only for Airbnb listings connected directly. Nothing was sent.
422idempotency_key_reusedThe `Idempotency-Key` was already used for a different request. Use a new key per distinct request.
422inventory_not_in_rate_updateA Booking.com rates update carried `roomsToSell`. Inventory belongs to the availability write.
422listing_not_on_airbnbThe listing named in a special offer is not linked to Airbnb in this workspace.
422message_not_sentThe channel refused the message and nothing reached the guest. `statusReason` carries the channel's own words.
422message_partially_sentSome of the channel messages a send produced reached the guest and some did not. `parts` says which; resend only the failed ones.
422pms_not_linkedThe listing is not managed in a PMS, so there is no PMS quote. Book it directly, or price it with `GET /v1/quotes`.
422pms_rejectedThe PMS refused the request as sent; its reason is in `message`. Nothing was changed. Correct it and retry with a new `Idempotency-Key`.
422pms_write_unsupportedThe 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.
422reservation_not_createdA direct booking was declined for a booking reason, for example dates that cannot be booked. `message` says why.
422reservation_not_modifiableThis reservation cannot be changed through the API (for example it has no confirmation code). Change it where it was made.
422reservation_not_modifiedThe change was refused for a booking reason, for example a move to a listing that cannot take it. Nothing was changed; `message` says why.
422restriction_not_supportedThe channel has no way to receive this restriction. It is refused rather than dropped; set it in the channel's extranet.
422too_many_attachmentsMore files than the channel takes in one request (5). Nothing was sent; split them across requests.
422unsupported_fieldA field this listing or endpoint cannot take. Refused by name, never silently dropped — `fields` (or `field`) names it.
429airbnb_rate_limitedAirbnb is rate-limiting writes for this host. Back off and batch dates.
429booking_rate_limitedBooking.com is rate-limiting this property. Back off and put more dates into one request.
429daily_limit_exceededThe 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.
429rate_limitedToo many requests. Back off and retry using the Retry-After header.
500internal_errorSomething went wrong on our side. Safe to retry with backoff.
500service_misconfiguredA fault on our side stopped the action before anything was sent. `retryable` is `false`: do not resend in a loop; report the `request_id`.
501not_implementedThe endpoint exists but the requested capability is not yet available.
502airbnb_errorAirbnb had an outage or timed out. The request is fine; retry with backoff.
502booking_errorBooking.com had an outage or timed out. The request is fine; retry with backoff.
502message_send_failedA message send failed for a reason Repull could not classify, usually the channel being unavailable. Retry with the same `Idempotency-Key`.
502offer_action_failedA pre-approval, special offer or request response failed for a reason Repull could not classify. Retry with the same `Idempotency-Key`.
502pms_errorThe PMS could not be reached or failed. Nothing was confirmed. Retry with the SAME `Idempotency-Key`.
502quote_failedThe PMS quote failed for a reason not covered by a more specific code. Nothing was booked; retry.
502reservation_create_failedCreating the reservation failed for a reason that is not about the request. Retry with the same `Idempotency-Key`.
502reservation_created_in_pms_onlyThe 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.
502reservation_update_failedChanging the reservation failed for a reason that is not about the request. Re-read the reservation, then retry.
503attachment_storage_failedA 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.

AI