diff --git a/fern/apis/api/definition/accounts.yml b/fern/apis/api/definition/accounts.yml index 5e12809..d845739 100644 --- a/fern/apis/api/definition/accounts.yml +++ b/fern/apis/api/definition/accounts.yml @@ -14,6 +14,22 @@ types: type: uuid docs: ID of provider. + AccountStatus: + enum: + - disabled + docs: | + `disabled` refuses every new sign-in for the account until it is + re-enabled. Access tokens already issued are not revoked. + + UpdateAccountStatus: + enum: + - enabled + - disabled + docs: | + Status to set. `disabled` stops the inbox from signing in at the + provider; `enabled` re-enables it. An enabled account reads back with no + `status`. + Account: docs: One inbox signed in at one provider. properties: @@ -34,6 +50,12 @@ types: sign_in_count: type: integer docs: Number of sign-ins at provider. + status: + type: optional + docs: Present only while the account is disabled. Absent means the inbox may sign in. + disabled_at: + type: optional + docs: Time at which the account was disabled. Present only while `status` is `disabled`. ListAccountsResponse: properties: @@ -42,6 +64,14 @@ types: next_page_token: optional accounts: list + UpdateAccountRequest: + docs: | + Fields to change on the account. Fields you omit are left unchanged and + unknown fields are rejected. Expects at least one field; today that is + `status`. + properties: + status: optional + service: url: Http base-path: /accounts @@ -52,7 +82,10 @@ service: method: GET path: "" display-name: List Accounts - docs: Lists accounts across all providers. + docs: | + Lists accounts across all providers, scoped to the API key: an + organization key sees every account, a pod key its pod's, an inbox key + its inbox's. Requires `inbox_read`. request: name: ListAccountsRequest query-parameters: @@ -67,8 +100,42 @@ service: method: GET path: /{account_id} display-name: Get Account + docs: | + Returns one account by ID. An account outside the key's scope is a 404. + Requires `inbox_read`. path-parameters: account_id: AccountId response: Account errors: - global.NotFoundError + + update: + method: PATCH + path: /{account_id}/update + display-name: Update Account + docs: | + Updates one account. Set `status` to `disabled` to stop the inbox from + signing in at the provider again, or to `enabled` to re-enable it. + Idempotent: disabling an already disabled account keeps its original + `disabled_at`, and enabling an enabled account is a no-op. + + Find the `account_id` with List Accounts. An account exists only after an + inbox's first sign-in at a provider, so it cannot be disabled in advance. + A disable applies to that inbox at that provider whichever sign-in key is + used: the provider's next authorization ends in `access_denied`, and a code + issued earlier is refused with `invalid_grant`. Access tokens already + issued stay valid until they expire, and the provider's own session is + unaffected. + + Requires `account_update`, which sign-in keys (`type: public_key`) cannot + hold, so call this with a bearer API key. An account outside the key's + scope is a 404. A 409 means the account changed during the write; read it + again and retry. + path-parameters: + account_id: AccountId + request: UpdateAccountRequest + response: Account + errors: + - global.ValidationError + - global.NotFoundError + - global.ConflictError diff --git a/fern/apis/api/definition/api-keys.yml b/fern/apis/api/definition/api-keys.yml index cc2b5f5..5605245 100644 --- a/fern/apis/api/definition/api-keys.yml +++ b/fern/apis/api/definition/api-keys.yml @@ -246,6 +246,11 @@ types: docs: | Share the organization owner's name and email with providers at sign-in. One permission for both values. + account_update: + type: optional + docs: | + Update accounts: disable or re-enable an inbox's sign-in at a provider. Reading accounts + needs only `inbox_read`. pod_read: type: optional docs: Read pods. diff --git a/fern/apis/api/definition/inboxes/__package__.yml b/fern/apis/api/definition/inboxes/__package__.yml index fb58680..c42387e 100644 --- a/fern/apis/api/definition/inboxes/__package__.yml +++ b/fern/apis/api/definition/inboxes/__package__.yml @@ -9,6 +9,7 @@ navigation: - metrics.yml - events.yml - api-keys.yml + - accounts.yml imports: global: ../__package__.yml diff --git a/fern/apis/api/definition/inboxes/accounts.yml b/fern/apis/api/definition/inboxes/accounts.yml new file mode 100644 index 0000000..b97f764 --- /dev/null +++ b/fern/apis/api/definition/inboxes/accounts.yml @@ -0,0 +1,43 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +imports: + global: ../__package__.yml + inboxes: __package__.yml + accounts: ../accounts.yml + +service: + url: Http + base-path: /inboxes/{inbox_id}/accounts + path-parameters: + inbox_id: inboxes.InboxId + auth: true + + endpoints: + list: + method: GET + path: "" + display-name: List Accounts + docs: | + Lists accounts held by the inbox, across all providers. Requires `inbox_read`. + request: + name: ListAccountsRequest + query-parameters: + limit: optional + page_token: optional + ascending: optional + response: accounts.ListAccountsResponse + errors: + - global.ValidationError + + get: + method: GET + path: /{account_id} + display-name: Get Account + docs: | + Returns one account held by the inbox. An account elsewhere is a 404. Requires + `inbox_read`. + path-parameters: + account_id: accounts.AccountId + response: accounts.Account + errors: + - global.NotFoundError diff --git a/fern/apis/api/definition/pods/__package__.yml b/fern/apis/api/definition/pods/__package__.yml index 9f796ed..9eba22f 100644 --- a/fern/apis/api/definition/pods/__package__.yml +++ b/fern/apis/api/definition/pods/__package__.yml @@ -9,6 +9,7 @@ navigation: - lists.yml - metrics.yml - api-keys.yml + - accounts.yml imports: global: ../__package__.yml diff --git a/fern/apis/api/definition/pods/accounts.yml b/fern/apis/api/definition/pods/accounts.yml new file mode 100644 index 0000000..c05a866 --- /dev/null +++ b/fern/apis/api/definition/pods/accounts.yml @@ -0,0 +1,43 @@ +# yaml-language-server: $schema=https://raw.githubusercontent.com/fern-api/fern/main/fern.schema.json + +imports: + global: ../__package__.yml + pods: __package__.yml + accounts: ../accounts.yml + +service: + url: Http + base-path: /pods/{pod_id}/accounts + path-parameters: + pod_id: pods.PodId + auth: true + + endpoints: + list: + method: GET + path: "" + display-name: List Accounts + docs: | + Lists accounts held by inboxes in the pod, across all providers. Requires `inbox_read`. + request: + name: ListAccountsRequest + query-parameters: + limit: optional + page_token: optional + ascending: optional + response: accounts.ListAccountsResponse + errors: + - global.ValidationError + + get: + method: GET + path: /{account_id} + display-name: Get Account + docs: | + Returns one account held by inboxes in the pod. An account elsewhere is a 404. Requires + `inbox_read`. + path-parameters: + account_id: accounts.AccountId + response: accounts.Account + errors: + - global.NotFoundError diff --git a/fern/apis/cli/openapi-overrides.yml b/fern/apis/cli/openapi-overrides.yml index b7e008a..842efc8 100644 --- a/fern/apis/cli/openapi-overrides.yml +++ b/fern/apis/cli/openapi-overrides.yml @@ -906,3 +906,32 @@ paths: - pods - inboxes x-fern-sdk-method-name: search + /v0/accounts/{account_id}/update: + patch: + x-fern-sdk-group-name: + - accounts + x-fern-sdk-method-name: update + /v0/inboxes/{inbox_id}/accounts: + get: + x-fern-sdk-group-name: + - inboxes + - accounts + x-fern-sdk-method-name: list + /v0/inboxes/{inbox_id}/accounts/{account_id}: + get: + x-fern-sdk-group-name: + - inboxes + - accounts + x-fern-sdk-method-name: get + /v0/pods/{pod_id}/accounts: + get: + x-fern-sdk-group-name: + - pods + - accounts + x-fern-sdk-method-name: list + /v0/pods/{pod_id}/accounts/{account_id}: + get: + x-fern-sdk-group-name: + - pods + - accounts + x-fern-sdk-method-name: get diff --git a/fern/changelog/2026-09-21.mdx b/fern/changelog/2026-09-21.mdx new file mode 100644 index 0000000..97a64f6 --- /dev/null +++ b/fern/changelog/2026-09-21.mdx @@ -0,0 +1,73 @@ +--- +tags: ["accounts-api", "pods-api", "inboxes-api", "new-feature"] +--- + +## Summary + +Disable an inbox's sign-in at a provider, and re-enable it later, with one call. Accounts now carry a `status`, and accounts can be listed and read under pods and inboxes so an agent can audit sign-ins at exactly the scope its key holds. + +### What's new? + +**New endpoints:** + +- `PATCH /v0/accounts/{account_id}/update` - Update one account. Send `{ "status": "disabled" }` to refuse every new sign-in for that inbox at that provider, or `{ "status": "enabled" }` to re-enable it. Requires the new `account_update` permission. +- `GET /v0/pods/{pod_id}/accounts` and `GET /v0/pods/{pod_id}/accounts/{account_id}` - List and read the accounts held by inboxes in one pod. +- `GET /v0/inboxes/{inbox_id}/accounts` and `GET /v0/inboxes/{inbox_id}/accounts/{account_id}` - List and read the accounts held by one inbox. + +**New features:** + +- **Account status**: a disabled account carries `status: "disabled"` and `disabled_at` on every read. An account without `status` can sign in. +- **Idempotent updates**: disabling an already disabled account keeps its original `disabled_at`, and enabling an enabled account is a no-op, so a retry never conflicts. +- **`account_update` permission**: a separate grant for the write. Unrestricted keys hold it; restricted keys get it only when granted. Reading accounts still needs only `inbox_read`. +- **Scoped views**: the pod and inbox forms return the same account object as `GET /v0/accounts`, narrowed to the pod or inbox in the path. An account outside that scope is a 404. + +### Use cases + +Build agents that: + +- Cut off a provider that is misbehaving, without deleting the sign-in record +- Pause one inbox's access to a provider during an incident and restore it afterward +- Audit, per pod or per inbox, which providers each agent has signed in to +- Give a tenant's key visibility into its own accounts and nothing beyond its pod + + +```python title="Python" +from agentmail import AgentMail + +client = AgentMail(api_key="your-api-key") + +# disable an inbox's sign-in at a provider +account = client.accounts.update( + account_id="3f1c2a4e-8b7d-4c6e-9a1f-2d3e4f5a6b7c", + status="disabled", +) +print(account.status, account.disabled_at) + +# list the accounts one inbox holds +accounts = client.inboxes.accounts.list(inbox_id="agent@agentmail.to") +for item in accounts.accounts: + print(item.provider_name, item.status or "ok") +``` + +```typescript title="TypeScript" +import { AgentMail } from "agentmail"; + +const client = new AgentMail({ apiKey: "your-api-key" }); + +// disable an inbox's sign-in at a provider +const account = await client.accounts.update("3f1c2a4e-8b7d-4c6e-9a1f-2d3e4f5a6b7c", { + status: "disabled", +}); +console.log(account.status, account.disabledAt); + +// list the accounts one inbox holds +const accounts = await client.inboxes.accounts.list("agent@agentmail.to"); +for (const item of accounts.accounts) { + console.log(item.providerName, item.status ?? "ok"); +} +``` + + + + See [Update Account](https://docs.agentmail.to/api-reference/accounts/update) in the API reference for the request and response shapes. + diff --git a/fern/pages/guides/agentid-sign-in.mdx b/fern/pages/guides/agentid-sign-in.mdx index 2b6d3a0..c3120db 100644 --- a/fern/pages/guides/agentid-sign-in.mdx +++ b/fern/pages/guides/agentid-sign-in.mdx @@ -152,7 +152,7 @@ if (key.type === "public_key") console.log(key.status); ``` -A single check reads the current status. Repeat with a delay while it is `pending`, stopping at expiry or an error. `active` means the key is ready to sign in as the inbox. Use [List Provider Accounts](https://docs.agentmail.to/api-reference/providers/list-accounts) to inspect accounts at the provider. +A single check reads the current status. Repeat with a delay while it is `pending`, stopping at expiry or an error. `active` means the key is ready to sign in as the inbox. Use [List Provider Accounts](https://docs.agentmail.to/api-reference/providers/list-accounts) to inspect accounts at the provider. To stop an inbox from signing in at a provider again, disable its account with [Update Account](https://docs.agentmail.to/api-reference/accounts/update). ## Manage sign-in keys diff --git a/openapi/openapi.yml b/openapi/openapi.yml index f62b1e9..f74d34d 100644 --- a/openapi/openapi.yml +++ b/openapi/openapi.yml @@ -770,7 +770,10 @@ paths: $ref: '#/components/schemas/webhooksUpdateWebhookHeadersRequest' /v0/accounts: get: - description: Lists accounts across all providers. + description: |- + Lists accounts across all providers, scoped to the API key: an + organization key sees every account, a pod key its pod's, an inbox key + its inbox's. Requires `inbox_read`. operationId: accounts_list tags: - Accounts @@ -816,6 +819,9 @@ paths: - BearerAuth: [] /v0/accounts/{account_id}: get: + description: |- + Returns one account by ID. An account outside the key's scope is a 404. + Requires `inbox_read`. operationId: accounts_get tags: - Accounts @@ -846,6 +852,74 @@ paths: - url: https://api.agentmail.eu security: - BearerAuth: [] + /v0/accounts/{account_id}/update: + patch: + description: |- + Updates one account. Set `status` to `disabled` to stop the inbox from + signing in at the provider again, or to `enabled` to re-enable it. + Idempotent: disabling an already disabled account keeps its original + `disabled_at`, and enabling an enabled account is a no-op. + + Find the `account_id` with List Accounts. An account exists only after an + inbox's first sign-in at a provider, so it cannot be disabled in advance. + A disable applies to that inbox at that provider whichever sign-in key is + used: the provider's next authorization ends in `access_denied`, and a code + issued earlier is refused with `invalid_grant`. Access tokens already + issued stay valid until they expire, and the provider's own session is + unaffected. + + Requires `account_update`, which sign-in keys (`type: public_key`) cannot + hold, so call this with a bearer API key. An account outside the key's + scope is a 404. A 409 means the account changed during the write; read it + again and retry. + operationId: accounts_update + tags: + - Accounts + parameters: + - name: account_id + in: path + required: true + schema: + $ref: '#/components/schemas/AccountId' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + '400': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationErrorResponse' + '404': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + '409': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + summary: Update Account + servers: + - url: https://api.agentmail.to + - url: https://x402.api.agentmail.to + - url: https://mpp.api.agentmail.to + - url: https://api.agentmail.eu + security: + - BearerAuth: [] + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/UpdateAccountRequest' /v0/agent/sign-up: post: description: >- @@ -1693,6 +1767,101 @@ paths: - url: https://api.agentmail.eu security: - BearerAuth: [] + /v0/inboxes/{inbox_id}/accounts: + get: + description: >- + Lists accounts held by the inbox, across all providers. Requires + `inbox_read`. + operationId: inboxes_accounts_list + tags: + - InboxesAccounts + parameters: + - name: inbox_id + in: path + required: true + schema: + $ref: '#/components/schemas/inboxesInboxId' + - name: limit + in: query + required: false + schema: + $ref: '#/components/schemas/Limit' + nullable: true + - name: page_token + in: query + required: false + schema: + $ref: '#/components/schemas/PageToken' + nullable: true + - name: ascending + in: query + required: false + schema: + $ref: '#/components/schemas/Ascending' + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ListAccountsResponse' + '400': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationErrorResponse' + summary: List Accounts + servers: + - url: https://api.agentmail.to + - url: https://x402.api.agentmail.to + - url: https://mpp.api.agentmail.to + - url: https://api.agentmail.eu + security: + - BearerAuth: [] + /v0/inboxes/{inbox_id}/accounts/{account_id}: + get: + description: >- + Returns one account held by the inbox. An account elsewhere is a 404. + Requires + + `inbox_read`. + operationId: inboxes_accounts_get + tags: + - InboxesAccounts + parameters: + - name: inbox_id + in: path + required: true + schema: + $ref: '#/components/schemas/inboxesInboxId' + - name: account_id + in: path + required: true + schema: + $ref: '#/components/schemas/AccountId' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + '404': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + summary: Get Account + servers: + - url: https://api.agentmail.to + - url: https://x402.api.agentmail.to + - url: https://mpp.api.agentmail.to + - url: https://api.agentmail.eu + security: + - BearerAuth: [] /v0/inboxes/{inbox_id}/api-keys: get: description: |- @@ -4710,6 +4879,101 @@ paths: - url: https://api.agentmail.eu security: - BearerAuth: [] + /v0/pods/{pod_id}/accounts: + get: + description: >- + Lists accounts held by inboxes in the pod, across all providers. + Requires `inbox_read`. + operationId: pods_accounts_list + tags: + - PodsAccounts + parameters: + - name: pod_id + in: path + required: true + schema: + $ref: '#/components/schemas/podsPodId' + - name: limit + in: query + required: false + schema: + $ref: '#/components/schemas/Limit' + nullable: true + - name: page_token + in: query + required: false + schema: + $ref: '#/components/schemas/PageToken' + nullable: true + - name: ascending + in: query + required: false + schema: + $ref: '#/components/schemas/Ascending' + nullable: true + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ListAccountsResponse' + '400': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ValidationErrorResponse' + summary: List Accounts + servers: + - url: https://api.agentmail.to + - url: https://x402.api.agentmail.to + - url: https://mpp.api.agentmail.to + - url: https://api.agentmail.eu + security: + - BearerAuth: [] + /v0/pods/{pod_id}/accounts/{account_id}: + get: + description: >- + Returns one account held by inboxes in the pod. An account elsewhere is + a 404. Requires + + `inbox_read`. + operationId: pods_accounts_get + tags: + - PodsAccounts + parameters: + - name: pod_id + in: path + required: true + schema: + $ref: '#/components/schemas/podsPodId' + - name: account_id + in: path + required: true + schema: + $ref: '#/components/schemas/AccountId' + responses: + '200': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/Account' + '404': + description: '' + content: + application/json: + schema: + $ref: '#/components/schemas/ErrorResponse' + summary: Get Account + servers: + - url: https://api.agentmail.to + - url: https://x402.api.agentmail.to + - url: https://mpp.api.agentmail.to + - url: https://api.agentmail.eu + security: + - BearerAuth: [] /v0/pods/{pod_id}/api-keys: get: description: |- @@ -8023,6 +8287,24 @@ components: type: string format: uuid description: ID of provider. + AccountStatus: + title: AccountStatus + type: string + enum: + - disabled + description: |- + `disabled` refuses every new sign-in for the account until it is + re-enabled. Access tokens already issued are not revoked. + UpdateAccountStatus: + title: UpdateAccountStatus + type: string + enum: + - enabled + - disabled + description: |- + Status to set. `disabled` stops the inbox from signing in at the + provider; `enabled` re-enables it. An enabled account reads back with no + `status`. Account: title: Account type: object @@ -8053,6 +8335,19 @@ components: sign_in_count: type: integer description: Number of sign-ins at provider. + status: + $ref: '#/components/schemas/AccountStatus' + nullable: true + description: >- + Present only while the account is disabled. Absent means the inbox + may sign in. + disabled_at: + type: string + format: date-time + nullable: true + description: >- + Time at which the account was disabled. Present only while `status` + is `disabled`. required: - account_id - provider_id @@ -8081,6 +8376,17 @@ components: - count - limit - accounts + UpdateAccountRequest: + title: UpdateAccountRequest + type: object + description: |- + Fields to change on the account. Fields you omit are left unchanged and + unknown fields are rejected. Expects at least one field; today that is + `status`. + properties: + status: + $ref: '#/components/schemas/UpdateAccountStatus' + nullable: true AgentSignupRequest: title: AgentSignupRequest type: object @@ -8526,6 +8832,14 @@ components: sign-in. One permission for both values. + account_update: + type: boolean + nullable: true + description: >- + Update accounts: disable or re-enable an inbox's sign-in at a + provider. Reading accounts + + needs only `inbox_read`. pod_read: type: boolean nullable: true