> ## Documentation Index
> Fetch the complete documentation index at: https://developers.perkstar.co.uk/llms.txt
> Use this file to discover all available pages before exploring further.

# Build an OAuth marketplace app

> Implement installation, consent, token rotation, revocation, and reconnect for a multi-merchant Perkstar integration.

Use OAuth when one application will be installed by multiple Perkstar
businesses. Each merchant approves access to one organisation; your backend
receives a scoped, short-lived access token and a rotating refresh token.

```mermaid theme={null}
sequenceDiagram
  participant M as Merchant
  participant A as Partner app
  participant P as Perkstar OAuth
  participant API as Perkstar API
  M->>A: Select Install Perkstar
  A->>A: Create state + PKCE verifier
  A->>P: GET /oauth/authorize
  P->>M: Sign in, choose business, review scopes
  M->>P: Allow
  P-->>A: callback?code=...&state=...
  A->>P: Exchange code + verifier
  P-->>A: Access token + rotating refresh token
  A->>API: Bearer perk_at_...
  API-->>A: Organisation-scoped response
```

## Supported profile

| Capability            | Perkstar behaviour                                         |
| --------------------- | ---------------------------------------------------------- |
| Authorization flow    | Authorization Code                                         |
| PKCE                  | Required, `S256` only                                      |
| Access token lifetime | 3,600 seconds                                              |
| Refresh tokens        | Single-use rotation; replay revokes the grant              |
| Redirect URIs         | Exact match; HTTPS except loopback development callbacks   |
| Client authentication | HTTP Basic or form secret for confidential apps            |
| Merchant approver     | Organisation owner or manager                              |
| Revocation            | RFC 7009-style endpoint; unknown tokens still return `200` |

<Warning>
  Perkstar does not support an implicit flow or a marketplace
  `client_credentials` grant. A merchant must approve access to their business.
</Warning>

## 1. Register the application

Marketplace applications are reviewed and registered by Perkstar. Send
[support@perkstar.co.uk](mailto:support@perkstar.co.uk):

* application name, description, and HTTPS icon URL;
* exact development, staging, and production redirect URIs;
* the minimum scopes required;
* a security/support contact and uninstall URL; and
* whether the application is ready for test or live merchants.

Perkstar returns a `client_id` and shows the `client_secret` once. Store the
secret in your backend's secret manager. Dynamic client registration at
`/api/oauth/register` is reserved for read-only MCP clients and cannot register
a marketplace app or request REST API scopes.

## 2. Create state and PKCE

Create a fresh verifier, challenge, and state for every install attempt. Store
the verifier and state in a short-lived, single-use server-side record tied to
the browser session.

<CodeGroup>
  ```ts TypeScript theme={null}
  import { createHash, randomBytes } from "node:crypto";

  const base64url = (value: Buffer) => value.toString("base64url");
  const verifier = base64url(randomBytes(48));
  const challenge = createHash("sha256").update(verifier).digest("base64url");
  const state = base64url(randomBytes(32));
  ```

  ```python Python theme={null}
  import base64
  import hashlib
  import secrets

  def b64url(value: bytes) -> str:
      return base64.urlsafe_b64encode(value).rstrip(b"=").decode()

  verifier = b64url(secrets.token_bytes(48))
  challenge = b64url(hashlib.sha256(verifier.encode()).digest())
  state = b64url(secrets.token_bytes(32))
  ```
</CodeGroup>

## 3. Send the merchant to consent

```text theme={null}
https://dashboard.perkstar.co.uk/oauth/authorize
  ?response_type=code
  &client_id=oac_replace_me
  &redirect_uri=https%3A%2F%2Fintegration.example.com%2Foauth%2Fperkstar%2Fcallback
  &scope=CUSTOMERS_READ%20ENROLLMENTS_READ%20TRANSACTIONS_WRITE
  &state=random_single_use_state
  &code_challenge=base64url_sha256_challenge
  &code_challenge_method=S256
```

The merchant signs in, chooses an organisation they own or manage, and sees the
requested permissions. Perkstar does not redirect to an unregistered URI when
the request is invalid.

## 4. Validate the callback

On success, Perkstar redirects to the exact registered URI with `code` and the
original `state`. On denial it returns `error=access_denied` and the state.

1. Compare `state` in constant time with the stored, unused value.
2. Refuse missing, mismatched, expired, or already-used state.
3. Mark the install attempt consumed before exchanging the code.
4. Never log the authorization code or callback query string.

Authorization codes are short-lived and single-use. A duplicate or delayed
callback must start a fresh install.

## 5. Exchange the code

The token endpoint accepts form-encoded bodies, not JSON.

