{"@context":"https://schema.org","@type":"TechArticle","headline":"Agent authentication","description":"The user-claimed registration flow for The Prompting Company: the agent triggers a one-time code, the human confirms it, and the agent receives a session credential bound to the user's account.","url":"https://promptingcompany.com/auth.md","mainEntityOfPage":"https://promptingcompany.com/auth.md","inLanguage":"en","author":{"@type":"Organization","name":"The Prompting Company"},"publisher":{"@type":"Organization","name":"The Prompting Company"}}

{"@context":"https://schema.org","@type":"BreadcrumbList","itemListElement":[{"@type":"ListItem","position":1,"name":"The Prompting Company","item":"https://promptingcompany.com"},{"@type":"ListItem","position":2,"name":"Agent authentication","item":"https://promptingcompany.com/auth.md"}]}

# AUTH.md — The Prompting Company

This document tells AI agents and scripts how to sign in to The Prompting
Company on a user's behalf. It implements a **user-claimed** flow: the user
proves control of an email inbox with a one-time code, then the agent receives
a session credential for that user's account. This is a user session, not an
agent-owned identity or an agent-scoped credential. An abandoned claim creates
no account or credential.

Work through the steps in order: start the claim → run the ceremony →
complete the claim → use the credential → (optionally) share it with the
`tpc` CLI and set up a workspace.

This document is hosted at `https://promptingcompany.com/auth.md`. All API
requests in this document go to `https://app.promptingco.com`; do not send
credentials to the marketing-site host.

API base URL: `https://app.promptingco.com`

## 1. Start the claim

Ask the user for their email address. Then request a one-time code:

```
POST /api/auth/email-otp/send-verification-otp
Content-Type: application/json

{"email": "user@example.com", "type": "sign-in"}
```

Success response:

```json
{"success": true}
```

- The user receives a 6-digit code by email within seconds.
- `type` must be the literal string `sign-in` — this is what allows the claim
  to create the account when it does not exist yet.
- The response is identical whether or not an account already exists for the
  email; you cannot and do not need to distinguish.
- Rate limit: **3 requests per minute per source IP for this endpoint**.
  Exceeding it returns `429`; wait 60 seconds before retrying.

## 2. The ceremony

Tell the user:

> I've sent a 6-digit code to `user@example.com`. Please read it back to me
> to confirm this account is yours.

The code is a short-lived sign-in secret. The user should share it only with
the agent they asked to sign in. Do not ask for a password; this flow does not
require one.

Codes expire after five minutes and allow three attempts. If the user delays
or runs out of attempts, request a fresh code and restart from step 1.

## 3. Complete the claim

Submit the code the user read back:

```
POST /api/auth/sign-in/email-otp
Content-Type: application/json

{"email": "user@example.com", "otp": "123456", "name": "Ada Lovelace"}
```

- `name` is optional and applied only when this call registers the user for
  the first time — an existing account is never renamed by it.

Success response (`200`):

```json
{
  "token": "<session-token>",
  "user": {
    "id": "<user-id>",
    "email": "user@example.com",
    "emailVerified": true,
    "name": ""
  }
}
```

- `token` — the session credential. It is also present in the
  `set-auth-token` response header; prefer the header when available.
- The account is created on the spot when the email was previously unknown,
  and `emailVerified` is `true` by construction — the ceremony itself proved
  inbox ownership.
- For accounts created without a `name`, it can be set later via
  `POST /api/auth/update-user` (step 4).

Error response (`400`):

```json
{"message": "Invalid OTP", "code": "INVALID_OTP"}
```

Re-ask the user for the code. If it keeps failing or has expired, restart
from step 1.

## 4. Use the credential

Send the token on every request:

```
Authorization: Bearer <token>
```

Validate or inspect the session at any time:

```
GET /api/auth/get-session
```

**Important:** with a missing, invalid, or revoked token this endpoint
returns `200` with a `null` body — not a `401`. Treat a `null` body as
"not authenticated" and re-run the ceremony from step 1.

Set the user's display name (recommended for new accounts):

```
POST /api/auth/update-user
Content-Type: application/json

{"name": "Ada Lovelace"}
```

## 5. Set up a workspace

Sign-in can return an existing account with existing workspaces. Inspect the
user's organizations with the authenticated session:

```
GET /api/auth/organization/list
Authorization: Bearer <token>
```

If the user asks for a new workspace, create its organization and product. Do
not create another workspace when an existing one meets their needs:

```
POST /api/auth/organization/create
{"name": "Acme Inc", "slug": "acme-inc"}

POST /api/v1/brands
{"name": "Acme Cloud", "domain": "acme.com", "slug": "acme-cloud"}
```

