Loop OAuth 2.0
An overview of what it takes to wire up Loop's OAuth 2.0 authorization-code flow into a third-party app. The end state: your app can authenticate as Loop merchants, hold their tokens, and call Loop's APIs on their behalf — without ever touching their passwords. Below is what you'll need to build regardless of language or framework. A working TypeScript / Next.js reference is linked at the top if you want to see one concrete implementation.
View full code on GitLabBefore you start
- OAuth credentials from Loop — request via the Integration Access Request form. You'll need a client ID, client secret, and a registered redirect URI.
- An HTTPS callback URL Loop can redirect to. For local dev, expose your server with ngrok (or similar) and submit the tunnel URL as your dev redirect on the form.
- A server-side environment that can hold your client secret. The exchange and refresh steps must happen server-side.
The pieces you need
Language-agnostic — every implementation has these moving parts.
Install URL
Server?organization=<id> so you know which merchant you're working with.Authorization redirect
Server/authorize with your client ID, scopes, the state, and the organization.Callback endpoint
ServerToken exchange handler
ServerToken store
Serverorganization identifier. Holds access token, refresh token, and expiration timestamp per merchant.Refresh wrapper
ServerAPI caller
Serverapi.loopreturns.com. Uses the token the refresh wrapper just verified.The flow
What happens, in order, when a merchant connects.
Merchant clicks Install
In the Loop admin, the merchant clicks Install on your integration. Loop redirects them to your install URL with?organization=<id>.You redirect them to Loop's `/authorize`
Your server generates a cryptographically random state, stores it (cookie or session), then redirects to Loop's/authorizewithclient_id,redirect_uri,scope,state,organization, andaudience=https://api.loopreturns.com/api.Merchant logs in and approves
Loop shows the merchant your app name and the scopes you're requesting. They log in and approve.Loop redirects back with a code
Loop sends the merchant to your registered redirect URI with?code=<one-time-code>&state=<same-state>.You verify state and exchange the code
Your server confirms the returned state matches what you stored (timing-safe comparison), then POSTs the code, your client ID, and your client secret tohttps://oauth.loopreturns.com/oauth/tokenwithgrant_type=authorization_code.Loop returns tokens
Response includesaccess_token,refresh_token, andexpires_in(typically 3600 seconds). Compute the absolute expiration timestamp and store all three keyed by theorganization.Merchant is connected
Your app can now call Loop's APIs on the merchant's behalf using the access token as a bearer credential.Refresh before every call
Before each API call, the refresh wrapper checks expiration. If less than ~2 minutes remain, POST back to/oauth/tokenwithgrant_type=refresh_token. Loop may rotate the refresh token — store whichever it returns.
Decisions you'll make
Where you store tokens. In-memory works for a POC; production needs persistent, encrypted storage keyed by Loop's organization identifier. Each record needs at minimum: access token, refresh token, and expiration timestamp. Treat tokens like passwords — encrypt at rest, never log them.
Which scopes you request. Loop scopes look like labels:read, labels:write, returns, developer_tools, offline_access. The merchant sees them on the consent screen — request only what you need. offline_access is required to get a refresh token; without it, the merchant has to re-consent every hour.
What you do on refresh failure. If the refresh token is revoked, expired, or the merchant uninstalled your app, refresh will fail. Surface a "reconnect" affordance in your UI rather than silently retrying. Plan for this from the start — it will happen.
Multi-merchant from day one. Build the token store as multi-tenant even if you only have one merchant today. Each Loop merchant is one record keyed by their organization identifier. Retrofitting later is painful.
Patterns worth lifting from the reference
Two patterns from the reference repo show up in any language — included here as small breadcrumbs.
Timing-safe state verification. Use constant-time comparison on the state parameter to prevent timing attacks. Every language has this primitive — Node's crypto.timingSafeEqual, Python's secrets.compare_digest, Go's subtle.ConstantTimeCompare. Don't use ===.
// Constant-time comparison — equivalent primitives exist in every language
export function verifyState(received: string, expected: string): boolean {
if (!received || !expected) return false;
if (received.length !== expected.length) return false;
let result = 0;
for (let i = 0; i < received.length; ++i) {
result |= received.charCodeAt(i) ^ expected.charCodeAt(i);
}
return result === 0;
}Refresh before every call
Wrap every Loop API call in a refresh check with a small buffer. You get back either a guaranteed-fresh token or an error — never an expired one. Centralize this so you only encrypt/decrypt and persist tokens in one place.
// If the token is expired or about to expire in the next 2 minutes, refresh.
const now = Date.now();
if (tokenRecord.expires_at - now < 2 * 60 * 1000) {
const refreshed = await refreshToken(tokenRecord.refresh_token);
tokenRecord = {
...tokenRecord,
access_token: refreshed.access_token,
expires_at: Date.now() + refreshed.expires_in * 1000,
// Loop may rotate the refresh token — keep whichever we got back
refresh_token: refreshed.refresh_token ?? tokenRecord.refresh_token,
};
await persist(organization, tokenRecord);
}Pitfalls
- Never exchange the code from the browser. The client secret stays server-side. Flow is: browser → your server → Loop, never browser → Loop.
- Use a CSPRNG for the state parameter.
Math.random()and equivalents are not cryptographically secure. Use the platform's secure RNG. - Compare state with timing-safe equality. Plain
===leaks information through timing. - Loop may rotate the refresh token on each refresh. Persist whichever you got back, not the original.
- HTTPS everywhere in production. Authorization URLs, redirect URIs, token exchanges, API requests. ngrok handles this for local dev.
Run it yourself
The reference repo is a Next.js app you can clone, drop your credentials into .env.local, and have running locally in a few minutes. It's intentionally a POC — token storage is in-memory, scopes are hardcoded — so you can see the moving parts clearly before adapting to your stack.