---
updatedAt: 2026-09-05T19:31:18.000Z
agentTools:
  projectIndex: https://developer.remote.com/llms.txt
---

# Authorization Code Flow

Let an existing Remote company grant your integration permission to manage their data on their behalf.

Use the Authorization Code Flow when a company already exists on Remote and you need their permission to act on their behalf. A company admin signs in, reviews what you are asking for, and approves. You receive tokens scoped to that company.

This is different from the [Client Credentials Flow](https://developer.remote.com/docs/client-credentials-flow-for-partners), which authenticates your integration as itself. Client credentials cannot read or write another company's data — only an admin's approval can grant that.

If instead you create the company yourself, see [Create a company with partner credentials](https://developer.remote.com/docs/getting-company-consent).

> :information\_source: **Sandbox vs production.** Every URL below uses `gateway.remote-sandbox.com`. For production, replace it with `gateway.remote.com`. Scope values are identical in both.

<Image src="https://files.readme.io/1c8f75c2baf2e498c551b7f44e795995a8380d571039116ce36448ec530afb8f-authorization_code_flow.png" align="center" />

## Before you begin

You need three things from Remote:

| Item                      | Notes                                                                      |
| :------------------------ | :------------------------------------------------------------------------- |
| `CLIENT_ID`               | Identifies your integration. Safe to include in URLs.                      |
| `CLIENT_SECRET`           | Never expose this in client-side code or a browser.                        |
| A registered redirect URI | Your callback URL, registered by Remote when your credentials are created. |

### Redirect URI matching is exact

Your redirect URI must match a registered value **character for character**. No wildcards, no prefix matching, no normalisation. These are all different values, and only one will work:

```
https://example.com/callback
https://example.com/callback/     (trailing slash)
http://example.com/callback       (scheme)
https://example.com:443/callback  (port)
```

If you need to add or change a redirect URI, ask Remote — you cannot self-serve it.

> :warning: **Local development.** Loopback URIs such as `http://localhost:3000/callback` cannot currently be registered for partner integrations. To test this flow you need a callback reachable over HTTPS.

## Step 1 — Send the admin to the authorization URL

```http
GET https://gateway.remote-sandbox.com/auth/oauth2/authorize
```

| Parameter      | Required | Description                                                                                                                                  |
| :------------- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------- |
| `client_id`    | Yes      | Your `CLIENT_ID`, URL-encoded.                                                                                                               |
| `redirect_uri` | Yes      | Must exactly match a registered redirect URI.                                                                                                |
| `state`        | Yes      | A random string of at least 8 characters, unique to this attempt.                                                                            |
| `scope`        | Yes      | Space-separated permissions. Request only the scopes you need — see [Scopes](https://developer.remote.com/docs/scopes). If omitted, your token is issued with full access. |

```
https://gateway.remote-sandbox.com/auth/oauth2/authorize
  ?client_id=YOUR_CLIENT_ID
  &redirect_uri=https%3A%2F%2Fexample.com%2Fcallback
  &state=c97b8fa15f7f8ba064b338779b8eecab
  &scope=employment%3Aread%20timeoff%3Aread
```

> :warning: **URL-encode your&#x20;**`client_id`**.** Client IDs can contain characters that are not URL-safe. A plus sign in particular must be written as `%2B` in the query string, or it will be read as a space and your request will fail. In the `Authorization` header later on, use the raw unencoded value.

### Use a fresh state on every attempt

`state` protects against cross-site request forgery: Remote echoes it back unchanged, and you should reject any callback whose `state` you did not generate.

It also identifies the attempt. If you reuse a `state` after the admin has already approved or denied it, you get `already_approved` or `already_denied` back and **no new code**. Generate a new random value each time.

## Step 2 — The admin signs in and approves

Remote handles this. If the admin is not signed in they are prompted to sign in first, then shown a consent screen naming your integration and the access you requested.

The signed-in user must be an **Owner** or **Admin** of the company. Anyone else sees an error instead of a consent screen.

## Step 3 — Handle the callback

On approval, Remote redirects the browser to your `redirect_uri` with `code` and `state` query parameters.

Before doing anything else, confirm `state` matches the value you generated.

The code is **valid for 5 minutes** and can be redeemed **once**. A second attempt with the same code fails with `invalid_grant` even if the five minutes have not elapsed.

## Step 4 — Exchange the code for tokens

```http
POST https://gateway.remote-sandbox.com/auth/oauth2/token
```

Authenticate with HTTP Basic using your client credentials. Send `grant_type=authorization_code` and `code` as form-encoded body parameters.

```bash
curl --request POST 'https://gateway.remote-sandbox.com/auth/oauth2/token' \
  --header 'Authorization: Basic BASE64_CLIENT_ID_AND_SECRET' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=authorization_code' \
  --data-urlencode 'code=YOUR_AUTHORIZATION_CODE'
```

> :warning: **How to encode in base64**
>
> ```typescript Bash
> $ echo -n "your_client_id:your_client_secret" | base64
> ```

Response:

```json
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "refresh_token": "84224550-dc8f-4153-a7cd-4f38c7ef90da",
  "expires_in": 3600,
  "token_type": "Bearer",
  "company_id": "3718b8ba-55d3-4fa6-ae45-91cd43b67997",
  "user_id": "e25c2e12-be43-4964-ac00-40ddfbd896c4"
}
```

Store the refresh token securely and keep the `company_id` — it identifies the company that just authorized you.

> :information\_source: **This is the step that links the company to your integration.** The company becomes available to your integration when you redeem the code, not when the admin clicks approve. If you never exchange the code, nothing is linked.

### Sending credentials in the body

Passing your credentials only in the request body is not supported. If you send `client_id` in the body **in addition to** the `Authorization` header, the two must match exactly, or the request fails with `401 invalid_client`.

## Step 5 — Call the API

Pass the access token as a bearer token. It acts on behalf of the approving admin, limited to the scopes you requested and to that single company.

```bash
curl 'https://gateway.remote-sandbox.com/v1/employments' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'
```

## Step 6 — Refresh the access token

Access tokens last **3600 seconds**. Refresh before expiry — every 55 minutes is a safe pattern. There is no limit on how many valid access tokens you may hold at once.

Send `grant_type=refresh_token` and your `refresh_token`, with the same Basic authentication. See [Refresh Token Flow](https://developer.remote.com/docs/refresh-token-flow).

Your refresh token **does not change** when you use it. The same value stays valid and is returned again, so treat it as a long-lived secret and store it as securely as your client secret. You do not need to persist a new one after each refresh.

## Choosing scopes

The `scope` parameter controls what your access token can do. Request only the scopes you need.

Scopes are enforced per endpoint. A token holding `employment:read` can list employments but cannot create one, and cannot touch payroll or expenses at all. See [Scopes](https://developer.remote.com/docs/scopes) for the values you can request and how they are evaluated.

> :warning: **Omitting&#x20;**`scope`**&#x20;grants full access.** If you leave `scope` out, your token is issued with `all:write` — permission to do everything the approving admin can do. Always request explicit scopes.

### Deprecated: company.manage

The scope `https://gateway.remote.com/company.manage` is **deprecated**. It grants full read and write access to everything the admin can do. Do not use it for new integrations — request the specific scopes your integration needs instead.

## Errors during authorization

If something goes wrong before a code is issued, Remote redirects with `error` and `error_description` instead of `code`. Your `state` is always echoed back.

| `error`                               | What it means                                                                               | What to do                                                                |
| :------------------------------------ | :------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------ |
| `invalid_request`                     | A parameter is missing or invalid. **This includes a redirect URI that is not registered.** | Check every parameter, especially that your redirect URI matches exactly. |
| `unauthorized_client`                 | Your client ID is unknown, or your integration is not enabled.                              | Confirm your credentials with Remote.                                     |
| `invalid_scope`                       | You requested a scope the approving user cannot grant.                                      | Check [Scopes](https://developer.remote.com/docs/scopes).                                               |
| `access_denied`                       | The user declined, or lacks a role permitted to approve.                                    | Have an Owner or Admin approve.                                           |
| `already_approved` / `already_denied` | This `state` value was already actioned.                                                    | Start again with a new random `state`.                                    |

> :warning: **A bad redirect URI reports to your registered URL.** If the redirect URI you send is not registered, Remote will not redirect to it — doing so would make Remote an open redirector. The error is delivered to your first registered redirect URI instead.
> So if you mistype your callback, watching that URL shows you nothing at all. Check your registered URL for an `invalid_request` error.

## Troubleshooting

`403 Forbidden`**&#x20;on an API call.** Your token is valid but lacks the scope for that endpoint. The response tells you exactly what would have worked:

```json
{
  "message": "Forbidden, missing required scope (one of): https://gateway.remote.com/company.manage, timeoff:write, time_and_attendance, all:write got: employment:read, timeoff:read"
}
```

Any value in the `one of` list satisfies the endpoint. Re-run the flow requesting one of them.

`401 invalid_client`**&#x20;on the token request.** Either your Basic credentials are wrong, or you sent a `client_id` in the body that does not match the header.

`invalid_grant`**&#x20;on the token request.** The code was already used, has expired after 5 minutes, or belongs to a different client.