# Personal API Keys (/docs/reference/api-keys)

Personal API keys let tools use Wesley as the person who created the key, within that person's **current paid-firm access**. Trial and inactive firms cannot create or use personal API access. A user who belongs to both paid and trial firms can use a key only for the paid firms. Read-only access stays read-only. Removing a membership removes that access on subsequent requests. Expired or revoked keys and deleted, banned, or locked accounts cannot authenticate.

**Connecting Claude or ChatGPT?** Start with the [MCP setup guide](/docs/reference/mcp). Copy a setup prompt and approve the connection with your Wesley key.

## For agents: start here

Base URL: `https://www.wesley-ai.co`

1. Read this guide. It is public and needs no credential.
2. Read `WESLEY_API_KEY` from your runtime's secret configuration. Send it only in the `Authorization` header to the Wesley origin.
3. Call `GET /api/users/me` to verify whose key you have.
4. Call `GET /api/users/accounting-firms`, select the intended firm, and call `GET /api/users/companies?accountingFirmId=...` to select the intended client.
5. Read the relevant API contract before making a request. Use returned IDs; ask the user when firm or client selection is ambiguous.
6. Execute the requested workflow using the same `/api/...` endpoints as the web app.

Machine-readable resources:

* [This guide as Markdown](/llms.mdx/docs/reference/api-keys)
* [API specification](/api/swagger.json)
* [One endpoint's contract](/api/swagger.json?path=/api/users/me)
* [Transaction API contracts](/api/swagger.json?tag=Transactions)
* [Documentation index](/llms.txt)
* [All product guides as text](/llms-full.txt)

There is no separate `/api/v1` base. In the specification, `x-wesley-personal-api-key: true` identifies handlers that accept personal keys. Other handlers can require an interactive session, an integration credential, or an internal service token. Key management itself requires signing in to Wesley. OAuth connection and reauthorization flows may require the user to finish in a browser.

Personal keys can call existing APIs marked `x-wesley-personal-api-key: true`; each endpoint still enforces its normal user, company, firm, role, quota, and approval checks. Database schema inspection and arbitrary SQL through `/api/agent/read-query` are never available to personal keys. Browser-session, service-credential, or other unmarked handlers remain unavailable. The public API specification describes HTTP contracts and remains available without a key.

## Create a key

Sign in to Wesley, open **API Keys & MCP** from a firm's sidebar, select **Create key**, and create a personal key with a descriptive name, such as “Claude on my laptop.” Keys belong to you, including when this page is opened inside a firm. Other firm members cannot list or revoke your keys.

Copy the full key when it is first displayed; it cannot be retrieved later. Configure it as `WESLEY_API_KEY` in your agent's environment or secret store. The page's **Copy agent instructions** button provides the guide URL and startup instructions without including the secret.

Creating a key requires recent identity verification in Wesley. You can hold at most 10 active keys and create at most 10 keys per hour. Choose 7, 30, or 90 days (the default), or **No expiration**. A key without expiration remains usable until revoked, as long as your account and paid access remain eligible. It still counts toward the 10-key limit. The session-only creation endpoint accepts `expiresInDays: null` for no expiration; omitting the field keeps the 90-day default. Existing keys retain their expiration dates. Key creation sends an in-app security notification.

Revoke an unused or lost key from the same page. **Revoke all keys** provides immediate recovery if several tools may be compromised. Revocation remains available during rate-limit exhaustion or Redis outages. Create a replacement before revoking an old key if an automation needs an uninterrupted transition. The page shows the latest successful use and expiration. Revoke-all appears when you have multiple unrevoked keys; revoked keys are collapsed under their history.

## Authenticate and identify the owner

These examples assume `WESLEY_API_KEY` has already been supplied securely. Do not place its literal value in shared commands, messages, URLs, or logs. Use an HTTP-capable agent or tool; fetching this public guide alone does not grant access to your data.

```bash
curl --fail-with-body 'https://www.wesley-ai.co/api/users/me' \
  -H "Authorization: Bearer $WESLEY_API_KEY"
```

Example response:

```json
{
  "user": {
    "id": "user_example",
    "name": "Example User",
    "email": "user@example.com"
  },
  "apiKeyId": "00000000-0000-4000-8000-000000000001"
}
```

The server determines the actor from the key. Do not submit a `userId` to impersonate another person. An explicitly supplied personal key determines the actor even if a browser session is also present. A bad or revoked personal key returns an error rather than falling back to a session.

## Identify your tool and check usage

Send the optional `X-Wesley-Client` header on API requests. Use `claude`, `chatgpt`, `codex`, or `script`; other values become `other`, and a missing header becomes `unspecified`. This label is self-reported, grants no permissions, and does not prove which tool made the request. Do not put a person, company, prompt, or credential in this header.

```bash
curl --fail-with-body 'https://www.wesley-ai.co/api/users/me' \
  -H "Authorization: Bearer $WESLEY_API_KEY" \
  -H 'X-Wesley-Client: claude'
```

The first completed HTTP response with a 2xx status marks first use; `GET /api/users/me` is a suitable connection check. A 202 response only means a job was accepted; check that job's status endpoint for completion. Keys do not maintain an online/offline connection session.

**API Keys & MCP** shows the latest successful request as **Last used**. First-success timestamps and detailed usage counts remain available to Wesley operators. Older authenticated-use timestamps are not relabeled as successful requests. Wesley operators can see hourly counts for up to 90 days by owner, key, normalized API route, HTTP method/status, coarse feature, and self-reported tool. They receive creation, first-use, revocation, known-key policy-block, and repeated-error events through Notiham in `#noti-event`.

This tracking does not store the key secret, request body, query strings, dynamic resource IDs, raw URL, IP address, or full user agent. Requests rejected before key identification are not attributed to an owner. Response tracking is best effort if the database/runtime is unavailable; key lifecycle events and queued notifications are durable.

## Select a firm and client

```bash
curl --fail-with-body 'https://www.wesley-ai.co/api/users/accounting-firms' \
  -H "Authorization: Bearer $WESLEY_API_KEY"
```

Read the `accountingFirms` array and select the intended firm's `id`. Use that returned ID as `FIRM_ID`:

```bash
curl --fail-with-body --get 'https://www.wesley-ai.co/api/users/companies' \
  -H "Authorization: Bearer $WESLEY_API_KEY" \
  --data-urlencode "accountingFirmId=$FIRM_ID"
```

Read the `companies` array and use the selected company's `id` as `COMPANY_ID`. The company list defaults to active clients; `status=active,inactive` includes both. Always provide the firm explicitly: without it, this endpoint selects one firm automatically and does not enumerate clients across all firms.

For client-specific handlers using company context, send `x-company-id: $COMPANY_ID`. For firm-specific handlers, send `x-accounting-firm-id: $FIRM_ID`. Follow the individual endpoint's contract when it requires IDs in the query, body, or path. These IDs select the target; they do not grant access to it.

## Find the right operation

Fetch a small part of the specification rather than reading every API at once:

```bash
curl --fail-with-body --get 'https://www.wesley-ai.co/api/swagger.json' \
  --data-urlencode 'path=/api/transactions'
```

`path` matches an exact path template, including placeholders such as `/api/document-conversion-files/{id}/download`. `tag` matches a category such as `Transactions`, `Reports`, or `Journal Entries`. The full specification lists all paths and tags. Contracts are method-specific: explicit schemas identify required and optional fields, while fields inferred from handlers are optional unless stated otherwise. Read operation descriptions and validation errors for conditional rules. Do not invent endpoints, fields, or supported HTTP methods.

| Task                       | Existing endpoint                    | Notes                                                    |
| -------------------------- | ------------------------------------ | -------------------------------------------------------- |
| Confirm key owner          | `GET /api/users/me`                  | Returns user and key ID                                  |
| List firms                 | `GET /api/users/accounting-firms`    | Returns `accountingFirms`                                |
| List clients               | `GET /api/users/companies`           | Query `accountingFirmId`; returns `companies`            |
| List ledger accounts       | `GET /api/chart-of-accounts`         | Company context required                                 |
| Read transactions          | `GET /api/transactions`              | Company context; use date filters and pagination         |
| Read transaction summaries | `GET /api/transactions/summary`      | Prefer summaries for counts and totals                   |
| Read journal entries       | `GET /api/journal-entries`           | Company context; inspect filters in the contract         |
| Read P\&L or balance sheet | `GET /api/reports/statement`         | `report_type`, `start_date`, `end_date`, `basis`         |
| Import bank statements     | `POST /api/bank-statement-imports`   | Multipart upload; current write permission required      |
| Read conversion progress   | `GET /api/document-conversion-files` | Query `status=all` to include completed/reviewable files |

## Example: read transactions

```bash
curl --fail-with-body --get 'https://www.wesley-ai.co/api/transactions' \
  -H "Authorization: Bearer $WESLEY_API_KEY" \
  -H "x-company-id: $COMPANY_ID" \
  --data-urlencode 'start_date=2026-08-01' \
  --data-urlencode 'end_date=2026-08-31' \
  --data-urlencode 'page=1' \
  --data-urlencode 'page_size=25'
```

Use the returned pagination metadata. Read additional pages only when needed. For whole-period totals, use summary/report endpoints rather than summing one page of transactions.

## Example: read a profit and loss statement

```bash
curl --fail-with-body --get 'https://www.wesley-ai.co/api/reports/statement' \
  -H "Authorization: Bearer $WESLEY_API_KEY" \
  -H "x-company-id: $COMPANY_ID" \
  --data-urlencode 'report_type=profit-loss' \
  --data-urlencode 'start_date=2026-08-01' \
  --data-urlencode 'end_date=2026-08-31' \
  --data-urlencode 'basis=accrual'
```

Use `report_type=balance-sheet` for a balance sheet. Select `cash` or `accrual` according to the user's request.

## Example: upload a bank statement

Choose the appropriate banking account from the client's accounts and set `ACCOUNT_ID` to its returned ID. Set `IMPORT_ID` to a new UUID for this import, and preserve that UUID if retrying the same files and parameters.

```bash
curl --fail-with-body 'https://www.wesley-ai.co/api/bank-statement-imports' \
  -H "Authorization: Bearer $WESLEY_API_KEY" \
  -H "x-company-id: $COMPANY_ID" \
  -F "companyId=$COMPANY_ID" \
  -F "accountId=$ACCOUNT_ID" \
  -F "idempotencyKey=$IMPORT_ID" \
  -F 'statements=@/absolute/path/to/statement.pdf;type=application/pdf'
```

Let your HTTP client set the multipart boundary. A successful response includes `batchId` and `files`; acceptance is not completion. Follow conversion progress using `/api/document-conversion-files?status=all` with the same company header. Review that endpoint's response and the uploaded file identifiers rather than assuming every returned document belongs to this import. Poll at modest intervals, such as 5–10 seconds with backoff.

Imports use the client's normal Wesley processing and automation settings and may affect bookkeeping data. Run them only as part of the user's requested workflow. Spreadsheet imports can require sheet selection and confirmed mappings; inspect the contract and [Supported Files](/docs/reference/supported-files).

## Errors and retries

| Response             | What to do                                                                                                                                           |
| -------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`                | Read the validation error and fix the request. Check the method-specific contract.                                                                   |
| `401`                | Check the credential configuration. Ask the owner to replace a revoked key or resolve an inactive account.                                           |
| `403`                | The current user lacks permission, or the operation requires an interactive session. Do not substitute another client's ID.                          |
| `404`                | Check IDs and the documented path. Some handlers conceal inaccessible resources as not found.                                                        |
| `405`                | Use a method listed for that endpoint.                                                                                                               |
| `409`                | Read the error code. Reuse operation IDs only with identical content. For unknown email delivery, check sent mail before starting another operation. |
| `410`                | The legacy route was retired; follow the replacement named in the response.                                                                          |
| `429`                | Respect `Retry-After` when present and retry with backoff.                                                                                           |
| `503` or other `5xx` | Back off on reads. Check whether a write completed before retrying it.                                                                               |

Response envelopes vary between existing endpoints; inspect the documented response instead of assuming a global `data` wrapper. A redirect or HTML sign-in page usually means the URL is a browser route. Use the documented `/api/...` handler.

Wesley does not provide a universal `Idempotency-Key` guarantee across all existing APIs. Bank-statement imports use the `idempotencyKey` field; AP/AR payment creation uses `operationId`.

`POST /api/client-emails/send`, `/api/accounting-firms/gmail/send`, `/api/accounting-firms/outlook/send`, and `/api/accounting-firms/clients-bulk-email` require a UUID `Idempotency-Key` header (or `operationId` body field). Preserve it for identical retries. Completed sends return their saved outcome. A changed payload returns `IDEMPOTENCY_CONFLICT`; a running or uncertain send is not automatically repeated. Query `GET /api/users/operations?operationId=...` for your operation status. Bulk email tracks each client's delivery separately, so retrying the same batch does not repeat already completed sends. Check provider sent mail before creating a new ID for an uncertain delivery.

## Usage and job limits

Limits aggregate across all of your keys. Creating extra keys does not increase your user allowance.

| Limit                        | Allowance                                                   |
| ---------------------------- | ----------------------------------------------------------- |
| Requests per key             | 300 per minute                                              |
| Requests per user            | 600 per minute                                              |
| Writes per user              | 180 per minute                                              |
| Heavy requests per user      | 20 per minute; 1,000 per day                                |
| Heavy requests per firm      | 5,000 per day                                               |
| Concurrent heavy requests    | 3 per user; 12 per firm                                     |
| Personal document conversion | 2,000 pages per day; at most 500 selected pages per request |
| Connected email delivery     | 500 recipients per user per day; 2,500 per firm per day     |

IP and upload quotas also apply. Limits use distributed server-side counters and may return `429` with `Retry-After`. If the safety service cannot verify capacity, work fails with `503`; do not increase concurrency to work around it. Daily quotas use a 24-hour window starting with the first request, rather than resetting at midnight.

Document conversion reserves credits before starting paid work, then settles the reservation. Files must be uploaded to Wesley under your own document upload session or be existing documents you can access. Arbitrary external URLs and another user's private file references are rejected. `POST /api/document-upload-session` requires authentication and returns your private upload prefix and upload-token endpoint. Anonymous document uploads are not supported. Upload documents through Banking to start conversion.

## Approve destructive actions

Company deletion, full reset, date-range deletion, and firm deletion require a one-time approval. The API key alone cannot approve them. Ordinary reads and edits do not require an interactive challenge.

1. Prepare `POST /api/users/action-approvals` with `action`, the target `companyId` (or `accountingFirmId` for firm deletion), and `payload`.
2. Keep the returned `token` in memory or a secret store. Give the user the returned `approvalUrl`, resolved against the Wesley base URL. Never put the token in the URL.
3. The user opens Wesley, reviews the exact target and action, enters its name, and completes identity verification when requested.
4. Poll `GET /api/users/action-approvals?id=...` at modest intervals until `approved_at` is present.
5. Send the original action with `x-wesley-action-approval: <token>`. Use the same personal key, user, company, and payload. Approval expires after five minutes and is consumed by one matching execution attempt. An expired or consumed approval requires a new user approval.

| Action                 | Approval payload                                         | Execution                             |
| ---------------------- | -------------------------------------------------------- | ------------------------------------- |
| `company.reset`        | `{ "disconnectPlaidConnections": false }`                | `POST /api/companies/reset`           |
| `company.delete`       | `{}`                                                     | `DELETE /api/companies/delete`        |
| `company.delete-range` | `{ "startDate": "2026-01-01", "endDate": "2026-01-31" }` | `POST /api/companies/delete-range`    |
| `firm.delete`          | `{}`                                                     | `DELETE /api/accounting-firms/delete` |

Provide the normal company or firm context on execution. Server permissions are checked again at execution time. After any timeout, inspect the resulting state before requesting another approval.
