Docs
API guides

Authentication & API tokens

Bearer tokens, account scoping, and how to manage credentials for a multi-client integration.

Every V2 endpoint requires a bearer token. V1 endpoints require none. The Sites API uses its own per-website tokens, covered in Sites authentication; this page is about V2.

Authorization: Bearer 47|kJ2xQ8vN3mP1rT6yU9wZ4aB7cD0eF5gH8iJ2kL6m
Accept: application/json

The number before the | is the token's id — the same id you see when you list tokens.

The one thing to understand first: tokens are account-pinned

A Resi account is a client — a property management company or owner. A token belongs to a user and is pinned to one account. Every account-scoped request runs against that account unless you say otherwise, and the account decides which records exist as far as the request is concerned.

The practical consequences:

  • One account per request. GET /properties returns one account's properties. There is no cross-account query.
  • You can target another of the user's accounts. If the token's user belongs to more than one account, add the optional account_id parameter — in the query string, or in the JSON body on writes — to run a single request against another of them. It must be an account the user is a member of; any other account is a 403, and a value that is not a UUID is a 422.
  • Every account-scoped response names its account. The X-Resi-Account-Id response header carries the id of the account the request ran against. Log it.
  • A record from another account returns 404, not 403. Resi does not confirm the existence of data you cannot see.

account_id works on every account-scoped endpoint — properties, inventory, content, media, connections, users, and the rest. It does not apply to /me, /accounts, or the token endpoints, which are not account-scoped; the token endpoints take a required account_id of their own (see below).

Which account is my token pinned to? Make any account-scoped call without account_id — GET /properties?per_page=1 is cheap — and read X-Resi-Account-Id. GET /me lists the accounts the user can reach, but not the token's pin.

Vendors serving several clients

Usually a client issues you a token from their own account, so a vendor serving five clients holds five tokens. Key them by account in your own storage. Build for this on day one — retrofitting multi-tenancy into a single-token integration is painful.

If your user is a member of several client accounts, one token can reach all of them with account_id. Two things to weigh first: the rate limit is per user, so every client then shares one budget; and if the user loses access to the token's own account, the token stops working everywhere, including for the other accounts.

Getting your first token

Tokens are created in the Resi app under Account Settings → API Tokens by someone whose role in the account has permission to create API tokens. Choose Create Token, give it a name, and copy the token when it is shown — it is displayed once. The token is created for the signed-in user and pinned to the account you are in.

That is the bootstrap path for a new integration: POST /api/v2/tokens itself requires an existing token, so it cannot create your first one.

For vendors: ask your client to issue you a token from their account and send it through a secret-sharing channel — never email or Slack. You will need one per client.

Managing tokens programmatically

Once you hold a token, you can manage the tokens of any account your user belongs to. All three endpoints require the caller to be a member of the account and hold the matching API token permission (view, create or delete).

List tokens

GET /api/v2/tokens takes a required account_id and returns every token pinned to that account, newest first. The list is not paginated.

curl "https://v2.getresi.com/api/v2/tokens?account_id=$ACCOUNT_ID" \
  -H "Authorization: Bearer $RESI_TOKEN" \
  -H "Accept: application/json"
{
  "data": [
    {
      "id": 47,
      "name": "Syndication feed — production",
      "account_id": "019dd603-889a-73be-8975-58a30f17b4e8",
      "abilities": ["*"],
      "last_used_at": "2026-08-05T11:42:09.000000Z",
      "created_at": "2026-02-14T09:03:51.000000Z",
      "created_by": {
        "id": "9ecee553-ece2-4a9c-90c3-5484b5ccfefa",
        "name": "Integration Service Account",
        "email": "integrations@example.com"
      }
    }
  ]
}

last_used_at is the field to watch. It is how you find abandoned credentials, and it is how a client verifies your integration is actually running.