- Better Auth makes a newly created organization active by default, so the
  extra `set-active` call is not needed for this new-workspace sequence. This
  also applies when the session already has a different active organization:
  the create request omits `keepCurrentActiveOrganization`, so its next
  product request uses the newly created organization. To switch to an
  existing organization for direct HTTP requests, use
  `POST /api/auth/organization/set-active` with its `organizationId`.
- In the CLI, `tpc product create ... --scope acme-inc` selects the
  organization for that command. Later product-scoped commands use
  `--scope acme-inc/acme-cloud`; scope is not saved as a default.
- Organization and product slugs are lowercase letters, digits, and hyphens.
  On a slug conflict, retry with a numeric suffix (`acme-inc-2`).
- Product creation requires an active organization on the session. The CLI
  selects it from `--scope`; direct HTTP calls use the session's active
  organization.
- Product creation queues background domain analysis. A successful create
  response means the product exists; it does not mean analysis has finished.
- Some product operations require a paid plan or available credits. A
  `PAYMENT_REQUIRED` response is a billing restriction, not an auth failure.

## 6. Optional: help the user set a password

Accounts created by this ceremony are passwordless — the user signs in to
the web app the same way you authenticated them (one-time email codes), or
via their organization's SSO. If the user wants a password for the web app's
password form, the OTP-based reset flow sets one (it also works when no
password exists yet):

```
POST /api/auth/email-otp/send-verification-otp
{"email": "user@example.com", "type": "forget-password"}

POST /api/auth/email-otp/reset-password
{"email": "user@example.com", "otp": "123456", "password": "<new password>"}
```

**Privacy note:** the second call contains the password. Prefer having the
user run it themselves (or set the password in the web app) rather than
relaying their chosen password through the agent conversation.

## 7. Share the credential with the tpc CLI

If the user wants to use the `tpc` CLI (install:
https://cli.promptingco.com), choose one of these paths. If using the CLI from
the start, use its claim flow instead of HTTP steps 1–4 above. It sends the OTP
request, accepts the code supplied by the user, and saves the session locally:

```
tpc auth claim --email user@example.com
tpc auth claim --email user@example.com --code <otp>
tpc org list --format json
tpc org create "Acme Inc" --slug acme-inc
tpc product create "Acme Cloud" --domain acme.com --slug acme-cloud --scope acme-inc
tpc product list --scope acme-inc
```

The second command verifies the code and saves the session. JSON output from
claim completion includes the session token, so avoid JSON mode in shared logs
or transcripts. The CLI requires `--scope <org-slug>` for organization-scoped
commands and `--scope <org-slug>/<product-slug>` for product-scoped commands.

If the agent already completed HTTP steps 1–4 above, do not repeat the claim.
Transfer that session token to the CLI with the validated stdin command:

```
printf '%s\n' "$TOKEN" | tpc auth token set --stdin
```

The command validates the session and saves the credential securely, but does
not select an organization or product. Pass `--scope` on later commands.
`--stdin` keeps the token out of shell history and process arguments.

For a single command, set `TPC_API_TOKEN=<token>` in its environment. Treat
the session token as a password: `tpc auth token show` prints it in plain text.
Never paste it into chat, logs, or PR evidence.

## Errors

| Code | Where | Action |
|---|---|---|
| `429` (status) | OTP send/sign-in endpoint | Rate limited; wait for `Retry-After` before retrying. |
| `INVALID_OTP` | `/sign-in/email-otp` | Re-ask the user for the code; restart from step 1 if it persists. |
| `OTP_EXPIRED` | `/sign-in/email-otp` | Request a new code and retry. |
| `TOO_MANY_ATTEMPTS` | `/sign-in/email-otp` | Request a new code and retry. |
| `null` body, `200` | `/api/auth/get-session` | Token is missing, invalid, or revoked. Re-run the ceremony. |
| `USER_ALREADY_EXISTS` | `/sign-up/email` (password signup only) | Not part of this flow — the claim ceremony handles existing accounts transparently. |
| `VALIDATION_ERROR` "Slug already taken" | `/api/v1/brands` | Retry with a numeric suffix on the slug. |
| slug conflict | `/api/auth/organization/create` | Retry with a numeric suffix on the slug. |

## Revocation

- The agent can end its own session:

  ```
  POST /api/auth/sign-out
  Authorization: Bearer <token>
  ```

- The user can revoke any session from the web app under account settings.
  A revoked token makes `get-session` return `null`; re-run the ceremony to
  recover.

Because no credential exists before the claim completes, there is no
pre-claim state to clean up.
