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, 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.
ℹ️ Sandbox vs production. Every URL below uses
gateway.remote-sandbox.com. For production, replace it withgateway.remote.com. Scope values are identical in both.

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.
⚠️ Local development. Loopback URIs such as
http://localhost:3000/callbackcannot 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
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. 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
⚠️ URL-encode your
client_id. Client IDs can contain characters that are not URL-safe. A plus sign in particular must be written as%2Bin the query string, or it will be read as a space and your request will fail. In theAuthorizationheader 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
POST https://gateway.remote-sandbox.com/auth/oauth2/tokenAuthenticate with HTTP Basic using your client credentials. Send grant_type=authorization_code and code as form-encoded body parameters.
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'Response:
{
"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.
ℹ️ 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.
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.
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 for the values you can request and how they are evaluated.
⚠️ Omitting
scopegrants full access. If you leavescopeout, your token is issued withall: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. |
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. |
⚠️ 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 aninvalid_requesterror.
Troubleshooting
403 Forbidden on an API call. Your token is valid but lacks the scope for that endpoint. The response tells you exactly what would have worked:
{
"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 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 on the token request. The code was already used, has expired after 5 minutes, or belongs to a different client.
Updated about 8 hours ago