OAuth 2.0
If you're building a product that ANY TattooBooking tenant could sign up to connect (a SaaS marketplace integration, a multi-tenant CRM, a Zapier-style platform), use OAuth 2.0 to let users authorize your app without ever sharing an API key with you.
Conformance: RFC 6749 (core), RFC 7636 (PKCE — required), RFC 7009 (revocation), RFC 7662 (introspection — first-party only), RFC 8414 (server metadata).
1. Register your app
OAuth apps are registered by TattooBooking super-admins today. To register yours, email developers@tattoobooking.com with:
- App name + description + homepage URL + logo URL
- List of redirect URIs your app uses (exact-match strings; subdomain wildcards not supported)
- Requested scopes — be minimal, ask for only what you need
You'll receive:
client_id— public, starts withtboa_client_secret— keep secret, starts withtbosk_
2. Direct users to authorize
https://api.tattoobooking.com/oauth/authorize
?response_type=code
&client_id=tboa_xxxxxxxxxxxx
&redirect_uri=https://yourapp.com/oauth/callback
&scope=read:clients%20write:appointments
&state=<csrf-random>
&code_challenge=<base64url(sha256(verifier))>
&code_challenge_method=S256PKCE is required — generate a random code_verifier (43–128 chars), hash it with SHA-256, base64url-encode for the challenge, and stash the verifier server-side keyed by state.
3. User approves at the consent screen
If the user isn't logged in to TattooBooking, they'll be prompted to sign in first. Then they see a consent screen showing your app's name + logo + the scopes you're requesting + which tenant they're authorizing.
On Approve, they're 302-redirected to your redirect_uri with?code=<authcode>&state=<original-state>.
On Deny, redirected with ?error=access_denied&state=<original-state>.
4. Exchange the code for tokens
POST https://api.tattoobooking.com/oauth/token
Content-Type: application/x-www-form-urlencoded
Authorization: Basic <base64(client_id:client_secret)>
grant_type=authorization_code
&code=<authcode-from-redirect>
&redirect_uri=https://yourapp.com/oauth/callback
&code_verifier=<the-verifier-you-stashed>HTTP/1.1 200 OK
{
"access_token": "tboat_xxxxxxxxxxxx",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "tbort_xxxxxxxxxxxx",
"scope": "read:clients write:appointments"
}Authorization codes are single-use and expire in 60s. If you accidentally try to redeem a code twice, the entire grant is revoked (security countermeasure against intercepted codes).
5. Call the API
curl https://api.tattoobooking.com/v1/clients \
-H "Authorization: Bearer tboat_xxxxxxxx"Same /v1/* surface, same scopes, same rate-limit tier — the only difference from API keys is the token format.
6. Refresh when expired
Access tokens live 1 hour. When you get a 401 with code: expired_access_token, refresh:
POST /oauth/token
grant_type=refresh_token
&refresh_token=<your-current-refresh-token>
&client_id=tboa_xxx
&client_secret=tbosk_xxxEach refresh issues a NEW refresh token and invalidates the old one (rotation). If you ever present an already-rotated refresh token, we treat it as a stolen-token replay and revoke the entire grant. Make sure your code always replaces the stored refresh token with the new one returned by /oauth/token.
7. Revoke when done
POST /oauth/revoke
token=<access-or-refresh-token>
&token_type_hint=refresh_token
&client_id=tboa_xxx
&client_secret=tbosk_xxxRevoking a refresh token cascades to all live access tokens in the same grant. Per RFC 7009, the response is always 200 — we never leak whether a given token was valid.
Server metadata discovery (RFC 8414)
Get the full set of endpoints + supported grants from one URL — useful for plugging us into an OAuth client library that knows how to read it.
GET https://api.tattoobooking.com/.well-known/oauth-authorization-serverSecurity notes
- Always use HTTPS for your redirect_uri. We reject HTTP redirect_uris in production.
- Validate
stateon the redirect. Match it against what you sent — protects against CSRF. - Don't embed client_secret in mobile apps. Public clients (mobile, SPAs) use PKCE without a secret. If you need a confidential client, run a backend that holds the secret.