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
-
Register your OAuth client once. This happens on your side, as the partner — not per merchant. You get back a
client_idandclient_secretthat identify your integration for every merchant that ever connects it. See Client Registration. -
Store
client_idandclient_secretin 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_tokenyou 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. -
Add a "Connect with Whatalo" button that redirects the merchant to
GET /oauth/authorizewith the scopes your integration needs (includewrite:webhooksif 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.
-
Handle the callback on your server and exchange the returned
codefor tokens atPOST /oauth/token. The response containsaccess_token,refresh_token,expires_in, andscope. This exchange happens server-to-server, using yourclient_secret— never in the merchant's browser. See Token Endpoint. -
Persist the tokens in your own database, keyed to the merchant's store. Save the
access_token, therefresh_token, and the expiry you derive fromexpires_in— you need them for every later API call on that merchant's behalf. Therefresh_tokenrotates: each renewal atPOST /oauth/tokenreturns a newrefresh_token, so overwrite the stored value every time. Therefresh_tokenalso 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. -
Auto-register your webhook with
POST /v1/webhooks, using theaccess_tokenyou just received. This requires thewrite:webhooksscope granted in step 3. The response includes a signingsecret(indata.secret) returned only once — store it immediately next to the tokens for this merchant. See Webhooks. -
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.
// 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");
}Related pages
| Page | When to read it |
|---|---|
| OAuth Overview | Conceptual model — opaque tokens, API Key vs. OAuth |
| Client Registration | Full request/response reference for step 1 |
| Authorization Flow | Full PKCE and consent-screen reference for step 3 |
| Token Endpoint | Full grant type reference for step 4, plus refresh rotation |
| Scopes | The complete scope table to decide what to request in step 3 |
| Webhooks | Full endpoint reference and payload shapes for steps 6–7 |
| OAuth Errors | Error codes you may hit at any step of this flow |