```bash theme={null}
curl --request POST https://dashboard.perkstar.co.uk/api/oauth/token \
  --user "$PERKSTAR_CLIENT_ID:$PERKSTAR_CLIENT_SECRET" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=authorization_code" \
  --data-urlencode "code=$PERKSTAR_AUTHORIZATION_CODE" \
  --data-urlencode "redirect_uri=https://integration.example.com/oauth/perkstar/callback" \
  --data-urlencode "code_verifier=$PERKSTAR_CODE_VERIFIER"
```

```json theme={null}
{
  "access_token": "perk_at_example",
  "token_type": "Bearer",
  "expires_in": 3600,
  "refresh_token": "perk_rt_example",
  "scope": "CUSTOMERS_READ ENROLLMENTS_READ TRANSACTIONS_WRITE",
  "organization_id": "org_123"
}
```

Store the organisation ID with the grant and enforce one active installation
record per `(application, organization_id)` in your system.

## 6. Call the API

```bash theme={null}
curl https://dashboard.perkstar.co.uk/api/v1/marketplace/ping \
  --header "Authorization: Bearer $PERKSTAR_ACCESS_TOKEN"
```

Treat a `401` as an authentication problem, not permission to switch to another
merchant's token. A `403` means the authenticated grant, scope, plan, or account
state does not permit that action.

## 7. Rotate refresh tokens atomically

Every successful refresh invalidates the supplied refresh token and returns a
new one. Use a lock or compare-and-swap around the installation row.

```bash theme={null}
curl --request POST https://dashboard.perkstar.co.uk/api/oauth/token \
  --user "$PERKSTAR_CLIENT_ID:$PERKSTAR_CLIENT_SECRET" \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "grant_type=refresh_token" \
  --data-urlencode "refresh_token=$PERKSTAR_REFRESH_TOKEN"
```

<Steps>
  <Step title="Lock one installation">
    Ensure two workers cannot refresh the same grant concurrently.
  </Step>

  <Step title="Exchange the current token">
    Send the token stored on the locked row. Do not retry an ambiguous refresh
    concurrently from another worker.
  </Step>

  <Step title="Commit both replacements">
    Replace the access token, refresh token, expiry, and scopes in one database
    transaction before releasing the lock.
  </Step>

  <Step title="Reconnect on invalid_grant">
    Refresh-token replay revokes the chain. Stop background jobs and send the
    merchant through authorization again; do not loop retries.
  </Step>
</Steps>

## 8. Revoke and uninstall

```bash theme={null}
curl --request POST https://dashboard.perkstar.co.uk/api/oauth/revoke \
  --header "Content-Type: application/x-www-form-urlencoded" \
  --data-urlencode "client_id=$PERKSTAR_CLIENT_ID" \
  --data-urlencode "client_secret=$PERKSTAR_CLIENT_SECRET" \
  --data-urlencode "token=$PERKSTAR_REFRESH_TOKEN" \
  --data-urlencode "token_type_hint=refresh_token"
```

Revoking either token revokes the pair. After a successful uninstall:

* stop polling, queue drains, and webhook side effects for the merchant;
* remove active credentials from normal application storage;
* delete copied personal data that is no longer required;
* retain only the minimum audit or legal record required by your policy; and
* make reinstall create a fresh grant rather than reviving old credentials.

Merchants can also revoke access in **Settings → Connected apps**. Your jobs
must handle that without relying on your own uninstall screen being called.

## Error handling

| Error                     | Meaning                                                    | Partner action                                  |
| ------------------------- | ---------------------------------------------------------- | ----------------------------------------------- |
| `invalid_request`         | Missing parameter or wrong content type                    | Correct the request; do not retry unchanged     |
| `invalid_client`          | Client ID/secret failed                                    | Stop and check configuration or secret rotation |
| `invalid_grant`           | Code/token expired, used, revoked, mismatched, or replayed | Start a fresh merchant authorization            |
| `invalid_scope`           | No still-allowed scope remains                             | Review app registration and requested scopes    |
| `unsupported_grant_type`  | Flow is not authorization code or refresh token            | Correct the implementation                      |
| `temporarily_unavailable` | Rate-limit or transient gate                               | Honour backoff and retry safely                 |

## Launch checklist

* [ ] Redirect URIs are exact and environment-specific.
* [ ] State and PKCE are fresh, server-stored, expiring, and single-use.
* [ ] Client secrets and tokens never reach browser or mobile code.
* [ ] Refresh replacement is atomic and concurrency-tested.
* [ ] `invalid_grant`, merchant denial, and revocation lead to a clear reconnect state.
* [ ] Jobs are pinned to the stored `organization_id`.
* [ ] Uninstall removes credentials and unnecessary cached personal data.
* [ ] Test and live application credentials are separated.
* [ ] The [security and privacy review](/fundamentals/security-privacy) is complete.


## Related topics

- [Build a POS integration](/guides/pos.md)
- [Perkstar developer platform](/index.md)
- [Integration gallery](/integrations/index.md)
- [Authentication and scopes](/fundamentals/authentication.md)
- [Changelog](/changelog.md)
