---
title: Authorization (OAuth)
description: Get a shop's approval and receive access tokens with the authorization code flow and PKCE.
---

The partner API uses the OAuth 2.1 authorization code flow. PKCE (S256) is required. Exchange codes for tokens on your server, where the client secret can stay secret.

## Endpoints

| Purpose                       | URL                                                                            |
| ----------------------------- | ------------------------------------------------------------------------------ |
| Authorization                 | `https://api.toreca-cloud.com/api/auth/oauth2/authorize`                       |
| Token                         | `https://api.toreca-cloud.com/api/auth/oauth2/token`                           |
| Revocation                    | `https://api.toreca-cloud.com/api/auth/oauth2/revoke`                          |
| Resource (`resource`)         | `https://api.toreca-cloud.com/partner/v1`                                      |
| Authorization server metadata | `https://api.toreca-cloud.com/api/auth/.well-known/oauth-authorization-server` |
| Protected resource metadata   | `https://api.toreca-cloud.com/.well-known/oauth-protected-resource/partner/v1` |

Send `resource=https://api.toreca-cloud.com/partner/v1` with every authorization, token and refresh request.

## 1. Open the authorization screen

Generate a random `code_verifier` and use its SHA-256 as the `code_challenge`. Generate a random `state` too and check it when the user comes back.

```ts
import { createHash, randomBytes } from "node:crypto";

const verifier = randomBytes(32).toString("base64url");
const challenge = createHash("sha256").update(verifier).digest("base64url");
const state = randomBytes(16).toString("hex");

const url = new URL("https://api.toreca-cloud.com/api/auth/oauth2/authorize");
url.search = new URLSearchParams({
  response_type: "code",
  client_id: process.env.TORECA_CLIENT_ID!,
  redirect_uri: "https://lp.example.com/oauth/callback",
  scope: "openid profile offline_access purchase-boosts:read",
  state,
  code_challenge: challenge,
  code_challenge_method: "S256",
  resource: "https://api.toreca-cloud.com/partner/v1",
}).toString();
// Save verifier and state in the session, then redirect to url
```

Include `offline_access` in `scope`. Without it no refresh token is issued and you'd need a new approval every 15 minutes. Asking for a scope the shop didn't allow when registering the app fails with `invalid_scope`.

## 2. A shop member approves

The member signs in to Toreca Cloud, checks the shop and what is being allowed, and presses Allow. The browser returns to `redirect_uri` with `code` and `state`.

## 3. Exchange the code for tokens

```ts
const response = await fetch(
  "https://api.toreca-cloud.com/api/auth/oauth2/token",
  {
    method: "POST",
    headers: {
      Authorization:
        "Basic " +
        Buffer.from(
          `${encodeURIComponent(clientId)}:${encodeURIComponent(clientSecret)}`,
        ).toString("base64"),
      "Content-Type": "application/x-www-form-urlencoded",
    },
    body: new URLSearchParams({
      grant_type: "authorization_code",
      code,
      redirect_uri: "https://lp.example.com/oauth/callback",
      code_verifier: verifier,
      resource: "https://api.toreca-cloud.com/partner/v1",
    }),
  },
);
const token = await response.json();
// { access_token, refresh_token, token_type: "Bearer", expires_in: 900, scope }
```

## 4. Create the connection

With the tokens in hand, call `POST /partner/v1/connection` first. The response names the connected shop.

```bash
curl -X POST https://api.toreca-cloud.com/partner/v1/connection \
  -H "Authorization: Bearer $ACCESS_TOKEN"
```

Every other call works only while this connection is active. See [tokens and expiry](/en/api/tokens) for storing and refreshing tokens.
