# Sign in with Plotwork

> Page: https://docs.plotwork.dev/sign-in-with-plotwork. Index of every page: https://docs.plotwork.dev/llms.txt

How to let the people of your app sign in with their Plotwork account and call the Plotwork API as themselves, with no API key pasted anywhere. It is OAuth 2.1 with PKCE, for developers who build a company portal, a product that inserts or publishes Plotwork diagrams, or a browser app with no server.

## TL;DR

| Question | Answer |
|---|---|
| Who is it for? | An app whose own users have Plotwork accounts: a developer portal such as Backstage that shows each engineer their organisation's private library, a product that lets its users insert or publish diagrams, or a browser app. |
| When do I use something else? | A public website whose visitors have no Plotwork account shows diagrams with a [stable link or an embed link](https://docs.plotwork.dev/hosted-diagrams.md). CI and scripts use an API key or a [trusted publisher](https://docs.plotwork.dev/trusted-publishers.md). |
| How do I start? | Register the app in the portal under Developers, Developer apps (or an organisation's Developers, Apps). You get a client id, and a client secret for an app with a server. |
| What can an app ask for? | `diagrams:use`, `diagrams:publish`, `library:read`, `profile` and `email`. The person sees each one in plain words and approves once. |
| How long do tokens last? | An access token an hour; a refresh token 30 days, used once, and replaced at each refresh. A refresh token used twice ends the connection. |
| Is there a library? | Yes: `@plotwork.official/sdk/oauth` for TypeScript and `plotwork.oauth` for Python, and the viewer shows private diagrams with the person's token. See [SDKs](https://docs.plotwork.dev/sdks.md). |
| Which plan? | Every plan may register and approve apps. An app's calls spend the plan of the person or organisation that approved it. |
| Can an organisation control it? | Its owner and admins see and end every app connected to it. On Enterprise, "Only approved apps can connect" keeps it to a list. |

## When to use it

| You want to | Use |
|---|---|
| Show a person their own diagrams, private ones included, in your app | Sign in with Plotwork with `library:read` |
| Let a person draw or publish from your product, on their own account and plan | Sign in with Plotwork with `diagrams:use` or `diagrams:publish` |
| Show a published diagram to anyone on a public page | A [stable link or an embed link](https://docs.plotwork.dev/hosted-diagrams.md), no sign-in |
| Draw diagrams from CI, a script or a server job | An API key, or a [trusted publisher](https://docs.plotwork.dev/trusted-publishers.md) in CI |

A token from Sign in with Plotwork belongs to one person and acts only within what they approved. An API key belongs to whoever made it and must stay on a server.

## Register an app

Open the [portal](https://app.plotwork.dev) and choose Developers, Developer apps, for an app of your own. For an app that belongs to an organisation, its owner or an admin chooses the organisation's Developers, Apps; such an app can be limited to the organisation's members.

| Field | Rule |
|---|---|
| Name | 3 to 60 characters. People see it on the consent page as a claim, beside where the app sends them back. |
| Homepage | Optional, `https`. |
| Redirect addresses | One to ten. `https`, or `http` on `localhost`, `127.0.0.1` or `[::1]` for development and desktop tools. No fragment, no user name or password, at most 2048 characters. A sign-in must name one exactly, port and path included. |
| Type | Confidential, for an app with a server: a secret (`pws_...`) is shown once, and you can make a new one. Public, for a browser or desktop app with no server: no secret. The type cannot change later. |
| Members only | For an organisation's app: only its members can connect it, and only to that organisation. |

An account or an organisation can register 25 apps. The portal shows the client id with a Copy button, the authorize, token and metadata addresses, and the scopes.

## The flow

**Diagram source**

```diagram
sequence "How an app signs a person in" desc="Your app sends the person to Plotwork, the person approves it in the portal, and your app trades the code for tokens that call the API as that person."

actor app     client  "Your app"
actor browser client  "Browser"
actor api     service "Plotwork API"
actor portal  client  "Plotwork portal"

app -> browser : "Redirect to /oauth/authorize with PKCE"
browser -> api : "GET /oauth/authorize"
api --> browser : "Redirect to the consent page"
browser -> portal : "The person approves"
portal --> browser : "Back to your redirect address"
browser --> app : "code, state and iss"
app -> api : "POST /oauth/token with code and verifier"
api --> app : "Access and refresh tokens"
app -> api : "GET /v1/library with Bearer token"
api --> app : "The person's diagrams"
```

*Picture: How an app signs a person in: 4 participants. Your app sends the person to Plotwork, the person approves it in the portal, and your app trades the code for tokens that call the API as that person.*

**Text description**

**Participants:**

- Your app, client
- Browser, client
- Plotwork API, service
- Plotwork portal, client

**Steps:**

- 1. Your app to Browser: Redirect to /oauth/authorize with PKCE
- 2. Browser to Plotwork API: GET /oauth/authorize
- 3. Plotwork API to Browser (dashed): Redirect to the consent page
- 4. Browser to Plotwork portal: The person approves
- 5. Plotwork portal to Browser (dashed): Back to your redirect address
- 6. Browser to Your app (dashed): code, state and iss
- 7. Your app to Plotwork API: POST /oauth/token with code and verifier
- 8. Plotwork API to Your app (dashed): Access and refresh tokens
- 9. Your app to Plotwork API: GET /v1/library with Bearer token
- 10. Plotwork API to Your app (dashed): The person's diagrams

| Step | What happens |
|---|---|
| 1. Authorize | Your app makes a PKCE verifier and its S256 challenge and a random `state`, keeps them, and sends the browser to `https://api.plotwork.dev/oauth/authorize`. |
| 2. Approve | Plotwork checks the app and the address, and shows the consent page. A person who is signed out signs in first. They see the app's name, where it sends them back, each scope, and "Connect as": their own account or an organisation. |
| 3. Code | The browser comes back to your redirect address with `code`, `state` and `iss`. The code works once, for 60 seconds. |
| 4. Exchange | Your app checks `state` and `iss`, then posts the code and the verifier to `/oauth/token` and gets an access token and a refresh token. |
| 5. Call | Every `/v1` call sends `Authorization: Bearer pwa_...`. |
| 6. Refresh | Before the hour is up, your app trades the refresh token for a new pair. |

A sign-in link is good for ten minutes and is answered once. A person who opens it again is told it was used, and starts again from your app.

## Scopes

| Scope | Lets the app | Routes |
|---|---|---|
| `diagrams:use` | Validate, draw, render and import: computation on what the app sends | Every `/v1` route that computes |
| `diagrams:publish` | Publish a diagram on the account or organisation | `POST /v1/diagrams/publish` |
| `library:read` | List and show the diagrams the account can see, private ones included, under the organisation's folder rules | `GET /v1/library`, `GET /v1/library/{id}.svg` |
| `profile` | Read the account's id and name | `GET /v1/me` |
| `email` | Add the email address to `GET /v1/me` (with `profile`) | `GET /v1/me` |

Ask only for what your feature needs. A route outside the approved scopes answers 403 `oauth.insufficient_scope` with the scope in `details.scope`. A token never opens the live whiteboards, the portal's own routes, or an organisation's settings.

## Tokens

| Token | Looks like | Lasts | Notes |
|---|---|---|---|
| Access token | `pwa_` and 64 hex characters | One hour (`expires_in` is 3600) | Send it as `Authorization: Bearer`. |
| Refresh token | `pwr_` and 64 hex characters | 30 days, once | Each refresh answers a new one with a fresh 30 days, and the old one stops working. |
| Code | `pwc_` and 64 hex characters | 60 seconds, once | Bound to your client, the redirect address and the PKCE challenge. |

A refresh token or a code that is used a second time ends the whole connection, because only a stolen copy is used twice. The person then has to approve the app again. Plotwork stores every token, code and secret only as a SHA-256 hash, so ending a connection ends its tokens at their next call.

Approving the same app again for the same account replaces the earlier connection.

## Endpoints

The issuer is `https://api.plotwork.dev`. The metadata at [`/.well-known/oauth-authorization-server`](https://docs.plotwork.dev/api/get--well-known-oauth-authorization-server.md) lists the endpoints, PKCE S256 only, the client authentication methods (`client_secret_basic`, `client_secret_post` and `none`), the five scopes, and `authorization_response_iss_parameter_supported`.

### Authorize

[`GET /oauth/authorize`](https://docs.plotwork.dev/api/get-oauth-authorize.md), in the browser.

| Parameter | Value |
|---|---|
| `response_type` | `code` |
| `client_id` | Your app's client id |
| `redirect_uri` | One of the app's registered addresses, exactly. May be left out when the app has one. |
| `scope` | One or more scopes, separated by spaces |
| `state` | A random value you keep and check, at most 1024 characters |
| `code_challenge` | The base64url SHA-256 of the verifier, 43 characters |
| `code_challenge_method` | `S256` |

An unknown `client_id` is 400 `oauth.invalid_client`, and an address the app did not register 400 `oauth.invalid_redirect_uri`, shown on the API host and never sent to the address. Every other mistake goes back to your address with `error` (`unsupported_response_type`, `invalid_request` or `invalid_scope`), `error_description`, `state` and `iss`. A person who says no comes back with `error=access_denied`.

### Token

[`POST /oauth/token`](https://docs.plotwork.dev/api/post-oauth-token.md), form-encoded, from your server or the browser app.

| Grant | Fields |
|---|---|
| `authorization_code` | `grant_type`, `code`, `redirect_uri` (the same one), `code_verifier`, `client_id` |
| `refresh_token` | `grant_type`, `refresh_token`, `client_id`, and optional `scope` naming only scopes the connection holds |

A confidential app sends its secret by HTTP Basic or as `client_secret` in the form, once. The answer is `access_token`, `token_type` (`Bearer`), `expires_in`, `refresh_token` and `scope`, with `cache-control: no-store`. A refusal is OAuth's own shape:

```json
{
  "error": "invalid_grant",
  "error_description": "The code is not valid, has expired, or was already used."
}
```

`invalid_client` is 401; `invalid_request`, `invalid_grant`, `invalid_scope` and `unsupported_grant_type` are 400. The endpoint answers any origin, without credentials, and is rate-limited by address.

### Revoke

[`POST /oauth/revoke`](https://docs.plotwork.dev/api/post-oauth-revoke.md), form-encoded, with `token` and the same client authentication. An access token ends; a refresh token ends the whole connection, which is how an app signs a person out. An unknown token is answered 200 as well.

### Calls

[`GET /v1/me`](https://docs.plotwork.dev/api/get-v1-me.md) answers who the token acts as. [`GET /v1/library`](https://docs.plotwork.dev/api/get-v1-library.md) answers a page of diagrams (`limit`, `cursor`, `folder`), and [`GET /v1/library/{id}.svg`](https://docs.plotwork.dev/api/get-v1-library-id-svg.md) one picture with an ETag. Every other `/v1` operation is in the [API reference](https://docs.plotwork.dev/api/reference.md).

## An app with a server

A confidential app keeps the secret and the tokens on its server; the browser never sees a token. With the TypeScript SDK, as Next.js route handlers:

```ts
// app/auth/plotwork/route.ts: send the person to Plotwork.
import { authorizeUrl, createPkce } from '@plotwork.official/sdk/oauth'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

const clientId = process.env.PLOTWORK_CLIENT_ID ?? ''
const redirectUri = 'https://example.com/auth/plotwork/callback'

export async function GET() {
  const pkce = await createPkce()
  const { url, state } = authorizeUrl({ clientId, redirectUri, scopes: ['library:read', 'profile'], codeChallenge: pkce.challenge })
  const jar = await cookies()
  jar.set('plotwork_sign_in', JSON.stringify({ state, verifier: pkce.verifier }), { httpOnly: true, secure: true, sameSite: 'lax', maxAge: 600 })
  redirect(url)
}
```

```ts
// app/auth/plotwork/callback/route.ts: trade the code for tokens.
import { exchangeCode, parseRedirect } from '@plotwork.official/sdk/oauth'
import { cookies } from 'next/headers'
import { redirect } from 'next/navigation'

const clientId = process.env.PLOTWORK_CLIENT_ID ?? ''
const clientSecret = process.env.PLOTWORK_CLIENT_SECRET
const redirectUri = 'https://example.com/auth/plotwork/callback'

export async function GET(request: Request) {
  const jar = await cookies()
  const { state, verifier } = JSON.parse(jar.get('plotwork_sign_in')?.value ?? '{}')
  jar.delete('plotwork_sign_in')
  const code = parseRedirect(request.url, state) // throws on a foreign state, another iss, or error=
  const tokens = await exchangeCode({ clientId, clientSecret, code, redirectUri, codeVerifier: verifier })
  await saveToSession(tokens) // your server-side session store
  redirect('/')
}
```

Then, for each request:

```ts
import { Plotwork } from '@plotwork.official/sdk'
import { withRefresh } from '@plotwork.official/sdk/oauth'

const plotwork = new Plotwork({
  accessToken: withRefresh(session.tokens, { clientId, clientSecret, onRefresh: (tokens) => saveToSession(tokens) }),
})
const { items } = await plotwork.library.list()
```

`withRefresh` refreshes a minute before the access token ends, once however many calls ask, and hands each new pair to `onRefresh` to save. Use one per connection. Every refusal throws `PlotworkOAuthError` with OAuth's `error`, its `description`, and the HTTP `status`.

| Helper | What it does |
|---|---|
| `createPkce()` | A verifier and its S256 challenge |
| `authorizeUrl(...)` | The address to send the browser to, and its `state` |
| `parseRedirect(url, state)` | The code, after checking `state`, `iss` and `error` |
| `exchangeCode(...)` | The token set |
| `refreshToken(...)` | A new token set |
| `revokeToken(...)` | Signs the person out |
| `withRefresh(tokens, ...)` | An access token function that refreshes on its own |

## A browser app with no server

A public app has no secret. The tokens live in the page's memory, and the refresh token is replaced at every use.

```ts
import { Plotwork } from '@plotwork.official/sdk'
import { authorizeUrl, createPkce, exchangeCode, parseRedirect, withRefresh } from '@plotwork.official/sdk/oauth'

const clientId = 'your client id'
const redirectUri = `${location.origin}/callback`

export async function signIn(): Promise<void> {
  const pkce = await createPkce()
  const { url, state } = authorizeUrl({ clientId, redirectUri, scopes: ['library:read'], codeChallenge: pkce.challenge })
  sessionStorage.setItem('plotwork-sign-in', JSON.stringify({ state, verifier: pkce.verifier }))
  location.assign(url)
}

// On the page at redirectUri:
export async function finishSignIn(): Promise<Plotwork> {
  const { state, verifier } = JSON.parse(sessionStorage.getItem('plotwork-sign-in') ?? '{}')
  sessionStorage.removeItem('plotwork-sign-in')
  const code = parseRedirect(location.href, state)
  const tokens = await exchangeCode({ clientId, code, redirectUri, codeVerifier: verifier })
  return new Plotwork({ accessToken: withRefresh(tokens, { clientId, onRefresh: () => undefined }) })
}
```

`/v1`, `/oauth/token` and `/oauth/revoke` answer any origin without credentials, so the page calls them from its own origin. A desktop or command line tool works the same way with a loopback redirect address such as `http://127.0.0.1:8765/callback`, registered with its port.

## Python

`plotwork.oauth` has the same helpers for a server app, on the standard library alone:

```python
import os

from plotwork import Plotwork
from plotwork.oauth import authorize_url, create_pkce, exchange_code, parse_redirect, with_refresh

CLIENT_ID = os.environ["PLOTWORK_CLIENT_ID"]
CLIENT_SECRET = os.environ["PLOTWORK_CLIENT_SECRET"]
REDIRECT_URI = "https://example.com/auth/plotwork/callback"

def start(session):
    pkce = create_pkce()
    request = authorize_url(client_id=CLIENT_ID, redirect_uri=REDIRECT_URI, scopes=["library:read", "profile"], code_challenge=pkce.challenge)
    session["plotwork_sign_in"] = {"state": request.state, "verifier": pkce.verifier}
    return request.url  # redirect the browser here

def callback(session, url):
    saved = session.pop("plotwork_sign_in")
    code = parse_redirect(url, saved["state"])
    session["plotwork_tokens"] = exchange_code(
        client_id=CLIENT_ID, client_secret=CLIENT_SECRET, code=code, redirect_uri=REDIRECT_URI, code_verifier=saved["verifier"]
    )

def client_for(session):
    def save(tokens):
        session["plotwork_tokens"] = tokens

    token = with_refresh(session["plotwork_tokens"], client_id=CLIENT_ID, client_secret=CLIENT_SECRET, on_refresh=save)
    return Plotwork(access_token=token)
```

`client_for(session).library.list()` then lists the person's diagrams. A refusal raises `PlotworkOAuthError`.

## Show private diagrams

With `library:read`, the viewer shows any diagram the person can see, private ones included, from `/v1/library/{id}.svg` with their token, and `<plotwork-library>` lets them pick one.

```tsx
import { PlotworkDiagram, PlotworkLibrary } from '@plotwork.official/viewer/react'
import { useState } from 'react'

export function Picker({ token }: { token: () => Promise<string> }) {
  const [picture, setPicture] = useState<string | null>(null)
  return (
    <>
      <PlotworkLibrary token={token} onSelect={({ pictureUrl }) => setPicture(pictureUrl)} />
      {picture !== null && <PlotworkDiagram src={picture} token={token} live />}
    </>
  )
}
```

The viewer sends the token only to an `https` address (or `http` on `localhost`), never with a cookie. A 401 is the `signed-out` event, and setting a fresh token loads the picture again. Show a private picture only to the person who signed in, never at a public address.

## Organisations

| Topic | What happens |
|---|---|
| Connect as | The consent page offers the person's own account and each organisation where they may publish and meet its single sign-on rule. A connection to an organisation reads its library and spends its plan. |
| Members only | An organisation's app registered for members only can be connected by its members, to that organisation alone. |
| Allowed apps | On Enterprise, an owner or admin can turn on "Only approved apps can connect" in the organisation's Settings, Security and access, Allowed apps, and list up to 50 client ids. The organisation's own apps are always allowed. An app off the list cannot be connected to the organisation (403 `oauth.app_not_allowed`), and taking one off ends its connections. |
| What admins see | The organisation's Developers, Apps shows its own apps and every app connected to it, who connected each, and Disconnect. |
| Audit events | `orgs.app.connect` and `orgs.app.disconnect` (reason `disconnected`, `replaced`, `not_allowed`, `member_left`, `revoked` or `reused`), `orgs.app.create`, `orgs.app.update`, `orgs.app.delete`, `orgs.app.rotate`, and `orgs.settings.allowed_apps`, in the [audit log](https://docs.plotwork.dev/audit-log.md). Never a token. |
| Call history | An app's calls to an organisation are kept in its call history like calls made with its keys. |

A connection to an organisation ends when the member who made it leaves or becomes a reader, or when the organisation is deleted.

## Disconnect

A person ends a connection in the portal's Settings, under Connected apps. An organisation's owner and admins end any connection to the organisation from its Apps section. Deleting an app ends every connection to it. Your app signs a person out by revoking its refresh token. After any of these, the next call answers 401 `oauth.invalid_token` and the next refresh `invalid_grant`: delete what you hold for that person and offer the sign-in button again.

## Limits and plans

| Rule | Detail |
|---|---|
| Who may register | Every plan, up to 25 apps an account or organisation |
| Who may approve | Every plan |
| What a call spends | The daily allowance and rate limits of the person or organisation that approved the app, as [Plans and billing](https://docs.plotwork.dev/plans-and-billing.md) describes. A Free account's connection gets Free limits. |
| `/v1/me` and the library | Open on every plan, metered and rate-limited like any call |
| The token endpoint | Rate-limited by address |

## Security

| Do | Why |
|---|---|
| Treat the app name on the consent page as a claim | Anyone can register an app with any name. People see where it sends them back beside it; so do you, when you approve one. |
| Keep a client secret on a server | Never put a secret, or a confidential app's refresh token, in a browser, a mobile app or a repository. A browser app registers as public. |
| Use PKCE and `state` on every sign-in, and check `iss` | They stop a code from being stolen or injected. `parseRedirect` checks all three. |
| Keep tokens out of logs and addresses | A token in a log or a query string can be read by anyone who sees it. |
| Make a new secret if one was seen | The old one stops working at once. |

Building an app means accepting the [developer terms](https://docs.plotwork.dev/developer-terms.md): use what you receive only for the person's request, never sell it, and delete it when they disconnect.

## The button

Start the sign-in from the Sign in with Plotwork button, for a light page or a dark page:

*Picture: Sign in with Plotwork*

Both files, `plotwork-sign-in-on-light.svg` and `plotwork-sign-in-on-dark.svg`, are in the `buttons` folder of the [brand kit](https://plotwork.dev/brand). Serve a copy from your own site, or link them as below, and show them as they are, 40 pixels tall:

```html
<a class="plotwork-sign-in" href="/auth/plotwork">
  <picture>
    <source media="(prefers-color-scheme: dark)" srcset="https://assets.plotwork.dev/brand/buttons/plotwork-sign-in-on-dark.svg">
    <img src="https://assets.plotwork.dev/brand/buttons/plotwork-sign-in-on-light.svg" alt="Sign in with Plotwork" width="203" height="40">
  </picture>
</a>
```

Or draw it in your own markup, with the same measurements and colours:

```html
<a class="plotwork-sign-in-button" href="/auth/plotwork">
  <svg viewBox="3 3 26 26" width="20" height="20" aria-hidden="true">
    <defs>
      <linearGradient id="plotwork-mark" gradientUnits="userSpaceOnUse" x1="2" y1="3" x2="30" y2="29">
        <stop offset="0" stop-color="#c6f04a"/><stop offset="0.55" stop-color="#14b8a6"/><stop offset="1" stop-color="#0f766e"/>
      </linearGradient>
    </defs>
    <path d="M3 3H14A11 11 0 0 1 3 14Z" fill="url(#plotwork-mark)"/>
    <path d="M29 3V14A11 11 0 0 1 18 3Z" fill="url(#plotwork-mark)"/>
    <path d="M29 29H18A11 11 0 0 1 29 18Z" fill="url(#plotwork-mark)"/>
    <path d="M3 29V18A11 11 0 0 1 14 29Z" fill="#0f766e"/>
  </svg>
  <span>Sign in with Plotwork</span>
</a>
```

```css
.plotwork-sign-in-button {
  display: inline-flex;
  align-items: center;
  gap: 10px;
  height: 40px;
  padding: 0 16px;
  border: 1px solid #dcdcdc;
  border-radius: 8px;
  background: #ffffff;
  color: #171717;
  font: 500 15px/1 'IBM Plex Sans', system-ui, sans-serif;
  text-decoration: none;
}

.plotwork-sign-in-button:focus-visible {
  outline: 2px solid #0f766e;
  outline-offset: 2px;
}

@media (prefers-color-scheme: dark) {
  .plotwork-sign-in-button {
    border-color: #404040;
    background: #141414;
    color: #fafafa;
  }
}
```

In React, the same button as a component:

```tsx
export function SignInWithPlotwork({ href = '/auth/plotwork' }: { href?: string }) {
  return (
    <a className="plotwork-sign-in-button" href={href}>
      <svg viewBox="3 3 26 26" width="20" height="20" aria-hidden="true">
        <defs>
          <linearGradient id="plotwork-mark" gradientUnits="userSpaceOnUse" x1="2" y1="3" x2="30" y2="29">
            <stop offset="0" stopColor="#c6f04a" />
            <stop offset="0.55" stopColor="#14b8a6" />
            <stop offset="1" stopColor="#0f766e" />
          </linearGradient>
        </defs>
        <path d="M3 3H14A11 11 0 0 1 3 14Z" fill="url(#plotwork-mark)" />
        <path d="M29 3V14A11 11 0 0 1 18 3Z" fill="url(#plotwork-mark)" />
        <path d="M29 29H18A11 11 0 0 1 29 18Z" fill="url(#plotwork-mark)" />
        <path d="M3 29V18A11 11 0 0 1 14 29Z" fill="#0f766e" />
      </svg>
      <span>Sign in with Plotwork</span>
    </a>
  )
}
```

Keep the words and the mark as they are, and do not suggest that Plotwork made or endorses your app. The [trademarks and icons](https://docs.plotwork.dev/trademarks-and-icons.md) page has the rules for the logo.

## Troubleshooting

| Error | Where | Means | Fix |
|---|---|---|---|
| `oauth.invalid_client` | `/oauth/authorize`, 400 | No app has that client id | Copy the client id from the portal again. |
| `oauth.invalid_redirect_uri` | `/oauth/authorize`, 400 | The address is not one the app registered | Register it exactly, with its port and path, or send one that is. |
| `invalid_request` | Your redirect address, or `/oauth/token` | PKCE missing or not S256, a challenge of the wrong shape, a `state` too long, a missing field, a field sent twice, or a body that is not form-encoded | Send every field once, form-encoded, with an S256 challenge. |
| `invalid_scope` | Your redirect address, or a refresh | A scope Plotwork does not know, or a refresh asking for more than the connection holds | Ask only for the five scopes; a refresh may only narrow. |
| `access_denied` | Your redirect address | The person said no | Show the button again. |
| `invalid_client` | `/oauth/token`, `/oauth/revoke`, 401 | The secret is wrong, sent twice, or missing for a confidential app | Send the current secret once, by HTTP Basic or `client_secret`. |
| `invalid_grant` | `/oauth/token`, 400 | The code or refresh token expired, was used already, belongs to another app or address, the verifier is wrong, or the connection ended | Sign the person in again. A reuse also ended the connection. |
| `unsupported_grant_type` | `/oauth/token`, 400 | A grant other than `authorization_code` or `refresh_token` | Use one of the two. |
| `oauth.invalid_token` | `/v1`, 401 | The access token is unknown, expired or its connection ended | Refresh, or sign in again. |
| `oauth.insufficient_scope` | `/v1`, 403 | The connection lacks the scope in `details.scope` | Ask the person to approve the app again with that scope. |
| `oauth.request_used`, `oauth.request_expired`, `oauth.request_invalid` | The consent page | The sign-in link was answered already, is past its ten minutes, or was not made by Plotwork | Start again from the app. |
| `oauth.app_members_only` | The consent page, 403 | The app is an organisation's, for its members only | Connect it as a member, to that organisation. |
| `oauth.app_not_allowed` | The consent page, 403 | The organisation allows only approved apps, and this is not one | Ask an owner or admin to add the client id under Allowed apps, or connect another account. |
| `apps.invalid` | Registering an app, 422 | A redirect address or homepage breaks the rules, or a public app was given a secret | Follow the rules in Register an app. |
| `apps.limit_reached` | Registering an app, 409 | The account or organisation already has 25 apps | Delete one first. |
| `oauth.unavailable` | Any endpoint, 503 | Sign in with Plotwork is not available on this server | Use `https://api.plotwork.dev`, or a server where it is turned on. |

## See also

| Page | Why |
|---|---|
| [SDKs](https://docs.plotwork.dev/sdks.md) | The TypeScript and Python clients and the viewer |
| [Developer terms](https://docs.plotwork.dev/developer-terms.md) | What an app may do with what it receives |
| [API reference](https://docs.plotwork.dev/api/reference.md) | Every `/v1` operation a token can call |
| [Start Sign in with Plotwork](https://docs.plotwork.dev/api/get-oauth-authorize.md) | The authorize endpoint in detail |
| [Trade a code or a refresh token](https://docs.plotwork.dev/api/post-oauth-token.md) | The token endpoint in detail |
| [Hosted diagrams](https://docs.plotwork.dev/hosted-diagrams.md) | Stable links and embed links, for pages with no sign-in |
| [Organisations](https://docs.plotwork.dev/organisations.md) | Roles, the private library and the owner's settings |
| [Privacy policy](https://docs.plotwork.dev/privacy-policy.md) | What Plotwork keeps about a connection |
