Building an OAuth App in Sandbox

This guide walks the full build loop for an OAuth app, end to end in sandbox: register the app, create a test company you own, put some real data in it, run the authorization flow against it exactly as a customer would, and call the Friday API with the resulting access token. Everything here happens in sandbox, so you can complete the whole loop before you have a single customer and before your app is reviewed for production.

Before You Start

You need a Developer Portal account. The portal has one login for both environments; use the environment switcher to select Sandbox before you begin.

WhatWhere
Developer Portal (sandbox)https://developer.friday-staging.com
Authorization server (sandbox)https://node.friday-staging.com/oauth
Friday API (sandbox)https://node.friday-staging.com/partner/v1

You will also need somewhere for the authorization server to redirect back to. During development that can be a local URL — sandbox accepts http://localhost redirect URIs. Production requires https.

OAuth or an API key?

Use OAuth if your app connects to many Friday companies and each customer authorizes it themselves. If you are building an internal integration against your own company, an API key is simpler — see Getting API Access.

The two models are separate. An OAuth app's test company is yours alone and never appears on the API Keys page, and no API key can be generated against it.


The Loop

┌──────────────────────────────────────────────┐
│  1. Register your app                        │  ← Developer Portal → Apps
└──────────────────┬───────────────────────────┘
                   ▼
┌──────────────────────────────────────────────┐
│  2. Generate a sandbox client secret         │  ← App detail page
└──────────────────┬───────────────────────────┘
                   ▼
┌──────────────────────────────────────────────┐
│  3. Create your sandbox test company         │  ← Apps page (sandbox only)
└──────────────────┬───────────────────────────┘
                   ▼
┌──────────────────────────────────────────────┐
│  4. Add employees and time to it             │  ← Sandbox Friday dashboard
└──────────────────┬───────────────────────────┘
                   ▼
┌──────────────────────────────────────────────┐
│  5. Send the user to /oauth/authorize        │  ← Your app, with PKCE
└──────────────────┬───────────────────────────┘
                   ▼
┌──────────────────────────────────────────────┐
│  6. Approve on the consent screen            │  ← As the test company's owner
└──────────────────┬───────────────────────────┘
                   ▼
┌──────────────────────────────────────────────┐
│  7. Exchange the code for tokens             │  ← POST /oauth/token
└──────────────────┬───────────────────────────┘
                   ▼
┌──────────────────────────────────────────────┐
│  8. Call the Friday API with the token       │  ← Authorization: Bearer
└──────────────────────────────────────────────┘

Step 1 — Register Your App

In the Developer Portal, go to Apps and click Register App. Registration is two steps:

  1. How customers see your app — name, logo, website, and support contact. These appear on the authorization screen your customers will see, so use real values.
  2. Review details — a questionnaire covering what your integration does, which permissions it needs and why, where Friday data is stored, and what happens to it when a customer disconnects. These answers go to the Friday review team, not to customers, and they are required to register.

You are answering the review questionnaire up front, but you are not submitting for review yet. The app is created as a draft, and you can build against sandbox indefinitely in that state.

No client secret is issued at registration. Secrets are per-environment and generated on demand, because a secret minted at registration would be the production one — unusable until your app is approved, and never valid against sandbox. Registration hands you a client_id only, plus a Set up credentials link to the next step.


Step 2 — Generate a Sandbox Client Secret

