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.
| What | Where |
|---|---|
| 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:
- 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.
- 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_idonly, 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:
- 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. - 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:
- Invite or create two or three employees.
- Add a customer and a project, if your app uses job costing.
- Clock some time in, and clock it out, so
GET /time-recordshas 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
| Parameter | Required | Notes |
|---|---|---|
client_id | Yes | From the app detail page |
redirect_uri | Yes | Must exactly match one registered on the app |
response_type | Yes | Always code |
scope | Yes | Space-separated; see the scope table below |
state | Recommended | Returned untouched on the callback; use it for CSRF protection |
code_challenge | Yes | Base64url SHA-256 of your verifier |
code_challenge_method | Yes | Always S256 |
Available scopes
| Scope | Grants |
|---|---|
users:read | View employees, departments, workplaces, and company details |
users:write | Create and manage employees, departments, and workplaces |
timetracking:read | View time records, customers, and projects |
timetracking:write | Create and manage time records, customers, and projects |
pto:read | View time-off policies, balances, and requests |
pto:write | Create and manage time-off requests |
payroll:read | View payrolls, payroll reports, and pay schedules |
payroll:write | Create 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_idis unknown or theredirect_uriis 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 yourredirect_uriwitherroranderror_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-secretEvery 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:
- Switch the portal to Production and add your production redirect URIs (
httpsonly). - Generate a production client secret. Sandbox credentials do not work against production.
- 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.
- After approval, point your app at
https://api.fridayapp.comand real customers can authorize it.
Common Pitfalls
- Expecting demo data — A new test company contains only its owner. If
GET /employeesreturns one row, that is correct; add data through the dashboard. - Redirect URI mismatch — Matching is exact.
http://localhost:6789/callbackandhttp://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.comfails. - Assuming the granted scope equals the requested scope — Read
scopefrom 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
| Step | Where | Action |
|---|---|---|
| 1 | Developer Portal → Apps | Register the app and answer the review questionnaire |
| 2 | App detail page (Sandbox) | Add redirect URIs, generate a client secret |
| 3 | Apps page (Sandbox) | Create your sandbox test company |
| 4 | Sandbox Friday dashboard | Add employees, projects, and time |
| 5 | Your app | Redirect to GET /oauth/authorize with PKCE |
| 6 | Consent screen | Approve as the test company's owner |
| 7 | Your backend | POST /oauth/token with the code and verifier |
| 8 | Your backend | Call /partner/v1/* with Authorization: Bearer |
| Endpoint | Method | Purpose |
|---|---|---|
/oauth/authorize | GET | Start the flow; redirects to the consent screen |
/oauth/token | POST | Exchange a code, or refresh an access token |
/oauth/revoke | POST | Revoke a refresh token and its connection |
What's Next
- Authorizing with OAuth — The full reference behind this walkthrough: endpoint parameters, the scope-to-endpoint map, token and connection lifecycle, and every error to handle.
- Friday API Overview — Base URLs, pagination, errors, rate limits, and the full resource list.
- The Employee Object — Employee fields and employment types.
- Timetracking — Time records, clock-in/out, breaks, and reports.
- Subscribing to Webhooks — Receive events instead of polling.
Updated 2 days ago
