Authentication
HassleFreeBooks is a standard OAuth 2.1 authorization server: the authorization-code flow with PKCE, rotating refresh tokens, revocation and introspection. Any OAuth client library works; point it at the metadata document and it configures itself.
| Metadata (RFC 8414) | https://hasslefreebooks.com/.well-known/oauth-authorization-server |
|---|---|
| Authorize | GET https://hasslefreebooks.com/oauth/authorize |
| Token | POST https://hasslefreebooks.com/oauth/token |
| Revoke (RFC 7009) | POST https://hasslefreebooks.com/oauth/revoke |
| Introspect (RFC 7662) | POST https://hasslefreebooks.com/oauth/introspect |
| Grant types | authorization_code, refresh_token |
| PKCE | S256 only — required for public clients, recommended for all |
| Client authentication | client_secret_basic, client_secret_post, or none (public clients) |
The flow
- Make a PKCE pair and a state.
code_verifier: 43–128 random URL-safe characters.code_challenge= base64url(SHA-256(verifier)), no padding.state: a random value you tie to the user's session, to defeat CSRF. - Send the person to the consent screen.
https://hasslefreebooks.com/oauth/authorize?response_type=code &client_id=hfbc_… &redirect_uri=https%3A%2F%2Fyourapp.example%2Fcallback &scope=read%20write &state=af0ifjsldkj &code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM &code_challenge_method=S256They sign in if they need to, pick one of their businesses, see what you asked for, and press Allow.scopemay be omitted (you get every scope the app is registered for);redirect_urimay be omitted when the app has exactly one (then it may be left out of the token request too; if you send it at either step it must match exactly). - They come back to your redirect URI with
?code=…&state=…&iss=https://hasslefreebooks.com. Checkstatematches, and (RFC 9207) thatissis ours. On Cancel you get?error=access_deniedinstead. The code lives 10 minutes and works once. - Exchange the code from your server (or app, for public clients):
POST /oauth/token Authorization: Basic base64(client_id:client_secret) # public clients: send client_id in the body instead Content-Type: application/x-www-form-urlencoded grant_type=authorization_code&code=…&redirect_uri=https://yourapp.example/callback&code_verifier=…
{ "access_token": "hfbat_…", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "hfbrt_…", "scope": "read write", "entity_id": 42, "entity_name": "Acme Plumbing LLC" }entity_idis the business they connected; store it with the tokens. A JSON body is accepted as well as the form encoding. - Call the API with
Authorization: Bearer hfbat_….
Refreshing
Access tokens last an hour. Before or after one expires (a 401 INVALID_TOKEN), refresh:
POST /oauth/token
grant_type=refresh_token&refresh_token=hfbrt_… # + client authentication as above
# optional: &scope=read (narrow, never widen)
refresh_token and the one you sent
stops working. Save the new one before you use the new access token. If an old refresh token is presented again
more than 30 seconds after it was rotated, we assume it was stolen and end the whole connection (the customer has
to connect again). Two workers refreshing the same connection at once: the loser gets invalid_grant
within those 30 seconds — re-read the stored token and carry on. Serialize refreshes per connection.
Refresh tokens expire 90 days after they were issued; a connection used at least once a quarter never needs the customer again.
Errors from the token endpoints
These follow RFC 6749 §5.2, not the API envelope, so OAuth libraries understand them:
HTTP/1.1 400 Bad Request
{"error": "invalid_grant", "error_description": "The authorization code has expired. Start the connection again."}
| error | Means |
|---|---|
invalid_client (401) | Unknown client_id or wrong secret. |
invalid_grant | Code or refresh token unknown, expired, used, revoked, or the PKCE verifier / redirect_uri does not match. |
invalid_scope | A scope that does not exist, or that the app is not registered for. |
unauthorized_client | The app is paused (see its page in the console). |
unsupported_grant_type | Only authorization_code and refresh_token exist. |
slow_down (429) | Over 600 requests a minute from one address; honour Retry-After. |
Disconnecting
When a customer disconnects in your app, revoke the refresh token — that ends the whole connection on our side too:
POST /oauth/revoke token=hfbrt_…
Customers can also disconnect from Settings › Connected apps, and the owner of a business can end any connection to it. Your next call then gets 401; treat it as "disconnected" and ask them to connect again.
Public clients (mobile, desktop, single-page)
- Register the app as phone, desktop or browser. No secret is issued; never embed one.
- PKCE is mandatory. Send
client_idin the token request body. - Mobile: use a private-use scheme you own (
com.yourcompany.app:/oauth) or an https universal link, and open the consent screen in the system browser (ASWebAuthenticationSession, Custom Tabs), not a web view. - Desktop: register
http://127.0.0.1/callback; any port is accepted at run time (RFC 8252). - Single-page apps:
/oauth/tokenand/oauth/revokeanswer CORS preflights from any origin (no cookies are involved, so none are allowed), so the browser can exchange the code and refresh directly./oauth/introspectis for servers and is not available cross-origin.
API keys
For your own automations, create a key at /api-keys with the scopes it needs and send it the
same way: Authorization: Bearer hfb_…. A key acts as you across every business you belong to; pass
entity_id where an endpoint takes one, or it uses your active business. Keys are never what you give a
third party — that is what OAuth is for.