Skip to content

Authentication

Quiqqy uses two kinds of credentials, and knowing which one a call wants is most of authentication:

CredentialWhat it isWhere it is used
client_id / client_secretYour app’s identity, issued in the consoleHTTP 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 tokenA Bearer token minted when a merchant authorizes your appEvery 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.

  1. 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
  2. The merchant signs in and consents. Quiqqy shows what your app is asking for. On approval the merchant is redirected to your redirect_uri with ?code=…&state=…. Verify state matches what you sent.

  3. 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"
    }'
    200 OK
    {
    "token_type": "Bearer",
    "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "refresh_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9…",
    "expires_in": 3600,
    "scope": "store.read menu.write orders.write webhooks.write"
    }
  4. 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.

Terminal window
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 another community_id return 404.
  • Mercnet Reports — request the reports.read scope for the Reports endpoints and catalogue.write for catalogue mutations. Tokens are scoped to your app’s authorized organizations — a store_id outside them returns 404.
TokenLifetimeRenewal
Access token3600 seconds (1 hour)POST /oauth2/refresh
Refresh token7 daysRe-run the authorization-code flow
Terminal window
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.

  • 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_token encrypted at rest. It is a 7-day credential for the merchant’s stores.
  • Keep secrets server-side. client_secret, refresh_token and every webhook_secret belong on your backend only — never in a POS terminal build, mobile app or browser code.

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.

Failures use the standard error envelope:

CodeStatusCause
auth.invalid_client401Unknown client_id, wrong secret, or a revoked credential — deliberately indistinguishable
auth.invalid_grant400Authorization code expired, already used, or issued to a different redirect_uri
auth.token_expired401Access token past expires_in. Refresh and retry once
auth.invalid_token401Malformed, revoked or wrong-environment token
auth.consent_revoked403The merchant uninstalled or revoked your app. Stop retrying; prompt for re-consent