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
AuthorizeGET https://hasslefreebooks.com/oauth/authorize
TokenPOST https://hasslefreebooks.com/oauth/token
Revoke (RFC 7009)POST https://hasslefreebooks.com/oauth/revoke
Introspect (RFC 7662)POST https://hasslefreebooks.com/oauth/introspect
Grant typesauthorization_code, refresh_token
PKCES256 only — required for public clients, recommended for all
Client authenticationclient_secret_basic, client_secret_post, or none (public clients)

The flow

  1. 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.
  2. 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=S256
    They sign in if they need to, pick one of their businesses, see what you asked for, and press Allow. scope may be omitted (you get every scope the app is registered for); redirect_uri may 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).
  3. They come back to your redirect URI with ?code=…&state=…&iss=https://hasslefreebooks.com. Check state matches, and (RFC 9207) that iss is ours. On Cancel you get ?error=access_denied instead. The code lives 10 minutes and works once.
  4. 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_id is the business they connected; store it with the tokens. A JSON body is accepted as well as the form encoding.
  5. 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 tokens rotate. Every refresh returns a new 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."}
errorMeans
invalid_client (401)Unknown client_id or wrong secret.
invalid_grantCode or refresh token unknown, expired, used, revoked, or the PKCE verifier / redirect_uri does not match.
invalid_scopeA scope that does not exist, or that the app is not registered for.
unauthorized_clientThe app is paused (see its page in the console).
unsupported_grant_typeOnly 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_id in 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/token and /oauth/revoke answer CORS preflights from any origin (no cookies are involved, so none are allowed), so the browser can exchange the code and refresh directly. /oauth/introspect is 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.