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: truemeans every connected account is stale — nothing in the response is current. - Top-level
stale: falsewithreason: "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 alongsidestale: 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
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
| Area | Key alone | X-Account-Id |
|---|---|---|
Connect setup (POST /v1/connect/{provider}), key & webhook management | Sufficient | Not used |
Inventory (/v1/properties, /v1/listings) & reservations | Sufficient | Optional — pin a connection |
| Per-channel reads & writes (pricing, availability, content) | Sufficient | Optional — pin a connection |
Airbnb collections (/v1/channels/airbnb/listings, reservations, messaging, reviews, transactions, alterations) | Sufficient — returns every connected account | Cannot separate two Airbnb hosts — use ?account_id= |
Related: Authentication · IDs & External IDs · OAuth Connect