LoopSolutions
All examples
AuthTypeScriptintermediate

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 GitLab

Before 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
The endpoint Loop redirects merchants to when they click Install on your integration. Loop appends ?organization=<id> so you know which merchant you're working with.

Authorization redirect

Server
Generates a CSRF state token, stores it, then redirects the merchant to Loop's /authorize with your client ID, scopes, the state, and the organization.

Callback endpoint

Server
Where Loop sends the merchant back with a one-time code and the state you sent. Verifies the state matches before doing anything else.

Token exchange handler

Server
Trades the one-time code (plus your client secret) for an access token + refresh token. Server-side only — the secret never touches the browser.

Token store

Server
Persistent, encrypted storage keyed by Loop's organization identifier. Holds access token, refresh token, and expiration timestamp per merchant.

Refresh wrapper

Server
Sits in front of every Loop API call. Checks expiration, refreshes if needed (with a buffer), and hands back a guaranteed-fresh access token.

API caller

Server
Standard bearer-token HTTP client against api.loopreturns.com. Uses the token the refresh wrapper just verified.

The flow

What happens, in order, when a merchant connects.

  1. Merchant clicks Install

    In the Loop admin, the merchant clicks Install on your integration. Loop redirects them to your install URL with ?organization=<id>.
  2. You redirect them to Loop's `/authorize`

    Your server generates a cryptographically random state, stores it (cookie or session), then redirects to Loop's /authorize with client_id, redirect_uri, scope, state, organization, and audience=https://api.loopreturns.com/api.
  3. Merchant logs in and approves

    Loop shows the merchant your app name and the scopes you're requesting. They log in and approve.
  4. Loop redirects back with a code

    Loop sends the merchant to your registered redirect URI with ?code=<one-time-code>&state=<same-state>.
  5. 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 to https://oauth.loopreturns.com/oauth/token with grant_type=authorization_code.
  6. Loop returns tokens

    Response includes access_token, refresh_token, and expires_in (typically 3600 seconds). Compute the absolute expiration timestamp and store all three keyed by the organization.
  7. Merchant is connected

    Your app can now call Loop's APIs on the merchant's behalf using the access token as a bearer credential.
  8. Refresh before every call

    Before each API call, the refresh wrapper checks expiration. If less than ~2 minutes remain, POST back to /oauth/token with grant_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 ===.

state verification (pattern)ts
// 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.

refresh wrapper (pattern)ts
// 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.