Every API here is available over the APIs.io API and to AI agents over MCP.
openapi: 3.2.0
info:
title: Customdomain Ai Members API
version: '1.0'
license:
name: Apache-2.0
identifier: Apache-2.0
description: 'Operations tagged Members across 2 of this provider''s published API definitions: customdomain-ai-openapi-original.json, customdomain-ai-openapi.yml. Each path carries the servers of the definition it was published in.'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
security:
- apiKey: []
tags:
- name: Members
description: 'Multi-user tenancy: invites, roles, and membership.'
paths:
/members:
post:
operationId: inviteMember
summary: Invite a member to the caller's tenant
tags:
- Members
description: Creates a pending member and its single-use invite link. The response carries `invite_token`; send the invitee a link built from it. The link works for 7 days, after which the member is reported as `expired` until an admin resends or revokes it. The inviter's email is taken from the credential that sends the invite, never from the body. Requires admin or owner (`member:manage`); a caller cannot invite a role above their own, and `owner` is reached only by promoting an existing member.
security:
- apiKey: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- email
properties:
email:
type: string
role:
$ref: '#/components/schemas/Role'
inviter_name:
type: string
description: Display name of the person sending the invite, shown to the invitee. Optional; at most 80 characters.
responses:
'201':
description: Invited
content:
application/json:
schema:
$ref: '#/components/schemas/Member'
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
'403':
$ref: '#/components/responses/Error'
get:
operationId: listMembers
summary: List the caller tenant's members
tags:
- Members
description: 'Every member and pending invite of the caller''s tenant, each with its `status`: `active` once accepted, `pending` while its invite link works, `expired` once the link has lapsed. Invite tokens never appear here.'
security:
- apiKey: []
responses:
'200':
description: Members
content:
application/json:
schema:
type: object
required:
- members
properties:
members:
type: array
items:
$ref: '#/components/schemas/Member'
'401':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
/members:preview:
post:
operationId: previewInvite
summary: Preview an invite by its token, without accepting it
tags:
- Members
description: 'What an invite link is for: the invited email, the workspace, the role, who sent it, and whether it still works. Nothing is accepted or spent. Public: the token is the capability, exactly as for `members:accept`, and it travels in the body so it never lands in an access log. A link that stopped working says why (`accepted`, `expired`, `revoked`, or `replaced` by a newer invite to the same address); a token that was never issued is a 404. Rate-limited per client address.'
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- token
properties:
token:
type: string
description: The invite token from the invite link.
responses:
'200':
description: The invite
content:
application/json:
schema:
$ref: '#/components/schemas/InvitePreview'
'400':
$ref: '#/components/responses/Error'
'404':
$ref: '#/components/responses/Error'
'429':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
/members:accept:
post:
operationId: acceptMember
summary: Accept an invite (single-use token)
tags:
- Members
description: Accepts the invite and returns the member with a member-scoped API key for the inviter's tenant. The token is the capability, so no API key is needed; when `email` is supplied it must match the invited email. The token is spent on success, once. A second accept that races the first, or one racing a resend that replaced the link, is refused with 409. A repeat after the accept has finished finds no invite and answers 404, as does a link for an invite that was removed. A pending invite whose link has lapsed is refused with 410 `invite_expired`.
security: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- token
properties:
token:
type: string
email:
type: string
description: When supplied, must match the invited email.
user_id:
type: string
description: The accepting user's id in the calling application, recorded as `invited_by`.
responses:
'200':
description: Accepted
content:
application/json:
schema:
allOf:
- $ref: '#/components/schemas/Member'
- type: object
required:
- application_id
- api_key
properties:
application_id:
type: string
description: The inviter tenant's default application.
api_key:
type: string
description: Member-scoped API key for the inviter's tenant; shown once.
'400':
$ref: '#/components/responses/Error'
'404':
$ref: '#/components/responses/Error'
'409':
$ref: '#/components/responses/Error'
'410':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
/members/{id}:
patch:
operationId: updateMemberRole
summary: Change a member's role (owner-protected)
tags:
- Members
description: Demoting the tenant's last owner is refused with 400, also when two demotions or removals of owners race each other. A 409 means the member's role changed after this request read it; reload and retry.
security:
- apiKey: []
parameters:
- $ref: '#/components/parameters/Id'
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- role
properties:
role:
$ref: '#/components/schemas/Role'
responses:
'200':
description: Updated
content:
application/json:
schema:
$ref: '#/components/schemas/Member'
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
'403':
$ref: '#/components/responses/Error'
'404':
$ref: '#/components/responses/Error'
'409':
$ref: '#/components/responses/Error'
delete:
operationId: removeMember
summary: Remove a member or revoke a pending invite (owner-protected)
tags:
- Members
description: Removing a member revokes their member-scoped API keys at once, on every application of the workspace (the console key and any key they minted). Revoking a pending invite stops its link working; a preview of that link then reports `revoked`. The last owner cannot be removed (400), and a 409 means the member's role changed after this request read it.
security:
- apiKey: []
parameters:
- $ref: '#/components/parameters/Id'
responses:
'204':
description: Removed
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
'403':
$ref: '#/components/responses/Error'
'404':
$ref: '#/components/responses/Error'
'409':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
/members/{id}/invite:
get:
operationId: getMemberInvite
summary: Get a pending member's current invite token
tags:
- Members
description: 'Returns the pending member with its current `invite_token`, so an admin can copy the link again. Reading it changes nothing: the link already sent keeps working. Requires admin or owner (`member:manage`). A member who already joined has no invite: 404.'
security:
- apiKey: []
parameters:
- $ref: '#/components/parameters/Id'
responses:
'200':
description: The pending member and its invite token
content:
application/json:
schema:
$ref: '#/components/schemas/Member'
'401':
$ref: '#/components/responses/Error'
'403':
$ref: '#/components/responses/Error'
'404':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
/members/{id}/invite:resend:
post:
operationId: resendMemberInvite
summary: Re-issue a pending invite with a fresh link
tags:
- Members
description: Gives a pending (or expired) invite a new token that works for another 7 days and returns it as `invite_token`; send the invitee a link built from it. The previous link stops working at once, and a preview of it reports `replaced`. Requires admin or owner (`member:manage`). A member who already joined, or an invite accepted or replaced while this request ran, is a 409.
security:
- apiKey: []
parameters:
- $ref: '#/components/parameters/Id'
requestBody:
required: false
content:
application/json:
schema:
type: object
properties:
inviter_name:
type: string
description: Display name of the person resending, shown to the invitee. Optional; at most 80 characters.
responses:
'200':
description: Re-issued
content:
application/json:
schema:
$ref: '#/components/schemas/Member'
'401':
$ref: '#/components/responses/Error'
'403':
$ref: '#/components/responses/Error'
'404':
$ref: '#/components/responses/Error'
'409':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
/notifications:test:
post:
operationId: sendTestNotification
summary: Send a test push to the signed in person's devices (console server)
tags:
- Members
description: 'Pushes one test notification to every browser in which `user_id` turned push notifications on. The console''s server calls this behind its "Send a test notification" button, guarded by the provision secret exactly like `POST /members:email-changed`: the console has already checked the session and passes the signed in person''s id, and the route takes no account, so a secret for one workspace cannot name another. The control plane holds the VAPID keys; without them it answers 503 `push_not_configured`. More than five tests a minute for one person answer 429 `rate_limited` with `Retry-After: 60`.'
security:
- provisionSecret: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- user_id
properties:
user_id:
type: string
maxLength: 128
description: The signed in person's id in the console.
responses:
'200':
description: What the test did
content:
application/json:
schema:
type: object
required:
- devices
- sent
- removed
- failed
properties:
devices:
type: integer
description: Browsers the person has turned push on in.
sent:
type: integer
description: Push services that accepted the message.
removed:
type: integer
description: Devices found gone and removed.
failed:
type: integer
description: Pushes that did not go through.
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
'429':
$ref: '#/components/responses/Error'
'503':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
/members:email-changed:
post:
operationId: memberEmailChanged
summary: A member's email change was confirmed (console server)
tags:
- Members
description: 'The console''s server calls this once a person''s email change is confirmed, guarded by the provision secret like `POST /tenants:provision`. By then the console has already corrected the member row it shares with the control plane, so `email` is the member''s new, saved address. The control plane''s one job here is to mirror the change to the CRM contact for that member (owner or teammate), best effort and never blocking the console: it answers `ok: true` as soon as the mirror is queued.'
security:
- provisionSecret: []
requestBody:
required: true
content:
application/json:
schema:
type: object
required:
- tenant_id
- email
properties:
tenant_id:
type: string
email:
type: string
description: The member's new address. Lower cased by the control plane.
responses:
'200':
description: Mirror queued
content:
application/json:
schema:
type: object
required:
- ok
properties:
ok:
type: boolean
'400':
$ref: '#/components/responses/Error'
'401':
$ref: '#/components/responses/Error'
servers:
- url: https://api.customdomain.ai/v1
description: Hosted control-plane
- url: http://localhost:8080/v1
description: Local control-plane
components:
parameters:
Id:
name: id
in: path
required: true
schema:
type: string
schemas:
InvitePreview:
type: object
required:
- status
- email
- role
- tenant_id
- workspace_name
properties:
status:
type: string
enum:
- pending
- accepted
- expired
- revoked
- replaced
description: '`pending`: the link works. `accepted`: it was used. `expired`: it lapsed. `revoked`: an admin withdrew the invite. `replaced`: a newer invite was sent to the same address.'
email:
type: string
description: The invited address.
role:
$ref: '#/components/schemas/Role'
tenant_id:
type: string
workspace_name:
type: string
description: The tenant's display name.
inviter_name:
type: string
inviter_email:
type: string
invited_at:
type: string
format: date-time
expires_at:
type: string
format: date-time
description: When the link stops (or stopped) working. Absent when it never expires.
Role:
type: string
enum:
- owner
- admin
- member
- viewer
Member:
type: object
required:
- id
- tenant_id
- email
- role
- status
- created_at
properties:
id:
type: string
tenant_id:
type: string
email:
type: string
role:
$ref: '#/components/schemas/Role'
status:
type: string
enum:
- active
- pending
- expired
description: '`active` once accepted, `pending` while the invite link works, `expired` once it has lapsed.'
invited_by:
type: string
description: The accepting user's id in the calling application, when one was supplied at accept.
inviter_email:
type: string
description: Who sent (or last re-sent) the invite, resolved from the sending credential.
inviter_name:
type: string
description: The sender's display name, when one was supplied.
invite_sent_at:
type: string
format: date-time
description: When the current invite link was issued.
invite_expires_at:
type: string
format: date-time
description: When the current invite link stops working. Absent for members never invited by link.
invite_token:
type: string
description: The single-use invite token. Present only in the invite, resend and get-invite responses, never in the roster.
accepted_at:
type: string
format: date-time
created_at:
type: string
format: date-time
Error:
type: object
required:
- code
- title
properties:
code:
type: string
description: stable machine code, e.g. invalid_request, unauthorized, not_found, service_unavailable, internal_error
title:
type: string
description: human-readable summary
details:
type: string
description: optional extra context
responses:
Error:
description: 'Error envelope: {code, title, details}.'
content:
application/json:
schema:
$ref: '#/components/schemas/Error'
securitySchemes:
managementKey:
type: http
scheme: bearer
description: Operator management key. Gates provisioning + reads across tenants.
provisionSecret:
type: apiKey
in: header
name: X-Provision-Secret
description: Narrow secret held by the customer console's server, for signup provisioning and its other server to server calls (also accepted as a Bearer token, except on POST /audit, where a Bearer is always a tenant credential).
apiKey:
type: http
scheme: bearer
description: Long-lived tenant API key, cd_live_… (production) or cd_test_… (staging, development), stored hashed. Keys created before the switch to cd_ start with sk_live_… / sk_test_… and are still accepted. Scoped to the owning application's tenant.
widgetJWT:
type: http
scheme: bearer
bearerFormat: JWT
description: Short-lived widget JWT minted server-side via POST /v1/tokens. Scoped to one application (and optionally one domain).
browserSession:
type: http
scheme: bearer
description: The session token of one cloud browser sign in, returned by `POST /browser-sessions:redeem`. It binds one session, one connection and one workspace, lives at most as long as the session, and stops working the moment the session ends. It lives in the popup's memory only.
agentToken:
type: oauth2
description: 'A delegated agent access token: an ES256 JWT that lives 15 minutes, sent as a bearer token. A person approves an agent in the console and the agent exchanges its authorization code (with PKCE) at the token endpoint. The token carries the workspace, the one application the grant is bound to, the grant, the approved scopes and its audience, and is verified against `/.well-known/jwks.json`. A route that accepts it lists the scope it needs; a token without the scope is refused with `403`, and a connection or order of another application answers `404`.'
flows:
authorizationCode:
authorizationUrl: https://api.customdomain.ai/oauth/agent/authorize
tokenUrl: https://api.customdomain.ai/oauth/agent/token
refreshUrl: https://api.customdomain.ai/oauth/agent/token
scopes:
domains:read: Look at availability, connections, records, certificates, diagnoses, orders and the billing standing.
domains:connect: Connect a domain, recheck it, reapply its records and start a one click setup.
domains:disconnect: Remove a connected domain. The call must name the domain.
domains:purchase: Buy a domain, up to the grant's monthly spend cap.
x-refined-from:
- customdomain-ai-openapi-original.json
- customdomain-ai-openapi.yml