Account API tokens carry the * ability. The list also includes the tokens that the account's websites use for the Sites API; their abilities name the website (website:{id}:deliver or website:{id}:preview). Those tokens cannot call V2: any V2 request made with one is a 403. Revoking one cuts that website off from its content, so leave them alone unless that is what you mean to do.

Create a token

POST /api/v2/tokens takes a name (up to 255 characters) and the account_id to pin the token to. The new token belongs to the calling user.

curl -X POST https://v2.getresi.com/api/v2/tokens \
  -H "Authorization: Bearer $RESI_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Accept: application/json" \
  -d '{"name": "Syndication feed — production", "account_id": "019dd603-889a-73be-8975-58a30f17b4e8"}'
{
  "data": {
    "id": 47,
    "name": "Syndication feed — production",
    "account_id": "019dd603-889a-73be-8975-58a30f17b4e8",
    "plain_text_token": "47|kJ2xQ8vN3mP1rT6yU9wZ4aB7cD0eF5gH8iJ2kL6m"
  }
}

plain_text_token is returned once. It is stored hashed and cannot be retrieved again. If you lose it, revoke and re-issue.

Revoke a token

DELETE /api/v2/tokens/{tokenId} revokes any token pinned to an account where you hold the delete permission, including tokens other users created.

curl -X DELETE "https://v2.getresi.com/api/v2/tokens/$TOKEN_ID" \
  -H "Authorization: Bearer $RESI_TOKEN" \
  -H "Accept: application/json"

Returns 204. Revocation is immediate. An unknown token id is a 404.

Rotating credentials without downtime

Tokens do not expire, so rotate them on a schedule using an overlap window:

  1. Create the replacement token (POST /tokens) with a name carrying the date — "Syndication feed — 2026-08".
  2. Deploy it to your integration.
  3. Watch last_used_at on the old token until it stops advancing.
  4. Revoke the old token.

Do this on a schedule — quarterly is a reasonable default — and any time someone with access to the credential leaves the project.

Permissions and what a token can do

Two layers apply on every request:

  1. Account scope — nothing outside the account the request runs against is reachable.
  2. The user's role and permissions in that account — enforced per action. A token has exactly the permissions of the user who created it, in whichever account the request runs against. If that user loses permission to delete units, the token loses it too, without being reissued. If the user is removed from the token's account, the token stops working.

There are no read-only or per-resource token scopes: what a token can do is decided entirely by the issuing user's role. The account can also limit a user to specific groups; a token issued by that user sees only those groups' records.

Scope access through the issuing user. The role of the user who issues your token is how you keep an integration to least privilege. Two practices worth building in from the start:

  • Ask the client to issue your token from a purpose-made service user whose role covers only what your integration needs — for a read-only feed, a viewer-level user.
  • Treat the credential as high-value in your own systems: encrypted at rest, never in source control, never in client-side code.

Security practices for vendors

  • Server-side only. A V2 token in browser or mobile code is a published credential. If you need data in a browser, proxy it through your backend, or use the public V1 endpoints, which are designed for that.
  • One token per environment, named so a human can tell what it is from the list — "Acme ILS — staging" beats "api".
  • Log the account id, never the token, in your request logs. X-Resi-Account-Id gives you the id on every account-scoped response.
  • Handle 401 as terminal, not retryable. A 401 means revoked or invalid; retrying a revoked token in a loop is how integrations end up on a blocklist. Alert a human.

Errors you will see

StatusMeaningWhat to do
401Missing, malformed, or revoked tokenStop. Alert. Do not retry.
403The user lacks permission for this actionAsk the client to widen the issuing user's role
403account_id names an account the user does not belong toCheck the id, or ask for access to that account
403The user no longer has access to the token's own accountStop. The token is dead; ask the client for a new one
404Record does not exist, belongs to another account, or is outside the user's groupsVerify the id and X-Resi-Account-Id
422account_id is not a UUIDFix the value
429Rate limitedBack off — see Errors, rate limits & reliability

Last updated on

On this page