Authentication
AccountFUN’s public API uses the OAuth 2.0 Authorization Code grant (RFC 6749 §4.1, hardened per RFC 9700). This is the standard three-legged flow used by server-side applications that act on behalf of a user.
The flow has three legs:
- Authorize — redirect the user to AccountFUN, where they sign in and approve your app.
- Token exchange — trade the short-lived authorization code for an access token (and optional refresh token) at your server.
- Resource calls — send the access token with every API request.
Endpoints
Section titled “Endpoints”| Endpoint | Method | Purpose |
|---|---|---|
/api/v1/oauth/authorize | GET, POST | Authorization-code grant entry. GET renders the consent screen; POST is the Allow/Deny action. |
/api/v1/oauth/token | POST | Exchange a code for tokens, or rotate a refresh token. |
/api/v1/oauth/revoke | POST | Revoke an access or refresh token (RFC 7009). |
All token-endpoint requests must use
Content-Type: application/x-www-form-urlencoded.
Clients that discover endpoints automatically can read the authorization
server metadata (RFC 8414) at /.well-known/oauth-authorization-server. This
is how AI assistants find them. If an authorization request
leaves out scope, the client’s registered scopes are requested.
Step 1 — Redirect the user to authorize
Section titled “Step 1 — Redirect the user to authorize”Direct the user’s browser to the authorize endpoint with your client_id, the
redirect_uri you registered, the scope you need, and a random state
value:
GET https://app.accountfun.com/api/v1/oauth/authorize \ ?response_type=code \ &client_id=YOUR_CLIENT_ID \ &redirect_uri=https://your-app.example.com/callback \ &scope=invoices:read%20invoices:write%20offline_access \ &state=RANDOM_UNGUESSABLE_STRING| Parameter | Required | Description |
|---|---|---|
response_type | Yes | Must be code. |
client_id | Yes | Your client identifier. |
redirect_uri | Yes | Must exactly match a URI registered on your client. HTTPS is required in production (http://localhost is allowed for development). |
scope | No | Space-delimited list of scopes. Scopes your client isn’t registered for are ignored; if none of the requested scopes is registered, the request fails. Omit it to request all of your client’s registered scopes. |
state | Yes | An unguessable value you generate. It is echoed back in the redirect and must be verified to prevent CSRF. |
code_challenge | Yes | PKCE challenge — base64url(SHA-256(code_verifier)), 43 characters (see PKCE). |
code_challenge_method | Yes | Must be S256. plain is not accepted. |
What the user sees
Section titled “What the user sees”The user signs in to AccountFUN (if they aren’t already), selects which of their organizations to connect, and reviews the requested scopes on a consent screen. They click Allow or Deny.
- Approving is opt-in.
bills:approveandinvoices:approveare listed under Only if you tick it: as unticked checkboxes. If the user doesn’t tick one, it’s left out of the grant. - The user’s role sets the limit. The list follows the selected organization: access the user’s role there can’t grant is shown as “(not available with your role in this organization)” and left out of the grant, and the connection works with the rest. If the role can grant none of the requested access, the connection is refused.
- Only the creator’s organizations. The picker offers only organizations that the person who registered the app (under Settings → API & Developers) belongs to. Members of other organizations see ”… was set up in another organization, so it can’t be connected here” and should ask their own admin to register the app.
- The screen tells the user how to take the access back later: they can disconnect the app under Settings → Account → Connected Apps, and an admin of the organization can revoke it under Settings → Authorized Applications.
The scope in the token response lists exactly what was
granted, which can be less than you asked for.
Request errors
Section titled “Request errors”If something is wrong with the request — response_type isn’t code,
state or a valid PKCE code_challenge is missing, the redirect_uri doesn’t
match a registered URI, or none of the requested scopes is registered on your
client — AccountFUN shows an error page and does not redirect back to your
app. The browser is sent to your redirect_uri only after the user clicks
Allow or Deny, and never to an unregistered URI.
Requested scopes that your client isn’t registered for are ignored, not treated as an error.
User denies
Section titled “User denies”Clicking Deny is the only case that redirects back to you with an error. You receive:
GET https://your-app.example.com/callback?error=access_denied&state=RANDOM_UNGUESSABLE_STRINGHandle this gracefully — show the user a “try again later” message.
Step 2 — Receive the authorization code
Section titled “Step 2 — Receive the authorization code”On Allow, AccountFUN redirects to your redirect_uri with a
single-use code:
GET https://your-app.example.com/callback?code=AUTHORIZATION_CODE&state=RANDOM_UNGUESSABLE_STRINGBefore exchanging the code, verify that state matches what you sent in
step 1. If it doesn’t, discard the request.
The authorization code is:
- Single-use — it can be exchanged exactly once.
- Short-lived — it expires after 60 seconds.
- Bound to your
client_idandredirect_uri— both must match at exchange time.
Exchange it immediately. Do not store it.
Step 3 — Exchange the code for tokens
Section titled “Step 3 — Exchange the code for tokens”From your server, POST to the token endpoint:
curl -X POST https://app.accountfun.com/api/v1/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=authorization_code \ -d code=AUTHORIZATION_CODE \ -d redirect_uri=https://your-app.example.com/callback \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRETYou may authenticate the client with HTTP Basic instead of the form body (RFC 6749 §2.3.1), but you must not mix both methods in one request:
Authorization: Basic base64(YOUR_CLIENT_ID:YOUR_CLIENT_SECRET)Success response
Section titled “Success response”{ "access_token": "ab12cd34...64-hex-chars", "token_type": "Bearer", "expires_in": 1800, "scope": "invoices:read invoices:write offline_access", "organization_id": "00000000-0000-0000-0000-000000000000", "refresh_token": "ef34ab12...64-hex-chars"}| Field | Description |
|---|---|
access_token | The opaque bearer token. Send it in the Authorization header on every API call. |
token_type | Always Bearer. |
expires_in | Seconds until the access token expires (1800 = 30 minutes). |
scope | The scopes granted to this token (space-delimited). This is exactly what the user granted, which can be less than you requested: an approve scope they didn’t tick, or access their role can’t grant. Check it before using a feature that needs a scope. |
organization_id | The organization the user selected at consent. All API calls are scoped here. |
refresh_token | Present only if offline_access was granted. Use it to mint new token pairs without re-prompting the user. |
Calling the API
Section titled “Calling the API”Once you have an access token, call any resource endpoint:
curl https://app.accountfun.com/api/v1/invoices \ -H "Authorization: Bearer ab12cd34...64-hex-chars"Every request is automatically scoped to the organization_id the token was
issued for. You cannot read or write data in any other organization with that
token — the boundary is enforced server-side on every query.
Refreshing tokens
Section titled “Refreshing tokens”Access tokens expire after 30 minutes. When the offline_access scope was
granted, you also received a refresh token. Use it to mint a new access +
refresh pair before the old access token expires:
curl -X POST https://app.accountfun.com/api/v1/oauth/token \ -H "Content-Type: application/x-www-form-urlencoded" \ -d grant_type=refresh_token \ -d refresh_token=CURRENT_REFRESH_TOKEN \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRETThe response has the same shape as the authorization-code response. The new refresh token replaces the old one — keep only the latest.
Refresh-token rotation
Section titled “Refresh-token rotation”AccountFUN rotates the refresh token on every use:
- The presented refresh token is revoked immediately.
- A new refresh token is returned, chained to the old one.
- Each successful rotation resets the 30-day inactivity window. There is no absolute lifetime cap; a refresh token expires after 30 days without use.
Reuse detection
Section titled “Reuse detection”If an already-rotated refresh token is presented again, AccountFUN treats this
as a theft signal and revokes that token’s family — the rotation chain it
belongs to and every access token minted along it. The next call with any
refresh token from that chain returns invalid_grant; resource requests using
an access token from it return 401 invalid_token.
A family is one authorization: the tokens issued when a code was exchanged, plus everything rotated from them. If the same user has authorized your app more than once (for example from two devices), each authorization is its own family, and a reuse in one leaves the others working. The consent itself (the grant) is not revoked either — the user does not have to consent again, your app only has to run the authorization flow again to get a new family.
This means: always discard the old refresh token the moment you’ve used it. If two parts of your system race and both try to rotate the same token, one will succeed and the other will trigger family invalidation. To avoid this, serialize refreshes per-session in your app.
Refresh-token lifetimes
Section titled “Refresh-token lifetimes”| Property | Value |
|---|---|
| Lifetime | Rolling 30 days of inactivity; no absolute cap |
| Rotation | On every use (old token revoked, new token issued) |
PKCE (required)
Section titled “PKCE (required)”PKCE (Proof Key for Code Exchange, RFC 7636) binds the authorization code
to the client instance that started the flow, so a leaked or injected code is
useless to anyone else. AccountFUN requires PKCE with the S256 method on
every authorization request, in line with the OAuth 2.0 Security Best
Current Practice (RFC 9700). This applies even though AccountFUN clients are
confidential (they authenticate with a secret).
- Generate a high-entropy
code_verifier: 43–128 characters fromA–Z a–z 0–9 - . _ ~. - Derive the
code_challenge:base64url(SHA-256(code_verifier))with no padding (always 43 characters). - Send
code_challenge+code_challenge_method=S256in the authorize request. - Send the original
code_verifierin the token exchange.
# Authorize&code_challenge=BASE64URL_SHA256_OF_VERIFIER&code_challenge_method=S256
# Token exchange&code_verifier=ORIGINAL_PLAIN_VERIFIERAn authorize request without a valid S256 challenge is rejected with an
AccountFUN error page; the user isn’t sent back to your app. The token
endpoint verifies the code_verifier against the stored challenge; a missing
or mismatched verifier returns invalid_grant.
Revoking tokens
Section titled “Revoking tokens”To revoke an access or refresh token (e.g. when a user disconnects your app), call the revocation endpoint (RFC 7009):
curl -X POST https://app.accountfun.com/api/v1/oauth/revoke \ -H "Content-Type: application/x-www-form-urlencoded" \ -d token=TOKEN_TO_REVOKE \ -d client_id=YOUR_CLIENT_ID \ -d client_secret=YOUR_CLIENT_SECRET| Parameter | Required | Description |
|---|---|---|
token | No | The access or refresh token to revoke. |
token_type_hint | No | access_token or refresh_token (a hint only — both tables are checked). |
client_id | Yes | Your client identifier. |
client_secret | Yes | Your client secret. |
The endpoint always returns 200 OK, even if the token was unknown,
already revoked, or belonged to a different client — this prevents leaking
token validity. A 401 with { "error": "invalid_client" } is returned only
when your own client credentials are wrong, and a 429 (with Retry-After)
when you exceed the rate limits — the same bodies
the token endpoint sends.
You may revoke only tokens that belong to your client.
Revoking from the user side
Section titled “Revoking from the user side”Inside AccountFUN, a user can disconnect your app at any time under
Settings → Account → Connected Apps, and an admin of the organization can
revoke any member’s connection under Settings → Authorized Applications;
the consent screen tells users both. Either one immediately invalidates the
consent grant and all derived access and refresh tokens — the next API call
returns 401 invalid_token.
AccountFUN also revokes every connection a user has made when their account
may have been at risk: a password reset or change, two-factor authentication
turned off with a recovery code or reset by AccountFUN support, and “sign out
other sessions”. Your refresh then fails with invalid_grant: send the user
through authorization again.
The role ceiling
Section titled “The role ceiling”A token’s authority is the intersection of two things:
- The scopes the token carries (what the user consented to).
- The user’s live role in the organization (re-checked on every request).
A token minted when the user was an admin becomes inert the instant the user is demoted or removed from the org — the token’s scope string hasn’t changed, but the live role no longer permits the action. This is enforced on every single API request; there is no staleness window.
Every resource scope maps to a permission the authorizing user’s role must actually hold — whatever the role is called. With custom roles, that means the specifics of the role, not its name, decide what a token can do: a custom role with the right permissions can use the API even though it isn’t a seeded role, and a role without them cannot use the API at all.
The same check runs at consent: access the user’s role can’t grant in the
selected organization is left out of the grant. bills:approve and
invoices:approve need the Approve Documents permission, and each
approves only its own document type — invoices:approve can’t approve a bill.
Client authentication methods
Section titled “Client authentication methods”The token and revoke endpoints accept client credentials in two ways:
| Method | How |
|---|---|
| Form body | client_id + client_secret in the URL-encoded body. |
| HTTP Basic | Authorization: Basic base64(client_id:client_secret). |
Pick one per request. Mixing both is rejected.
Security checklist
Section titled “Security checklist”When building your integration:
- Store the
client_secreton your server only. Never ship it in a mobile app, single-page app, or any code that runs in the user’s browser. - Verify
stateon every callback. This is your CSRF defense. - Read the granted
scopein the token response. Don’t assume the user granted everything you asked for — approving, for one, is opt-in. - Store tokens encrypted at rest. Rotate your storage key periodically.
- Discard old refresh tokens immediately after rotation. Never keep a stash — reuse triggers family invalidation.
- Use HTTPS everywhere. Redirect URIs must be HTTPS in production.
- Handle
401 invalid_tokengracefully by re-prompting authorization. - Never put tokens in URLs (query strings, fragments). Use the
Authorizationheader.