OAuth 2.1

Partner Integration Quickstart

The one canonical, linear flow for connecting a third-party integration to Whatalo stores with OAuth 2.1 — from client registration to your first verified webhook.

This page is the single connect-flow reference for partner integrations — SaaS products, logistics/shipping platforms, marketing tools, and any external service that needs to act on behalf of one or more Whatalo merchants. It threads together the deep-dive pages in this section into one linear path so there is exactly one correct way to wire this up.

The pattern in one sentence

You, the integrating partner, register ONE OAuth client for your entire product. It is not tied to any single store. Each merchant who wants to connect your integration clicks an "Authorize" button, picks their store on the Whatalo consent screen, and that produces a separate access token scoped to their store — all under your one client_id.

A merchant never creates an OAuth Client ID/Secret and never sees or pastes OAuth credentials into your product. If your onboarding UI asks a merchant to paste a Client ID or Client Secret, that is the wrong pattern — stop and follow the flow below instead.

The full flow

  1. Register your OAuth client once. This happens on your side, as the partner — not per merchant. You get back a client_id and client_secret that identify your integration for every merchant that ever connects it. See Client Registration.

  2. Store client_id and client_secret in your backend environment. They live in server-side configuration (secrets manager, environment variables) — never in a mobile app bundle, browser bundle, or any client-side code.

    The client secret, and every refresh_token you receive later, must never be exposed in frontend code or sent to the browser. They are server-side secrets for the lifetime of the integration.

  3. Add a "Connect with Whatalo" button that redirects the merchant to GET /oauth/authorize with the scopes your integration needs (include write:webhooks if you plan to self-register a webhook in step 6). See Authorization Flow for the full request shape including PKCE, and Scopes for the complete scope table.

    The merchant does not create or provide any credentials at this step. They sign in to Whatalo, pick the store they want to connect, review the requested scopes, and click Authorize. That is their entire involvement in the OAuth flow.

  4. Handle the callback on your server and exchange the returned code for tokens at POST /oauth/token. The response contains access_token, refresh_token, expires_in, and scope. This exchange happens server-to-server, using your client_secret — never in the merchant's browser. See Token Endpoint.

  5. Persist the tokens in your own database, keyed to the merchant's store. Save the access_token, the refresh_token, and the expiry you derive from expires_in — you need them for every later API call on that merchant's behalf. The refresh_token rotates: each renewal at POST /oauth/token returns a new refresh_token, so overwrite the stored value every time. The refresh_token also has its own expiry, and presenting an already-rotated one revokes the merchant's entire token family — so always keep only the latest. See Token Endpoint for refresh and rotation details.

  6. Auto-register your webhook with POST /v1/webhooks, using the access_token you just received. This requires the write:webhooks scope granted in step 3. The response includes a signing secret (in data.secret) returned only once — store it immediately next to the tokens for this merchant. See Webhooks.

  7. Verify the signature of every incoming webhook before trusting its payload. See Webhook Security for the exact HMAC-SHA256 scheme and a full verification example.

Why is the token exchanged on your backend, and never in the merchant's browser?

Your client_secret proves to Whatalo that a token request genuinely comes from your integration and not from an impersonator. If it were ever shipped to a browser or mobile client, anyone could extract it and mint tokens as you. Keeping the exchange server-side is also exactly what makes the anti-pattern impossible: there is never a moment where a merchant needs to know, generate, or paste an OAuth credential — the credential belongs to your integration, not to them.

Callback handler example

The example below shows steps 4 through 6 together: exchanging the authorization code for tokens, persisting them for the merchant, then immediately self-registering a webhook with the resulting access token.

Node.js — OAuth callback handler
// Express-style handler mounted at the redirect_uri you registered in step 1.
// `codeVerifier` and `expectedState` were persisted server-side (session or
// short-lived cache) when you built the /oauth/authorize redirect in step 3.
export async function handleOAuthCallback(req, res) {
  const { code, state } = req.query;
  const { codeVerifier, expectedState, merchant } =
    await getPendingAuthorization(state);

  if (state !== expectedState) {
    return res.status(400).send("Invalid state");
  }

  // Step 4 — exchange the code for tokens, server-to-server.
  const tokenResponse = await fetch("https://app.whatalo.com/oauth/token", {
    method: "POST",
    headers: {
      "Content-Type": "application/x-www-form-urlencoded",
      Authorization:
        "Basic " +
        Buffer.from(
          `${process.env.WHATALO_CLIENT_ID}:${process.env.WHATALO_CLIENT_SECRET}`
        ).toString("base64"),
    },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: process.env.WHATALO_REDIRECT_URI,
      client_id: process.env.WHATALO_CLIENT_ID,
      code_verifier: codeVerifier,
    }),
  });

  const { access_token, refresh_token, expires_in } =
    await tokenResponse.json();

  // Step 5 — persist the tokens in YOUR database, keyed to this merchant's
  // store, before anything else. These are how you call the API later and
  // refresh once the access_token expires. The refresh_token rotates on every
  // renewal, so always overwrite the stored value with the newest one.
  await saveTokensForMerchant(merchant, {
    access_token,
    refresh_token,
    expires_at: Date.now() + expires_in * 1000,
  });

  // Step 6 — self-register your webhook with the access token you just received.
  // Requires the write:webhooks scope requested in step 3.
  const webhookResponse = await fetch("https://api.whatalo.com/v1/webhooks", {
    method: "POST",
    headers: {
      "Content-Type": "application/json",
      Authorization: `Bearer ${access_token}`,
    },
    body: JSON.stringify({
      url: "https://your-app.com/webhooks/whatalo",
      events: ["order.created", "order.updated"],
    }),
  });

  const { data: webhook } = await webhookResponse.json();

  // data.secret is returned ONLY in this response — persist it now, next to
  // this merchant's tokens, for signature verification in step 7.
  await saveWebhookSecretForMerchant(merchant, webhook.secret);

  return res.redirect("/connected");
}
PageWhen to read it
OAuth OverviewConceptual model — opaque tokens, API Key vs. OAuth
Client RegistrationFull request/response reference for step 1
Authorization FlowFull PKCE and consent-screen reference for step 3
Token EndpointFull grant type reference for step 4, plus refresh rotation
ScopesThe complete scope table to decide what to request in step 3
WebhooksFull endpoint reference and payload shapes for steps 6–7
OAuth ErrorsError codes you may hit at any step of this flow

On this page