Authentication
Quiqqy uses two kinds of credentials, and knowing which one a call wants is most of authentication:
| Credential | What it is | Where it is used |
|---|---|---|
client_id / client_secret | Your app’s identity, issued in the console | HTTP Basic on POST /pos/signup_url; the client_id on OAuth authorize and token calls; exchanged directly for app-level tokens on Hub Ordering and Mercnet Reports |
| Merchant access token | A Bearer token minted when a merchant authorizes your app | Every other Online Ordering call — Authorization: Bearer <access_token> |
For the Online Ordering API there is no client-credentials grant. A merchant access token always represents a merchant who consented to your app acting on their stores, which is why the grant there is authorization code and nothing else. Hub Ordering and Mercnet Reports work differently — they use the client-credentials grant with app-level tokens.
The authorization-code flow
Section titled “The authorization-code flow”-
Send the merchant to the authorize URL. The easiest way is
POST /pos/signup_url(see Store onboarding): it provisions the merchant account and store if needed and returns a ready-made URL. You can also build the URL directly:GET https://auth.quiqqy.ai/api/v2/oauth2/authorize?client_id=app_7pk2mv9qx4rt&redirect_uri=https://pos.example.com/quiqqy/callback&email=owner@mainstreetpizza.example&state=af0ifjsldkj -
The merchant signs in and consents. Quiqqy shows what your app is asking for. On approval the merchant is redirected to your
redirect_uriwith?code=…&state=…. Verifystatematches what you sent. -
Exchange the code for tokens.
Terminal window curl -s https://auth.quiqqy.ai/api/v2/oauth2/token \-H 'Content-Type: application/json' \-d '{"grant_type": "authorization_code","code": "SplxlOBeZQQYbYS6WxSbIA","redirect_uri": "https://pos.example.com/quiqqy/callback","client_id": "app_7pk2mv9qx4rt","client_secret": "qk_pk_5zkq5vj2rn8pxw3tfm6bd1yh4sc7ae0g"}'const response = await fetch('https://auth.quiqqy.ai/api/v2/oauth2/token', {method: 'POST',headers: { 'Content-Type': 'application/json' },body: JSON.stringify({grant_type: 'authorization_code',code,redirect_uri: 'https://pos.example.com/quiqqy/callback',client_id: process.env.QUIQQY_CLIENT_ID,client_secret: process.env.QUIQQY_CLIENT_SECRET,}),});if (!response.ok) {const { error } = await response.json();throw new Error(`${error.code}: ${error.message}`);}const { access_token, refresh_token, expires_in } = await response.json();import osimport requestsresponse = requests.post("https://auth.quiqqy.ai/api/v2/oauth2/token",json={"grant_type": "authorization_code","code": code,"redirect_uri": "https://pos.example.com/quiqqy/callback","client_id": os.environ["QUIQQY_CLIENT_ID"],"client_secret": os.environ["QUIQQY_CLIENT_SECRET"],},)response.raise_for_status()tokens = response.json()200 OK {"token_type": "Bearer","access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…","refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…","expires_in": 3600,"scope": "store.read menu.write orders.write webhooks.write"} -
Call the API. Send the access token as a Bearer credential on every partner request:
Authorization: Bearer <access_token>
App-level tokens: Hub Ordering and Mercnet Reports
Section titled “App-level tokens: Hub Ordering and Mercnet Reports”The authorization-code flow above is the Online Ordering model — every token
represents a consenting merchant. The
Hub Ordering and
Mercnet Reports APIs instead use the OAuth2
client-credentials grant: no merchant redirect, just your app’s own
credentials exchanged for an app-level Bearer token at POST /oauth2/token.
curl -s https://auth.quiqqy.ai/api/v2/oauth2/token \ -H 'Content-Type: application/json' \ -d '{ "grant_type": "client_credentials", "client_id": "app_7pk2mv9qx4rt", "client_secret": "qk_pk_5zkq5vj2rn8pxw3tfm6bd1yh4sc7ae0g", "audience": "partner-community" }'The response is { access_token, expires_in } — send it as
Authorization: Bearer <access_token> and cache it until it expires; do not
mint one per request.
- Hub Ordering — pass
audience: partner-community. The token is scoped to your community; requests against anothercommunity_idreturn404. - Mercnet Reports — request the
reports.readscope for the Reports endpoints andcatalogue.writefor catalogue mutations. Tokens are scoped to your app’s authorized organizations — astore_idoutside them returns404.
Token lifetimes and refresh
Section titled “Token lifetimes and refresh”| Token | Lifetime | Renewal |
|---|---|---|
| Access token | 3600 seconds (1 hour) | POST /oauth2/refresh |
| Refresh token | 7 days | Re-run the authorization-code flow |
curl -s https://auth.quiqqy.ai/api/v2/oauth2/refresh \ -H 'Content-Type: application/json' \ -d '{ "grant_type": "refresh_token", "refresh_token": "eyJhbGciOiJIUzI1…" }'The refresh response has the same shape as the token response, minus
refresh_token. When the refresh token itself expires — or the merchant
revokes access — refreshes fail and you must send the merchant through the
consent flow again.
Handling tokens well
Section titled “Handling tokens well”- Cache one token per merchant and reuse it for its whole lifetime. Minting on every request adds latency and burns your rate limit for nothing.
- Refresh 60 seconds early, so an in-flight request never expires mid-call, and guard the refresh with a single-flight lock so a burst of concurrent requests produces one refresh, not fifty.
- Treat tokens as opaque. They are signed JWTs, but the claim layout is not part of the contract. Authorize by calling the API, not by decoding.
- Store
refresh_tokenencrypted at rest. It is a 7-day credential for the merchant’s stores. - Keep secrets server-side.
client_secret,refresh_tokenand everywebhook_secretbelong on your backend only — never in a POS terminal build, mobile app or browser code.
Rotating a secret
Section titled “Rotating a secret”Rotate from Apps → Credentials → Rotate in the console. The new secret is shown once and works immediately. If a secret is committed to a repository, pasted into a ticket or logged, rotate it right away — existing merchant tokens keep working; only future OAuth exchanges use the new secret.
Auth failures
Section titled “Auth failures”Failures use the standard error envelope:
| Code | Status | Cause |
|---|---|---|
auth.invalid_client | 401 | Unknown client_id, wrong secret, or a revoked credential — deliberately indistinguishable |
auth.invalid_grant | 400 | Authorization code expired, already used, or issued to a different redirect_uri |
auth.token_expired | 401 | Access token past expires_in. Refresh and retry once |
auth.invalid_token | 401 | Malformed, revoked or wrong-environment token |
auth.consent_revoked | 403 | The merchant uninstalled or revoked your app. Stop retrying; prompt for re-consent |