CustomDomain Members API

Multi-user tenancy: invites, roles, and membership.

GitHub OpenAPI

Operations 10

POST /members Invite a member to the caller's tenant #
GET /members List the caller tenant's members #
POST /members:preview Preview an invite by its token, without accepting it #
POST /members:accept Accept an invite (single-use token) #
PATCH /members/{id} Change a member's role (owner-protected) #
DELETE /members/{id} Remove a member or revoke a pending invite (owner-protected) #
GET /members/{id}/invite Get a pending member's current invite token #
POST /members/{id}/invite:resend Re-issue a pending invite with a fresh link #
POST /notifications:test Send a test push to the signed in person's devices (console server) #
POST /members:email-changed A member's email change was confirmed (console server) #

Work with this as data

Every API here is available over the APIs.io API and to AI agents over MCP.

MCP server

One button, every client — Claude, Cursor, VS Code and the rest.

https://apis.io/mcp

Tools for apis

7 MCP tools reach this
  • find_apisBrowse and filter every API in the catalog.
  • get_api_artifactsOne API's artifacts, grouped by type.
  • get_openapiThe primary OpenAPI for this API.
  • find_similar_apisAPIs that look like this one.
  • apis_io_searchSTART HERE — APIs, providers and tags for one query, each with its total.
  • resolveTurn a domain, URL or GitHub org into the provider it belongs to.
  • find_cohortsEvery scored population of providers in the catalog.
All 92 tools →

Call it yourself

curl for this page
This API
curl "https://apis.io/api/v1/apis/customdomain-ai:customdomain-ai-members-api"
All apis
curl "https://apis.io/api/v1/apis?limit=25"

Calls without X-API-Key need no key. Calls with it need a paid plan — get a key.

Get an API key

Free tier, no form to fill in. Signing in shares your email address with us — we store it to create your key and to recognise you if you sign in with another provider. See our Privacy Policy and Terms.

A second provider on the same verified email joins the account you already have.

OpenAPI Specification

customdomain-ai-members-api-openapi.yml Raw ↑
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