Follow Set up credentials, or open the app from the Apps list later. With the environment switcher on Sandbox:

  1. Add your redirect URI (for example http://localhost:6789/callback). Redirect URIs are matched exactly, including scheme, port, and path, so register the precise URL your app will use.
  2. Click Generate client secret.

The secret is shown once:

Client secret generated. Copy it now -- it will not be shown again.

Copy it and store it somewhere your app can read it. If you lose it, use Rotate secret on the same page to issue a new one — which immediately invalidates the old one.

Sandbox and production secrets are independent. Generating one does not affect the other.


Step 3 — Create Your Sandbox Test Company

An OAuth app authorizes against a company, so you need a company to authorize against. On the Apps page in sandbox there is a Sandbox test company card. Click Create sandbox test company.

This gives you a company you own, on the premium plan so no plan gate hides part of the API surface while you are testing. It is created once — pressing the button again returns the same company rather than making another one.

Your test company starts with exactly one employee: its owner, "Sandbox Admin". There is no seeded demo data — no time records, projects, customers, or payrolls. You create what your integration needs in the next step, which means the data you test against is data you actually understand.


Step 4 — Add Employees and Time

On the same card, click Open sandbox dashboard. This signs you in to the sandbox Friday dashboard as your test company's owner, in a new tab — the same interface your customers use.

Add whatever your integration reads. For a time-tracking app that usually means:

  1. Invite or create two or three employees.
  2. Add a customer and a project, if your app uses job costing.
  3. Clock some time in, and clock it out, so GET /time-records has something to return.

Because you created this data through the dashboard, it went through the same write paths a real customer's data does — so what your app reads back is shaped exactly like production.

Keep this tab open. You are now signed in as the test company's owner, which is precisely who needs to approve your app in Step 6.


Step 5 — Send the User to the Authorization Endpoint

Friday uses the authorization code flow with PKCE. Before redirecting, generate a random code_verifier and derive its challenge:

code_verifier  = 43–128 random characters
code_challenge = BASE64URL( SHA256( code_verifier ) )   // no padding

Keep the verifier; you will need it in Step 7. Then redirect the user to:

GET https://node.friday-staging.com/oauth/authorize
  ?client_id=feb2bc57-0157-4b6a-b155-fda37de26ef4
  &redirect_uri=http://localhost:6789/callback
  &response_type=code
  &scope=timetracking:read%20users:read
  &state=nonce-you-generated
  &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM
  &code_challenge_method=S256
ParameterRequiredNotes
client_idYesFrom the app detail page
redirect_uriYesMust exactly match one registered on the app
response_typeYesAlways code
scopeYesSpace-separated; see the scope table below
stateRecommendedReturned untouched on the callback; use it for CSRF protection
code_challengeYesBase64url SHA-256 of your verifier
code_challenge_methodYesAlways S256

Available scopes

ScopeGrants
users:readView employees, departments, workplaces, and company details
users:writeCreate and manage employees, departments, and workplaces
timetracking:readView time records, customers, and projects
timetracking:writeCreate and manage time records, customers, and projects
pto:readView time-off policies, balances, and requests
pto:writeCreate and manage time-off requests
payroll:readView payrolls, payroll reports, and pay schedules
payroll:writeCreate and manage payrolls and pay schedules

Request only the scopes you use. Customers see every scope you ask for on the consent screen, and unexplained requests cost you approvals.

If the client_id is unknown or the redirect_uri is not registered, Friday shows an error page rather than redirecting. This is deliberate: an unverified redirect target must never receive a redirect, even an error one. Every other failure redirects back to your redirect_uri with error and error_description.


Step 6 — Approve on the Consent Screen

The authorization endpoint redirects to Friday's consent screen, which shows your app's name, logo, and support contact, the permissions you asked for, and a picker of the companies the signed-in user can authorize.

Because you are already signed in as your test company's owner from Step 4, you land straight on the consent screen with your test company selected. Approve it.

Only company owners and admins can authorize an app. If you open the flow in a browser that is not signed in to Friday, you will be asked to log in first.

Sandbox skips app review; production does not. A draft app authorizes normally in sandbox. On production the same request is refused until the app is approved, so plan to submit for review before you launch.

Approving redirects the browser to your redirect_uri with a one-time code:

http://localhost:6789/callback?code=Ax7...&state=nonce-you-generated

Check that state matches what you sent before doing anything with the code. The code expires in 5 minutes and can be exchanged once.


Step 7 — Exchange the Code for Tokens

Exchange happens server-side, from your backend, using your client secret and the verifier from Step 5:

curl -X POST https://node.friday-staging.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=authorization_code \
  -d code=Ax7... \
  -d redirect_uri=http://localhost:6789/callback \
  -d client_id=feb2bc57-0157-4b6a-b155-fda37de26ef4 \
  -d client_secret=your-sandbox-secret \
  -d code_verifier=your-original-verifier
{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5...",
  "token_type": "Bearer",
  "expires_in": 3599,
  "refresh_token": "113a659a0bfac07cd508cbb0...",
  "scope": "timetracking:read users:read"
}

Note the scope in the response — it is what was actually granted, which may be narrower than what you requested. Read it rather than assuming you got everything you asked for.

Refreshing

Access tokens last about an hour. Refresh with:

curl -X POST https://node.friday-staging.com/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d grant_type=refresh_token \
  -d refresh_token=113a659a0bfac07cd508cbb0... \
  -d client_id=feb2bc57-0157-4b6a-b155-fda37de26ef4 \
  -d client_secret=your-sandbox-secret

Every refresh rotates the refresh token. The response contains a new one and the old one stops working, so persist the new value immediately. If a previously used refresh token is presented again, Friday treats it as a stolen credential and revokes the entire connection — the customer has to reauthorize. Refresh from one place in your code, not from several concurrent workers.


Step 8 — Call the Friday API

Pass the access token as a Bearer token. There is no company ID in the URL; the token already identifies the company that authorized you.

curl https://node.friday-staging.com/partner/v1/employees \
  -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5..."

On a company you have just provisioned, that returns exactly what you put there:

{
  "data": [
    {
      "employee_id": 213,
      "first_name": "Sandbox",
      "last_name": "Admin",
      "email": "[email protected]",
      "status": "active",
      "employment_type": "admin",
      "timetrack_only": false
    }
  ],
  "pagination": { "page": 1, "per_page": 25, "total_count": 1, "total_pages": 1 }
}

Calling an endpoint outside your granted scopes returns 403 and names the missing scope:

{
  "error": "Your connection to this company does not include the 'payroll:read' permission. The company can grant it by re-approving your application with that permission included."
}

To add a scope, send the customer through the authorization flow again with the wider scope. The existing connection is updated in place rather than duplicated.

Connecting more companies

Repeat Steps 5 through 8 for each customer. One app holds a separate connection per company, each with its own tokens. Your test company is one connection like any other — you can hold it alongside real customer connections.


After the Loop: Going to Production

Once the loop works in sandbox:

  1. Switch the portal to Production and add your production redirect URIs (https only).
  2. Generate a production client secret. Sandbox credentials do not work against production.
  3. Submit your app for review from the app detail page. Your questionnaire answers from Step 1 are prefilled; make sure the demo URL shows the working integration, because that is what the review team looks at first.
  4. After approval, point your app at https://api.fridayapp.com and real customers can authorize it.

Common Pitfalls

  • Expecting demo data — A new test company contains only its owner. If GET /employees returns one row, that is correct; add data through the dashboard.
  • Redirect URI mismatch — Matching is exact. http://localhost:6789/callback and http://localhost:6789/callback/ are different URIs, and an unregistered one produces an error page instead of a redirect.
  • Reusing a refresh token — Rotation means the old token is dead the moment you refresh. Replaying one revokes the whole connection.
  • Storing the client secret in your frontend — The code exchange is a server-side call. PKCE protects the code in the browser; the secret must never get there.
  • Mixing environments — Sandbox and production have separate client secrets, separate redirect URIs, and separate connections. A sandbox secret against api.fridayapp.com fails.
  • Assuming the granted scope equals the requested scope — Read scope from the token response.
  • Testing only in sandbox — Sandbox skips app review. Your first production authorization will fail if the app has not been approved.

Quick Reference

StepWhereAction
1Developer Portal → AppsRegister the app and answer the review questionnaire
2App detail page (Sandbox)Add redirect URIs, generate a client secret
3Apps page (Sandbox)Create your sandbox test company
4Sandbox Friday dashboardAdd employees, projects, and time
5Your appRedirect to GET /oauth/authorize with PKCE
6Consent screenApprove as the test company's owner
7Your backendPOST /oauth/token with the code and verifier
8Your backendCall /partner/v1/* with Authorization: Bearer
EndpointMethodPurpose
/oauth/authorizeGETStart the flow; redirects to the consent screen
/oauth/tokenPOSTExchange a code, or refresh an access token
/oauth/revokePOSTRevoke a refresh token and its connection

What's Next


Did this page help you?