Flavoso APIFundamentalsAuthentication
Authentication
OAuth 2.0 with PKCE: how an owner connects your app to their restaurant, and how you exchange, refresh and revoke tokens.
Overview
An app only gets access to a restaurant with its owner's consent. Flavoso uses OAuth 2.0 with authorization code (RFC 6749) and PKCE with S256 (RFC 7636), plus the iss parameter on the redirect (RFC 9207), revocation per RFC 7009 and metadata per RFC 8414.
- 1
Your app
“Connect with Flavoso”
Your app creates state and a code_verifier and sends the browser to /oauth/authorize.
- 2
Flavoso
Sign in
The owner signs in with their Flavoso account, with a second factor if they set one up.
- 3
Flavoso
Consent
They pick the restaurant, read your app's name, website and scopes and press “Connect”.
- 4
Browser
Back with a code
Flavoso redirects back to your redirect_uri with code, state and iss. The code is valid for 10 minutes and only once.
- 5
Your server
Exchange
Your server exchanges code and code_verifier for an access token and a refresh token.
- 6
Your server
Request
Call the API with the access token (1 hour); the refresh token gets new ones.
| Endpoint | Address |
|---|---|
| Authorization (browser) | https://app.flavoso.com/oauth/authorize |
| Token | POST https://api.flavoso.com/oauth/token |
| Revocation | POST https://api.flavoso.com/oauth/revoke |
| Metadata | https://api.flavoso.com/.well-known/oauth-authorization-server |
Authorization is a page on app.flavoso.com, because that is where the owner signs in. Token, revocation and metadata live at https://api.flavoso.com. The endpoints with every parameter are also in the reference.
Confidential and public clients
You choose the client type when you create the app:
- Confidential (
confidential): for apps with their own server. They get a client secret (flv_cs_…), which is shown exactly once and belongs on the server only. At the token endpoint they authenticate with HTTP Basic or sendclient_idandclient_secretin the body. Real restaurants connect them only through anhttpsredirect URI. - Public (
public): for apps that cannot keep a secret safe, for example a POS app on a tablet or a program on the restaurant's computer. They get no secret and sendclient_idonly. They may use loopback addresses and a custom scheme (RFC 8252), including for real restaurants.
PKCE with S256 is mandatory for both. The token endpoint accepts no calls from a browser (no CORS); a web app therefore exchanges code and refresh token on its server, which makes it a confidential client.
The client ID (flv_app_…) is not secret. “Create new client secret” in the console replaces the client secret; the old one stays valid for 24 hours, so you switch over without interruption.
Redirect URIs
After the consent, Flavoso sends the browser back to one of your app's redirect URIs. The rules for them:
- Flavoso compares character by character with the registered addresses. The only exception is the port of loopback addresses.
https, without credentials in the address. Real restaurants connect any app through these.- Loopback (
http://127.0.0.1:<port>/…,http://localhost:<port>/…,http://[::1]:<port>/…, any port, RFC 8252) and custom schemes with a dot (com.example.pos:/callback): for a public app with every restaurant, for a confidential app only with your own test restaurant. The consent screen then offers only that one. - No fragment (
#), at most 300 characters, at most 10 addresses per app.
The consent
On the consent screen the user signs in with their Flavoso account; if they have set up a second factor, they are asked for it. Then they pick the restaurant, see your app's logo, name and website and the requested scopes in plain words, and press “Connect” or “Cancel”. Approved apps carry “Reviewed by Flavoso” there.
- Only owners connect. Someone with another role in the restaurant does not see it in the list.
- Before approval, only you connect: to your test restaurant and to restaurants you own. Through a loopback address or a custom scheme, a confidential app connects only your test restaurant, even after approval.
- All owners of the restaurant get an e-mail after connecting: which app, which vendor, which scopes, who and when. The same happens when an existing connection gets more scopes.
- Connecting again replaces the scopes of the existing connection, whether that means more or fewer. Your app then receives
app.scopes_changedwith the old scopes.
The redirect
After “Connect”, Flavoso redirects to your redirect_uri with code, state and iss. Check that state matches the value you sent and that iss is exactly https://api.flavoso.com, the issuer from the metadata (even though the consent took place on app.flavoso.com). Only then exchange the code.
The code is valid for 10 minutes and exactly once. If it is redeemed a second time, Flavoso revokes everything that was issued with it.
With an unknown client_id, an unregistered redirect_uri or duplicate parameters, Flavoso shows an error page and does not redirect. The other errors come back as a redirect to the redirect_uri, with error, error_description, state and iss, but only for an approved app or when you are signed in as its developer. Otherwise Flavoso shows the error itself; a rejected or suspended app never gets a redirect. error can have these values:
| error | Meaning |
|---|---|
access_denied | The user pressed “Cancel”, or may not connect this app to the chosen restaurant: for example because they do not own it, or because a confidential app only connects your test restaurant through a loopback address or a custom scheme. |
invalid_request | A parameter is missing or invalid, for example state or the code_challenge. |
invalid_scope | A requested scope does not exist, or your app may not request it. |
unsupported_response_type | response_type is not code. |
server_error | Something went wrong on Flavoso's side, for example the connection could not be saved. Try again later. |
Exchanging the code for tokens
You exchange the code with a POST to https://api.flavoso.com/oauth/token, body as application/x-www-form-urlencoded: grant_type=authorization_code with code, redirect_uri (the same as in the authorization request) and code_verifier. Confidential clients authenticate with HTTP Basic, public ones send client_id.
# Confidential client: HTTP Basic (or client_id and client_secret in the body)
curl https://api.flavoso.com/oauth/token \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=authorization_code \
-d code=AUTHORIZATION_CODE \
--data-urlencode redirect_uri=https://example.com/flavoso/callback \
-d code_verifier=$CODE_VERIFIER
# Public client (native or desktop app): client_id only, no secret
curl https://api.flavoso.com/oauth/token \
-d grant_type=authorization_code \
-d code=AUTHORIZATION_CODE \
--data-urlencode redirect_uri=com.example.pos:/callback \
-d code_verifier=$CODE_VERIFIER \
-d client_id=$CLIENT_IDThe response contains both tokens, the granted scopes, the connection's id and the restaurant:
{
"access_token": "flv_at_...",
"token_type": "Bearer",
"expires_in": 3600,
"refresh_token": "flv_rt_...",
"scope": "orders:write menu:write store:write reports:read",
"connection_id": "4b3f7c1e-9a2d-4e8b-b5f6-0c1d2e3f4a5b",
"tenant": {
"id": "7d0e5b8a-3c41-4f2e-9a6b-1e2d3c4b5a69",
"name": "Pizzeria Example",
"slug": "pizzeria-example"
}
}Store the tokens per connection, for example under tenant.id. A token is valid for exactly one restaurant; every further restaurant gets its own connection with its own tokens.
Tokens and lifetimes
| What | Format | Valid |
|---|---|---|
| Authorization code | flv_ac_… | 10 minutes, exactly once |
| Access token | flv_at_… | 1 hour (expires_in: 3600) |
| Refresh token | flv_rt_… | 180 days from its last use, replaced on every use |
| Client secret | flv_cs_… | until the next “Create new client secret”, the old one then for another 24 hours |
Treat tokens like passwords: store them encrypted, never write them to logs, never put them into a browser or an address.
Refreshing with rotation
You get a new access token with grant_type=refresh_token and refresh_token. The response also brings a new refresh token; the old one is used up. Store the new one before you use the new access token.
If the response gets lost on the way and you send the same refresh token again within 60 seconds, you get a new pair, but only while the refresh token from the lost response is still unused. Flavoso revokes the pair from the lost response in doing so. In every other case a second redemption counts as theft: Flavoso revokes the whole family, meaning all tokens from the same consent, and the owner has to connect again.
With scope you narrow the new access token to fewer scopes, for example for a part of your system that only reads. The refresh token keeps all of the connection's scopes.
// A valid access token for one connection. tokenRequest() as above.
// Refresh at one place at a time per connection, and always store the NEW refresh token.
export async function accessTokenFor(conn) {
if (Date.now() < conn.expiresAt - 60_000) return conn.accessToken;
try {
const t = await tokenRequest({ grant_type: "refresh_token", refresh_token: conn.refreshToken });
conn.accessToken = t.access_token;
conn.refreshToken = t.refresh_token; // the old one is used up
conn.expiresAt = Date.now() + t.expires_in * 1000;
await saveConnection(conn); // persist BEFORE using the new token
return conn.accessToken;
} catch (err) {
if (err.code === "invalid_grant") await markDisconnected(conn); // the owner has to connect again
if (err.code === "unauthorized_client") await markSuspended(conn); // app suspended: keep the tokens, retry later
throw err;
}
}Revoking and disconnecting
POST https://api.flavoso.com/oauth/revoke with token revokes a token (RFC 7009); you may send token_type_hint, but Flavoso recognises the kind from the token's form. The client authenticates as at the token endpoint. Without token, Flavoso answers 400 invalid_request; if the authentication fails, 401 invalid_client. Otherwise the response is always 200, even for an unknown or already revoked token. A refresh token takes its whole family with it, an access token only itself. The connection itself remains.
To end the connection to the restaurant, your app calls DELETE /connection. Then no token of this connection works any more, the owners get an e-mail, and your webhook receives app.disconnected. For the same app and restaurant, a new consent is needed afterwards.
# Revoke a token (RFC 7009). The client authenticates as at the token endpoint;
# after that the answer is always 200. A refresh token takes its whole family with it.
curl https://api.flavoso.com/oauth/revoke \
-u "$CLIENT_ID:$CLIENT_SECRET" \
-d token=$REFRESH_TOKEN \
-d token_type_hint=refresh_token
# End the connection to the restaurant: every token of it stops working, the owners are notified
curl -X DELETE https://api.flavoso.com/v1/connection \
-H "Authorization: Bearer $ACCESS_TOKEN"Metadata
Addresses and supported methods are available machine-readable at https://api.flavoso.com/.well-known/oauth-authorization-server (RFC 8414). Many OAuth libraries read them automatically.
curl https://api.flavoso.com/.well-known/oauth-authorization-serverErrors at the token endpoint
The token endpoint answers per RFC 6749 with error and error_description, every response with Cache-Control: no-store:
| error | HTTP | Meaning |
|---|---|---|
invalid_request | 400 | A parameter is missing, repeated or invalid. |
invalid_client | 401 | Client authentication failed, with WWW-Authenticate. |
invalid_grant | 400 | The code or refresh token is invalid, expired, already used or revoked, or redirect_uri or code_verifier does not match. On a refresh, this means the connection no longer exists. |
unauthorized_client | 400 | The app is suspended. Its connections stay: keep the tokens and try again later. |
unsupported_grant_type | 400 | This grant_type does not exist. |
invalid_scope | 400 | A scope is invalid or goes beyond the connection. |
temporarily_unavailable | 429 | Too many requests; Retry-After gives the wait in seconds. |
server_error | 500 | Something went wrong on Flavoso's side. |
{
"error": "invalid_grant",
"error_description": "The authorization code is invalid, expired or already used."
}Many restaurants
A POS with many customers has many connections. Each has its own tokens, its own rate limit and its connection_id. Your app's webhook receives the events of all restaurants; tenant_id and connection_id in the envelope say which one it belongs to.
A restaurant that connects you a second time keeps the same connection with new scopes. After a disconnect, connecting again creates a new connection with a new connection_id.
Still unclear? Write to support@flavoso.com and include the id from X-Request-Id. It lets us find your request in the log.