# Accounts API reference

> Login, sessions, accounts, organizations, invitations and API-key management routes.

Page: https://docs.aggre.org/api/accounts/reference · Section: API reference · All pages: https://docs.aggre.org/llms.txt

Every route of the accounts service at `https://accounts.aggre.org` that a signed-in person's browser uses. Read the [accounts API overview](https://docs.aggre.org/api/accounts.md) first: these routes take a **session cookie** (not an API key), an allowed `Origin` and, for anything but GET, the header `X-Aggre-CSRF: 1`. The OpenAPI file is [`/openapi/accounts.yaml`](https://docs.aggre.org/openapi/accounts.yaml) (also [JSON](https://docs.aggre.org/openapi/accounts.json)).

The platform-staff routes under `/v1/admin/*` are not part of this reference.

| Group | Route | Summary |
|---|---|---|
| Configuration | `GET /v1/config` | [Public console configuration (login buttons, limits, defaults)](#getConfig) |
| Login and sessions | `GET /login/{provider}` | [Start a login (full-page navigation, answers 302 to the provider)](#loginStart) |
| Login and sessions | `GET /login/callback/{provider}` | [OAuth redirect URI. Sets the session cookie and answers 302 to return_to or {origin}/login?error=<code>](#loginCallback) |
| Login and sessions | `POST /v1/auth/logout` | [Revoke the current session and clear its cookie](#logout) |
| Login and sessions | `POST /v1/auth/logout-all` | [Revoke every session of the user (both audiences)](#logoutAll) |
| The signed-in user | `GET /v1/me` | [The signed-in user, identities, memberships and session](#getMe) |
| The signed-in user | `DELETE /v1/me` | [Delete the personal account (keys revoked, identities and sessions removed, handle stays reserved)](#deleteMe) |
| The signed-in user | `DELETE /v1/me/identities/{identity_id}` | [Unlink a login method (409 if it is the last one)](#unlinkIdentity) |
| Accounts and usage | `GET /v1/handles/{handle}` | [Is this handle usable? (signed-in users only)](#checkHandle) |
| Accounts and usage | `GET /v1/accounts/{handle}` | [One account you can see (self, member, or staff)](#getAccount) |
| Accounts and usage | `PATCH /v1/accounts/{handle}` | [Change display_name and/or handle (user: self; org: owner). First rename free, then once per 30 days.](#updateAccount) |
| Accounts and usage | `GET /v1/accounts/{handle}/usage` | [Request counts per UTC day and per key over the last N days](#getUsage) |
| Accounts and usage | `GET /v1/accounts/{handle}/audit` | [Audit log of the account (owner/admin/staff), newest first](#listAudit) |
| API keys | `GET /v1/accounts/{handle}/api-keys` | [Keys of the account, newest first, revoked ones included (max 200). Never contains secrets.](#listApiKeys) |
| API keys | `POST /v1/accounts/{handle}/api-keys` | [Create a key. The secret is in the response once and never again.](#createApiKey) |
| API keys | `PATCH /v1/accounts/{handle}/api-keys/{key_id}` | [Rename a key or replace its ip_allowlist](#updateApiKey) |
| API keys | `DELETE /v1/accounts/{handle}/api-keys/{key_id}` | [Revoke a key (takes effect on the gateway within ~30 s)](#revokeApiKey) |
| Organizations and members | `POST /v1/orgs` | [Create an org; the caller becomes its owner](#createOrg) |
| Organizations and members | `DELETE /v1/orgs/{handle}` | [Delete an org (owner). Keys are revoked, memberships removed, the handle stays reserved.](#deleteOrg) |
| Organizations and members | `GET /v1/orgs/{handle}/members` | [Members of the org (any member, or staff)](#listMembers) |
| Organizations and members | `PATCH /v1/orgs/{handle}/members/{user_handle}` | [Change a member's role (owner). 409 if it would leave no owner.](#updateMember) |
| Organizations and members | `DELETE /v1/orgs/{handle}/members/{user_handle}` | [Remove a member, or leave (user_handle = yourself). 409 for the last owner.](#removeMember) |
| Invitations | `GET /v1/orgs/{handle}/invitations` | [Pending invitations (owner/admin)](#listInvitations) |
| Invitations | `POST /v1/orgs/{handle}/invitations` | [Invite an e-mail address. Re-inviting the same address replaces the pending invitation. No e-mail is sent; share the link.](#createInvitation) |
| Invitations | `DELETE /v1/orgs/{handle}/invitations/{invitation_id}` | [Revoke a pending invitation](#revokeInvitation) |
| Invitations | `GET /v1/invitations/{token}` | [What an invitation link is for (no session needed, so the page can ask the visitor to sign in first)](#previewInvitation) |
| Invitations | `POST /v1/invitations/{token}/accept` | [Join the org. The caller's verified primary e-mail must equal the invited address (403 email_mismatch).](#acceptInvitation) |

## Public console configuration (login buttons, limits, defaults) {#getConfig}

`GET /v1/config`

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | `Config` | yes |  |

## Start a login (full-page navigation, answers 302 to the provider) {#loginStart}

`GET /login/{provider}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `provider` | path | yes | string |  |
| `audience` | query | no | `app` \| `admin` (default `app`) |  |
| `return_to` | query | no | string, uri | Absolute URL on the audience origin; otherwise ignored. |
| `link` | query | no | `1` | Attach this provider to the signed-in user. |

**Errors**: `302`, `400`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## OAuth redirect URI. Sets the session cookie and answers 302 to return_to or {origin}/login?error=<code> {#loginCallback}

`GET /login/callback/{provider}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `provider` | path | yes | string |  |
| `code` | query | no | string |  |
| `state` | query | no | string |  |
| `error` | query | no | string |  |

**Errors**: `302`, `400`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Revoke the current session and clear its cookie {#logout}

`POST /v1/auth/logout`

**Response** `204`.

## Revoke every session of the user (both audiences) {#logoutAll}

`POST /v1/auth/logout-all`

**Response** `204`.

## The signed-in user, identities, memberships and session {#getMe}

`GET /v1/me`

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | `Me` | yes |  |

**Errors**: `401`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Delete the personal account (keys revoked, identities and sessions removed, handle stays reserved) {#deleteMe}

`DELETE /v1/me`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `confirm` | query | yes | string | Must equal the user's handle. |

**Response** `204`.

**Errors**: `401`, `409`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Unlink a login method (409 if it is the last one) {#unlinkIdentity}

`DELETE /v1/me/identities/{identity_id}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `identity_id` | path | yes | string, uuid |  |

**Response** `204`.

**Errors**: `404`, `409`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Is this handle usable? (signed-in users only) {#checkHandle}

`GET /v1/handles/{handle}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | object | yes |  |
| `data.handle` | string | yes |  |
| `data.available` | boolean | yes |  |
| `data.reason` | `invalid` \| `reserved` \| `taken` \| `null` | no |  |

## One account you can see (self, member, or staff) {#getAccount}

`GET /v1/accounts/{handle}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | `Account` | yes |  |

**Errors**: `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Change display_name and/or handle (user: self; org: owner). First rename free, then once per 30 days. {#updateAccount}

`PATCH /v1/accounts/{handle}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |

**Request body** (JSON)

| Field | Type | Required | Description |
|---|---|---|---|
| `display_name` | nullable, string | no |  |
| `handle` | string | no |  |


**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | `Account` | yes |  |

**Errors**: `403`, `404`, `409`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Request counts per UTC day and per key over the last N days {#getUsage}

`GET /v1/accounts/{handle}/usage`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |
| `days` | query | no | integer, 1–90, default `30` |  |

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | `Usage` | yes |  |

**Errors**: `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Audit log of the account (owner/admin/staff), newest first {#listAudit}

`GET /v1/accounts/{handle}/audit`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |
| `limit` | query | no | integer, 1–100, default `50` |  |
| `before` | query | no | integer, int64 | Return entries with id < before. |

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of `AuditEntry` | yes |  |
| `next_before` | nullable, integer, int64 | yes |  |

**Errors**: `403`, `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Keys of the account, newest first, revoked ones included (max 200). Never contains secrets. {#listApiKeys}

`GET /v1/accounts/{handle}/api-keys`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of `ApiKey` | yes |  |

**Errors**: `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Create a key. The secret is in the response once and never again. {#createApiKey}

`POST /v1/accounts/{handle}/api-keys`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |

**Request body** (JSON)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | yes |  |
| `usage_limit` | nullable, integer, int64 | no | Omit = server default. null (Unlimited) and values above the default: staff only. |
| `usage_period` | `day` \| `month` \| `total` | no |  |
| `rate_policy` | `default` \| `exempt` | no | exempt: staff only. |
| `internal` | boolean, default `false` | no | true gives the key the `internal` scope (the data API's `/v1/internal/*` per-row routes). Platform staff on an admin session, freshly signed in; anyone else gets 403. |
| `ip_allowlist` | array of string | no |  |
| `expires_at` | nullable, string, date-time | no |  |


**Response** `201`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | `ApiKey` | yes |  |
| `secret` | string | yes |  |

**Errors**: `400`, `403`, `404`, `409`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Rename a key or replace its ip_allowlist {#updateApiKey}

`PATCH /v1/accounts/{handle}/api-keys/{key_id}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |
| `key_id` | path | yes | string, uuid |  |

**Request body** (JSON)

| Field | Type | Required | Description |
|---|---|---|---|
| `name` | string | no |  |
| `ip_allowlist` | array of string | no | CIDRs or bare addresses; [] = open. |


**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | `ApiKey` | yes |  |

**Errors**: `400`, `403`, `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Revoke a key (takes effect on the gateway within ~30 s) {#revokeApiKey}

`DELETE /v1/accounts/{handle}/api-keys/{key_id}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |
| `key_id` | path | yes | string, uuid |  |

**Response** `204`.

**Errors**: `403`, `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Create an org; the caller becomes its owner {#createOrg}

`POST /v1/orgs`

**Request body** (JSON)

| Field | Type | Required | Description |
|---|---|---|---|
| `handle` | string | yes |  |
| `display_name` | string | no |  |


**Response** `201`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | `Account` | yes |  |

**Errors**: `400`, `409`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Delete an org (owner). Keys are revoked, memberships removed, the handle stays reserved. {#deleteOrg}

`DELETE /v1/orgs/{handle}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |
| `confirm` | query | yes | string | Must equal the org handle. |

**Response** `204`.

**Errors**: `403`, `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Members of the org (any member, or staff) {#listMembers}

`GET /v1/orgs/{handle}/members`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of `Member` | yes |  |

**Errors**: `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Change a member's role (owner). 409 if it would leave no owner. {#updateMember}

`PATCH /v1/orgs/{handle}/members/{user_handle}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |
| `user_handle` | path | yes | string |  |

**Request body** (JSON)

| Field | Type | Required | Description |
|---|---|---|---|
| `role` | `Role` | yes |  |


**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | `Member` | yes |  |

**Errors**: `403`, `404`, `409`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Remove a member, or leave (user_handle = yourself). 409 for the last owner. {#removeMember}

`DELETE /v1/orgs/{handle}/members/{user_handle}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |
| `user_handle` | path | yes | string |  |

**Response** `204`.

**Errors**: `403`, `404`, `409`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Pending invitations (owner/admin) {#listInvitations}

`GET /v1/orgs/{handle}/invitations`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | array of `Invitation` | yes |  |

**Errors**: `403`, `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Invite an e-mail address. Re-inviting the same address replaces the pending invitation. No e-mail is sent; share the link. {#createInvitation}

`POST /v1/orgs/{handle}/invitations`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |

**Request body** (JSON)

| Field | Type | Required | Description |
|---|---|---|---|
| `email` | string, email | yes |  |
| `role` | `Role` | no |  |


**Response** `201`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | `Invitation` | yes |  |
| `token` | string | yes | One-time secret; the console links to {app}/invitations/{token}. |

**Errors**: `400`, `403`, `404`, `409`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Revoke a pending invitation {#revokeInvitation}

`DELETE /v1/orgs/{handle}/invitations/{invitation_id}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `handle` | path | yes | string |  |
| `invitation_id` | path | yes | string, uuid |  |

**Response** `204`.

**Errors**: `403`, `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## What an invitation link is for (no session needed, so the page can ask the visitor to sign in first) {#previewInvitation}

`GET /v1/invitations/{token}`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `token` | path | yes | string |  |

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | object | yes |  |
| `data.org` | object | yes |  |
| `data.role` | `Role` | yes |  |
| `data.email_hint` | string | yes |  |
| `data.expires_at` | string, date-time | yes |  |

**Errors**: `404`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).

## Join the org. The caller's verified primary e-mail must equal the invited address (403 email_mismatch). {#acceptInvitation}

`POST /v1/invitations/{token}/accept`

**Parameters**

| Name | In | Required | Type | Description |
|---|---|---|---|---|
| `token` | path | yes | string |  |

**Response** `200`

| Field | Type | Required | Description |
|---|---|---|---|
| `data` | object | yes |  |
| `data.org` | `Account` | yes |  |
| `data.role` | `Role` | yes |  |

**Errors**: `401`, `403`, `404`, `409`, in the shared envelope ([Errors](https://docs.aggre.org/concepts/errors.md)).
