API Keys & Account Scope

TL;DR

Your API key identifies your workspace and scopes every request to it — you don't pass a tenant id. The optional X-Account-Id header pins a specific provider connection. On the Airbnb collection routes, ?account_id=<airbnb host id> goes one level finer and pins a single connected host account.

There are two levels of scope in the Repull API: the workspace (who you are) and the connected account (which provider connection a request touches). Almost everything you do is scoped by your key alone.

The API key is your workspace

Every request authenticates with your key in the Authorization header (or x-api-key). The key resolves to exactly one workspace, and the API automatically scopes all reads and writes to it. You never pass a customer or tenant id — attempting to read a property, reservation, or connection that isn't yours returns 404 not_found, never another tenant's data.

curl https://api.repull.dev/v1/properties \
  -H "Authorization: Bearer sk_live_YOUR_KEY"

One more thing to know about the key itself:

    Connected-account scope

    When a request needs a specific provider connection — reading a channel's listings, pushing pricing or availability — the API resolves the connection for your workspace automatically. For a single connection per provider (the common case) there is nothing extra to send.

    The optional X-Account-Id header lets you name a specific connection explicitly. When you send it, it must reference a connection your workspace owns, or the whole request returns 404 not_found. Get the account id from GET /v1/connect — each connection in the response has an id field, and that id is exactly what X-Account-Id resolves against.

    # 1. List your connected accounts and copy the id you want
    curl https://api.repull.dev/v1/connect \
      -H "Authorization: Bearer sk_live_YOUR_KEY"
    # → { "data": [ { "id": 55, "provider": "airbnb", "status": "active", ... } ] }
    
    # 2. Pin that account for a request via X-Account-Id
    curl https://api.repull.dev/v1/channels/airbnb/listings \
      -H "Authorization: Bearer sk_live_YOUR_KEY" \
      -H "X-Account-Id: 55"

    Optional today, forward-looking

    X-Account-Id is optional on every endpoint. It exists for workspaces that connect more than one account for the same provider; today the API already resolves your connection from the key, so single-account integrations can omit it entirely. Send it when you want to be explicit about which connection a request targets.

    Several Airbnb accounts on one workspace

    A workspace can connect more than one Airbnb host account. X-Account-Id cannot separate them — it carries a connection id, and there is exactly one Airbnb connection per workspace. The Airbnb collection routes take a second, finer scope instead: ?account_id=<airbnb host id>, the same id GET /v1/connect/airbnb returns as accounts[].externalAccountId and DELETE /v1/connect/airbnb?accountId= accepts.

    It is accepted on the Airbnb collection routes — listings, reservations, messaging, reviews, transactions and alterations. Omit it and you get every connected account, exactly as before.

    Worked example: a workspace with two accounts

    1. List the connected accounts. Each entry's externalAccountId is the scope key.

    curl https://api.repull.dev/v1/connect/airbnb \
      -H "Authorization: Bearer sk_live_YOUR_KEY"
    
    # {
    #   "connected": true,
    #   "provider": "airbnb",
    #   "accounts": [
    #     { "externalAccountId": "1000000000000000002", "name": "Seaside Stays", "status": "active", "connected": true },
    #     { "externalAccountId": "80000001",            "name": "Casey",   "status": "active", "connected": false }
    #   ]
    # }

    2. Scope a request to one of them. Every row carries accountId and accountName whether or not you scope, so you can also group a single unscoped page yourself.

    curl 'https://api.repull.dev/v1/channels/airbnb/reservations?account_id=1000000000000000002&limit=50' \
      -H "Authorization: Bearer sk_live_YOUR_KEY"
    
    # {
    #   "data": [
    #     {
    #       "confirmationCode": "HMABC12345",
    #       "accountId": "1000000000000000002",
    #       "accountName": "Seaside Stays",
    #       "listingId": "18871326",
    #       "startDate": "2026-07-06"
    #     }
    #   ],
    #   "pagination": { "nextCursor": null, "hasMore": false },
    #   "dataFreshness": { "...": "see step 3" }
    # }

    The TypeScript SDK has no account-scoped listing helper yet, so scope the read with fetch (or curl) until it does:

    const res = await fetch(
      'https://api.repull.dev/v1/channels/airbnb/reservations?account_id=1000000000000000002',
      { headers: { Authorization: `Bearer ${process.env.REPULL_API_KEY}` } },
    )
    const { data, dataFreshness } = await res.json()

    3. Read freshness per account. Every Airbnb read is served from Repull's local mirror, so each response carries a dataFreshness envelope. accounts[] gives the verdict for each connected account; the top-level fields aggregate it. lastSyncedAt is the last import that actually landed data — a failed or rate-limited attempt never moves it.

    {
      "dataFreshness": {
        "lastSyncedAt": "2026-09-18T04:12:09.000Z",
        "stale": false,
        "reason": "partial_account_staleness",
        "fixUrl": "https://repull.dev/dashboard/connections",
        "accounts": [
          {
            "accountId": "1000000000000000002",
            "accountName": "Seaside Stays",
            "lastSyncedAt": "2026-09-18T04:12:09.000Z",
            "stale": false
          },
          {
            "accountId": "80000001",
            "accountName": "Casey",
            "lastSyncedAt": null,
            "stale": true,
            "reason": "host_disconnected",
            "fixUrl": "https://repull.dev/dashboard/connections"
          }
        ]
      }
    }
    • Top-level stale: true means every connected account is stale — nothing in the response is current.
    • Top-level stale: false with reason: "partial_account_staleness" means some accounts are fine and some are not. The response is usable; accounts[] says which rows to distrust. The reason is emitted deliberately alongside stale: false, so a consumer reading only the aggregate is never told everything is fine while an account is down.
    • With ?account_id=, accounts[] holds exactly that account and the top-level fields mirror it.

    Host ids are strings, never numbers

    Airbnb host ids exceed 253. Number("1000000000000000002") is 1772489413932732200 — a different account, and a 404. Keep them as strings end to end, including in JSON you re-serialize.

    An account_id that is not connected to your workspace returns 404 not_found with field: "account_id" and your own ids in valid_values— copy one of those, or drop the parameter. An id belonging to another workspace answers identically: confirming it exists would leak that workspace's account.

    By operation

    AreaKey aloneX-Account-Id
    Connect setup (POST /v1/connect/{provider}), key & webhook managementSufficientNot used
    Inventory (/v1/properties, /v1/listings) & reservationsSufficientOptional — pin a connection
    Per-channel reads & writes (pricing, availability, content)SufficientOptional — pin a connection
    Airbnb collections (/v1/channels/airbnb/listings, reservations, messaging, reviews, transactions, alterations)Sufficient — returns every connected accountCannot separate two Airbnb hosts — use ?account_id=

    Related: Authentication · IDs & External IDs · OAuth Connect

    AI