{
  "openapi": "3.1.0",
  "info": {
    "title": "Centerfy AI API",
    "version": "2026.10.07",
    "description": "Sub-account and agency endpoints of the Centerfy public API. Human-readable docs: https://setup.centerfy.ai/api/overview/ . Authenticate with an API key (cfy_…) in the x-api-key header or as a Bearer token. Agency endpoints (tag prefix \"Agency:\") take an agency key; everything else takes a sub-account key."
  },
  "servers": [
    {
      "url": "https://api.centerfy.ai"
    }
  ],
  "security": [
    {
      "ApiKeyHeader": []
    },
    {
      "BearerAuth": []
    }
  ],
  "tags": [
    {
      "name": "Contacts"
    },
    {
      "name": "Tags"
    },
    {
      "name": "Notes"
    },
    {
      "name": "AI Calls"
    },
    {
      "name": "Phone numbers"
    },
    {
      "name": "Number pools"
    },
    {
      "name": "Voices"
    },
    {
      "name": "Agents"
    },
    {
      "name": "Agent folders"
    },
    {
      "name": "Tools"
    },
    {
      "name": "Knowledge bases"
    },
    {
      "name": "Appointments"
    },
    {
      "name": "Messages"
    },
    {
      "name": "Conversations"
    },
    {
      "name": "Calls"
    },
    {
      "name": "Pipelines"
    },
    {
      "name": "Opportunities"
    },
    {
      "name": "Companies"
    },
    {
      "name": "Smart Lists"
    },
    {
      "name": "Custom Fields"
    },
    {
      "name": "Contact Consent"
    },
    {
      "name": "Lead Scoring"
    },
    {
      "name": "Analytics"
    },
    {
      "name": "Call Recordings"
    },
    {
      "name": "Wallet"
    },
    {
      "name": "Calendars"
    },
    {
      "name": "Scheduled Calls"
    },
    {
      "name": "Scheduled Messages"
    },
    {
      "name": "Workflows"
    },
    {
      "name": "Reminder Templates"
    },
    {
      "name": "Forms"
    },
    {
      "name": "Voice Widget"
    },
    {
      "name": "Outbound Webhook Subscriptions"
    },
    {
      "name": "Team Users"
    },
    {
      "name": "Campaigns"
    },
    {
      "name": "Contract Templates"
    },
    {
      "name": "Contracts"
    },
    {
      "name": "Reviews"
    },
    {
      "name": "Review Requests"
    },
    {
      "name": "Invoices"
    },
    {
      "name": "Quotes"
    },
    {
      "name": "Phone Number Purchasing"
    },
    {
      "name": "Agency: Sub-Accounts"
    },
    {
      "name": "Agency: Sub-Account Users"
    },
    {
      "name": "Agency: Sub-Account API Keys"
    },
    {
      "name": "Agency: Agency Wallet"
    }
  ],
  "paths": {
    "/webhooks/inbound/contacts": {
      "get": {
        "tags": [
          "Contacts"
        ],
        "summary": "List contacts",
        "description": "Inbound webhook — list contacts, newest first, paginated.\n\nhas_more is true when there are more records beyond the current page (offset + returned count < total). 401 if unauthenticated.",
        "operationId": "list-contacts",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100. Defaults to 50 when omitted or invalid; values above 100 are capped to 100.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0, defaults to 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 137,
                  "limit": 50,
                  "offset": 0,
                  "has_more": true,
                  "contacts": [
                    {
                      "id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "name": "Jane Doe",
                      "email": "jane.doe@example.com",
                      "phone": "+14155550123",
                      "address": "123 Market St, San Francisco, CA",
                      "tags": [
                        "vip",
                        "newsletter"
                      ],
                      "lead_source": "website_form",
                      "created_at": "2026-06-10T14:22:01.000Z",
                      "updated_at": "2026-06-11T09:05:44.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Contacts"
        ],
        "summary": "Create a contact",
        "description": "Inbound webhook — create (or match and update) a contact.\n\nMatch-and-update behavior: an existing contact matched by email (then phone) is updated in place and returns 200 with created:false; a new contact returns 201 with created:true. 400 if neither a usable email nor a valid E.164 phone is provided. Tags are not accepted here — use POST /webhooks/inbound/contacts/:id/tags. 401 if unauthenticated. The extended fields (custom_field_values, company_id, assigned_user_id, status) return 400 when invalid — e.g. a company or user outside this sub-account. If the contact saves but its custom field values fail to save, the response is 500 with contact_id.",
        "operationId": "create-contact",
        "parameters": [
          {
            "$ref": "#/components/parameters/XWebhookSource"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "created": true,
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Full name; if omitted it is derived from first_name + last_name, then email, then phone."
                  },
                  "first_name": {
                    "type": "string",
                    "description": "Used with last_name to build name when name is not given."
                  },
                  "last_name": {
                    "type": "string",
                    "description": "Used with first_name to build name when name is not given."
                  },
                  "email": {
                    "type": "string",
                    "description": "Lowercased on save; at least one of email or a valid phone is required (400 'contacts_email_or_phone_required' otherwise). Also used to match an existing contact."
                  },
                  "phone": {
                    "type": "string",
                    "description": "Must be E.164 (e.g. +14155550123); non-conforming values are silently dropped (treated as no phone). At least one of email or a valid phone is required. Also used to match an existing contact."
                  },
                  "address": {
                    "type": "string",
                    "description": "Optional postal/street address."
                  },
                  "source": {
                    "type": "string",
                    "description": "Lead source label; defaults to 'inbound_webhook' when omitted. Only applied when a new contact is created."
                  },
                  "custom_field_values": {
                    "type": "object",
                    "description": "Custom field values keyed by field key (see GET /webhooks/inbound/custom-fields). Keys must be active fields; values must match the field type (number, boolean, YYYY-MM-DD date, a select option, or a string array for multi-select). Read-only GoHighLevel fields are rejected."
                  },
                  "company_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Company to link the contact to; must be in this sub-account. null clears it."
                  },
                  "assigned_user_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Assign the contact to an active member of this sub-account. null unassigns."
                  },
                  "status": {
                    "type": "string",
                    "description": "Contact status label (max 100 characters; cannot be empty)."
                  }
                }
              },
              "example": {
                "name": "Jane Doe",
                "first_name": "Jane",
                "last_name": "Doe",
                "email": "jane.doe@example.com",
                "phone": "+14155550123",
                "address": "123 Market St, San Francisco, CA",
                "source": "website_form"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}": {
      "get": {
        "tags": [
          "Contacts"
        ],
        "summary": "Get a contact",
        "description": "Inbound webhook — get a single contact by id.\n\n400 if :id is not a valid UUID. 404 if the contact does not exist.",
        "operationId": "get-contact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact": {
                    "id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "name": "Jane Doe",
                    "email": "jane.doe@example.com",
                    "phone": "+14155550123",
                    "address": "123 Market St, San Francisco, CA",
                    "tags": [
                      "vip",
                      "newsletter"
                    ],
                    "lead_source": "website_form",
                    "created_at": "2026-06-10T14:22:01.000Z",
                    "updated_at": "2026-06-11T09:05:44.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Contacts"
        ],
        "summary": "Update a contact",
        "description": "Inbound webhook — update a contact; only fields present in the body are changed.\n\nOnly provided fields are changed. 400 if :id is not a valid UUID, if an explicit phone is non-E.164, or if no updatable fields are provided ('no updatable fields provided'). 404 if the contact does not exist. 409 if the phone is already used by another contact. The extended fields (custom_field_values, company_id, assigned_user_id, status) return 400 when invalid — e.g. a company or user outside this sub-account. If the contact saves but its custom field values fail to save, the response is 500 with contact_id.",
        "operationId": "update-contact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id to update.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/XWebhookSource"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "updated": true,
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Full name; if omitted but first_name/last_name are given, name is rebuilt from those."
                  },
                  "first_name": {
                    "type": "string",
                    "description": "Used with last_name to build name when name is not given."
                  },
                  "last_name": {
                    "type": "string",
                    "description": "Used with first_name to build name when name is not given."
                  },
                  "email": {
                    "type": "string",
                    "description": "New email; lowercased on save."
                  },
                  "phone": {
                    "type": "string",
                    "description": "New phone; must be E.164 (e.g. +14155550123). A provided-but-invalid phone returns 400 (not silently ignored, unlike create)."
                  },
                  "address": {
                    "type": "string",
                    "description": "New postal/street address."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Array of strings; when provided it replaces the entire tag set (lowercased, trimmed, de-duped). The create endpoint does not accept tags, but PATCH does. To merge or remove instead of replace, use the /tags endpoints."
                  },
                  "source": {
                    "type": "string",
                    "description": "Lead source label."
                  },
                  "custom_field_values": {
                    "type": "object",
                    "description": "Custom field values keyed by field key (see GET /webhooks/inbound/custom-fields). Keys must be active fields; values must match the field type (number, boolean, YYYY-MM-DD date, a select option, or a string array for multi-select). Read-only GoHighLevel fields are rejected."
                  },
                  "company_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Company to link the contact to; must be in this sub-account. null clears it."
                  },
                  "assigned_user_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Assign the contact to an active member of this sub-account. null unassigns."
                  },
                  "status": {
                    "type": "string",
                    "description": "Contact status label (max 100 characters; cannot be empty)."
                  }
                }
              },
              "example": {
                "name": "Jane A. Doe",
                "email": "jane.a.doe@example.com",
                "phone": "+14155550199",
                "address": "456 Mission St, San Francisco, CA",
                "tags": [
                  "vip",
                  "renewal"
                ],
                "source": "crm_sync"
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Contacts"
        ],
        "summary": "Delete a contact",
        "description": "Inbound webhook — permanently delete a contact.\n\nHard delete. 400 if :id is not a valid UUID. 404 if the contact does not exist. 409 if the contact is still referenced by related records (conversations, appointments, etc.) and cannot be deleted.",
        "operationId": "delete-contact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id to delete.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/tags": {
      "get": {
        "tags": [
          "Tags"
        ],
        "summary": "List all tags",
        "description": "Inbound webhook — list contact tags.\n\nReturns tags sorted A→Z, each with a usage count. Tags are derived from contacts and de-duped; the scan is capped at 5000 contacts.",
        "operationId": "list-tags",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "tag_count": 3,
                  "tags": [
                    {
                      "tag": "customer",
                      "count": 128
                    },
                    {
                      "tag": "newsletter",
                      "count": 42
                    },
                    {
                      "tag": "vip",
                      "count": 9
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/tags": {
      "post": {
        "tags": [
          "Tags"
        ],
        "summary": "Add tags to a contact",
        "description": "Inbound webhook — add (merge) one or more tags to a contact; existing tags are preserved.\n\nMerge semantics — existing tags are preserved and the new tags are unioned in. Tags are managed only via this endpoint and the DELETE /tags endpoint; the create endpoint doesn't accept tags. Accepts a single 'tag' and/or a 'tags[]' array (combined, lowercased, trimmed, de-duped). A write occurs only when the tag set actually changes, which fires the outbound contact.updated webhook. 'added' echoes the normalized incoming tags; 'tags' is the resulting full set. 400 if :id is not a valid UUID or no tags are provided. 404 if the contact does not exist.",
        "operationId": "add-tags",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id to add tags to.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/XWebhookSource"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "tags": [
                    "existing-tag",
                    "vip",
                    "newsletter",
                    "renewal"
                  ],
                  "added": [
                    "vip",
                    "newsletter",
                    "renewal"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tag": {
                    "type": "string",
                    "description": "A single tag to add; normalized (lowercased and trimmed). Provide at least one of tag or tags."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Array of tag strings to add; normalized (lowercased and trimmed) and de-duped. Provide at least one of tag or tags — 400 'provide at least one tag via \"tag\" or \"tags\"' if both are empty."
                  }
                }
              },
              "example": {
                "tag": "vip",
                "tags": [
                  "newsletter",
                  "renewal"
                ]
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Tags"
        ],
        "summary": "Remove tags from a contact",
        "description": "Inbound webhook — remove tags from a contact.\n\n'tags' in the response is the remaining tag set; 'removed' lists only tags that were actually present and removed. The write fires (and the outbound contact.updated webhook) only when the tag set actually changes. 400 if the id is not a valid UUID or no tags are provided; 404 if the contact does not exist.",
        "operationId": "remove-tags",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "<contact_id>",
                  "tags": [
                    "customer"
                  ],
                  "removed": [
                    "vip",
                    "newsletter",
                    "lead"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "tag": {
                    "type": "string",
                    "description": "A single tag to remove. At least one of tag or tags must resolve to a non-empty value (after lowercase and trim) or it returns 400. Tags are matched case-insensitively. Tags not currently on the contact are ignored."
                  },
                  "tags": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "An array of tag strings to remove. At least one of tag or tags must resolve to a non-empty value or it returns 400. Combined with tag, normalized (lowercase and trim) and de-duped. Tags not present on the contact are ignored."
                  }
                }
              },
              "example": {
                "tags": [
                  "vip",
                  "newsletter"
                ],
                "tag": "lead"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/notes": {
      "post": {
        "tags": [
          "Notes"
        ],
        "summary": "Create a contact note",
        "description": "Inbound webhook — create a note on a contact (local note only, not mirrored to GHL/HubSpot/Salesforce).\n\n400 if :id is not a valid UUID or content is missing/empty. 404 if the contact does not exist. Returns 201 on success.",
        "operationId": "create-note",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id the note is attached to.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "note": {
                    "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "content": "Called the customer; they requested a callback next Tuesday afternoon.",
                    "created_at": "2026-06-12T10:15:30.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "content": {
                    "type": "string",
                    "description": "The note text; required, minLength 1, maxLength 5000. Empty or whitespace-only content is rejected with 400."
                  }
                },
                "required": [
                  "content"
                ]
              },
              "example": {
                "content": "Called the customer; they requested a callback next Tuesday afternoon."
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Notes"
        ],
        "summary": "List contact notes",
        "description": "Inbound webhook — list a contact's active notes, newest first.\n\nReturns active notes only, newest first. 400 if :id is not a valid UUID. 404 if the contact does not exist.",
        "operationId": "list-notes",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id whose notes are listed.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "note_count": 1,
                  "notes": [
                    {
                      "id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "content": "Called the customer; they requested a callback next Tuesday afternoon.",
                      "agent_id": null,
                      "created_at": "2026-06-12T10:15:30.000Z",
                      "updated_at": "2026-06-12T10:15:30.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/notes/{noteId}": {
      "delete": {
        "tags": [
          "Notes"
        ],
        "summary": "Delete a contact note",
        "description": "Inbound webhook — soft-delete a contact's note.\n\nSoft delete only: the note is marked inactive rather than removed. 400 if :id or :noteId is not a valid UUID. 404 if the contact does not exist, or the note does not exist, belongs to another contact, or has already been deleted (an already-deleted note returns 404 since it is no longer active).",
        "operationId": "delete-note",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id the note belongs to.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "noteId",
            "in": "path",
            "required": true,
            "description": "The note id to delete. Must be an active note belonging to this contact.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "note_id": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/call": {
      "post": {
        "tags": [
          "AI Calls"
        ],
        "summary": "Make an AI call to a contact",
        "description": "Inbound webhook — place an outbound AI phone call to a contact. Dials the contact's phone on file.\n\nPlaces an outbound AI call to the contact's phone on file. 400: invalid contact id, invalid agent_id, or the contact has no phone on file. 401: missing or invalid key. 404: contact not found, or agent not found. The success response fields shown are representative.",
        "operationId": "make-ai-call",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id to call. The number dialed is this contact's phone on file. 404 if not found; 400 if the contact has no phone.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "call_id": "call_7f3a2b1c9d8e",
                  "message": "Call initiated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent_id": {
                    "type": "string",
                    "description": "UUID of the AI agent that should place the call. Must be a valid UUID (400 otherwise) and an agent you own (404 otherwise)."
                  },
                  "pool_id": {
                    "type": "string",
                    "description": "Optional. UUID of a number pool to round-robin the caller-ID (from-number) across. Omit to use the agent's default."
                  }
                },
                "required": [
                  "agent_id"
                ]
              },
              "example": {
                "agent_id": "<agent_id>",
                "pool_id": "<pool_id>"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/phone-numbers": {
      "get": {
        "tags": [
          "Phone numbers"
        ],
        "summary": "List owned numbers",
        "description": "Inbound webhook — list your owned phone numbers.\n\nReturns only active numbers, ordered by phone number ascending. `purchased` is true when the number was bought on the platform and false when it was imported. `agent_name` is the assigned agent's name, or null if the number is unassigned.",
        "operationId": "list-phone-numbers",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "phone_number_count": 2,
                  "phone_numbers": [
                    {
                      "id": "3f1c2b9a-7d4e-4a21-9c33-0a1b2c3d4e5f",
                      "phone_number": "+14155552671",
                      "purchased": true,
                      "agent_id": "a1b2c3d4-e5f6-7890-abcd-1234567890ab",
                      "agent_name": "Front Desk Agent",
                      "is_active": true,
                      "purchased_at": "2026-01-15T10:30:00.000Z",
                      "next_billing_date": "2026-07-15T10:30:00.000Z"
                    },
                    {
                      "id": "8c7b6a5d-4e3f-2a1b-9c8d-7e6f5a4b3c2d",
                      "phone_number": "+14155559876",
                      "purchased": false,
                      "agent_id": null,
                      "agent_name": null,
                      "is_active": true,
                      "purchased_at": null,
                      "next_billing_date": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/phone-numbers/{id}": {
      "get": {
        "tags": [
          "Phone numbers"
        ],
        "summary": "Get a phone number",
        "description": "Inbound webhook — get a single owned phone number by id.\n\nReturns the same shape as the list endpoint, including the assigned agent's name. 400 if the id is not a valid UUID; 404 if no such phone number exists.",
        "operationId": "get-phone-number",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The phone number id to retrieve.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "phone_number": {
                    "id": "3f1c2b9a-7d4e-4a21-9c33-0a1b2c3d4e5f",
                    "phone_number": "+14155552671",
                    "purchased": true,
                    "agent_id": "a1b2c3d4-e5f6-7890-abcd-1234567890ab",
                    "agent_name": "Front Desk Agent",
                    "is_active": true,
                    "purchased_at": "2026-01-15T10:30:00.000Z",
                    "next_billing_date": "2026-07-15T10:30:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "tags": [
          "Phone Number Purchasing"
        ],
        "summary": "Release a phone number",
        "description": "Release a phone number — permanent and cannot be undone. A purchased number goes back to the carrier and stops renewing (it may not be possible to buy it back); an imported number has its SIP setup removed. Any agent loses the number immediately.\n\nwas_purchased is true for numbers bought through Centerfy, false for imported ones. 400 if id is not a UUID. 404 if the number is not in this sub-account. 502 if the release failed — the number was not deleted.",
        "operationId": "release-phone-number",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The phone number id (from GET /webhooks/inbound/phone-numbers or the purchase/import response).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "released": true,
                  "phone_number_id": "9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a",
                  "phone_number": "+14155550123",
                  "was_purchased": true,
                  "dispatch_rule_deleted": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/phone-numbers/{id}/agent": {
      "post": {
        "tags": [
          "Phone numbers"
        ],
        "summary": "Assign to an agent",
        "description": "Inbound webhook — assign a phone number to an agent.\n\nAssigning a number routes its inbound calls to the agent. Idempotent when re-assigning to the SAME agent (returns 200 with already_assigned: true). 400 for invalid UUIDs; 404 if the number or agent does not exist; 409 if the number is already assigned to a DIFFERENT agent (unassign first); 502 if provisioning fails.",
        "operationId": "assign-phone",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The phone number id to assign.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "assigned": true,
                  "phone_number_id": "3f1c2b9a-7d4e-4a21-9c33-0a1b2c3d4e5f",
                  "phone_number": "+14155552671",
                  "agent_id": "a1b2c3d4-e5f6-7890-abcd-1234567890ab"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent_id": {
                    "type": "string",
                    "description": "The agent id to assign the number to. Required, and must be a valid UUID for an agent you own, else 400/404."
                  }
                },
                "required": [
                  "agent_id"
                ]
              },
              "example": {
                "agent_id": "<agent_id>"
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Phone numbers"
        ],
        "summary": "Remove from agent",
        "description": "Inbound webhook — remove the agent from a phone number.\n\nRemoves the agent so the number no longer routes inbound calls, while keeping the number active and immediately reassignable. Releasing the number itself is a separate operation. Idempotent: a number that has no agent succeeds with already_unassigned: true. 400 if the id is not a valid UUID; 404 if no such phone number exists.",
        "operationId": "unassign-phone",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The phone number id to unassign.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "unassigned": true,
                  "phone_number_id": "3f1c2b9a-7d4e-4a21-9c33-0a1b2c3d4e5f",
                  "phone_number": "+14155552671",
                  "agent_id": null
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/number-pools": {
      "get": {
        "tags": [
          "Number pools"
        ],
        "summary": "List number pools",
        "description": "Inbound webhook — list your number pools.\n\nPools group owned numbers so outbound calls can rotate across them. Each pool resolves its member numbers to the actual phone strings. Ordered by created_at descending.",
        "operationId": "list-number-pools",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "pool_count": 1,
                  "number_pools": [
                    {
                      "id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
                      "name": "Outbound rotation",
                      "number_count": 2,
                      "numbers": [
                        {
                          "phone_number_id": "3f1c2b9a-7d4e-4a21-9c33-0a1b2c3d4e5f",
                          "phone_number": "+14155552671"
                        },
                        {
                          "phone_number_id": "8c7b6a5d-4e3f-2a1b-9c8d-7e6f5a4b3c2d",
                          "phone_number": "+14155559876"
                        }
                      ],
                      "created_at": "2026-03-01T09:00:00.000Z",
                      "updated_at": "2026-03-10T12:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Number pools"
        ],
        "summary": "Create a number pool",
        "description": "Inbound webhook — create a number pool.\n\nReturns HTTP 201. The pool starts empty — numbers are added separately. 400 if name is missing or blank.",
        "operationId": "create-number-pool",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "created": true,
                  "number_pool": {
                    "id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
                    "name": "Outbound rotation",
                    "number_count": 0,
                    "numbers": [],
                    "created_at": "2026-06-12T14:00:00.000Z",
                    "updated_at": "2026-06-12T14:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The pool name. Required and may not be empty or whitespace-only (400 otherwise)."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Outbound rotation"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/number-pools/{id}": {
      "get": {
        "tags": [
          "Number pools"
        ],
        "summary": "Get a number pool",
        "description": "Inbound webhook — get a single number pool.\n\nReturns the same public shape as a row from the list endpoint, including member numbers resolved to phone strings and number_count. 400 if the id is not a valid UUID; 404 if the number pool does not exist.",
        "operationId": "get-number-pool",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the number pool id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "number_pool": {
                    "id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
                    "name": "Outbound rotation",
                    "number_count": 2,
                    "numbers": [
                      {
                        "phone_number_id": "3f1c2b9a-7d4e-4a21-9c33-0a1b2c3d4e5f",
                        "phone_number": "+14155552671"
                      },
                      {
                        "phone_number_id": "8c7b6a5d-4e3f-2a1b-9c8d-7e6f5a4b3c2d",
                        "phone_number": "+14155559876"
                      }
                    ],
                    "created_at": "2026-03-01T09:00:00.000Z",
                    "updated_at": "2026-03-10T12:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "tags": [
          "Number pools"
        ],
        "summary": "Delete a number pool",
        "description": "Inbound webhook — delete a number pool.\n\nAlso removes the pool's number assignments — removed_number_assignments reports how many were cleared — but does NOT delete or release the phone numbers themselves; they stay in your inventory, just no longer grouped. 400 if the id is not a valid UUID; 404 if the number pool does not exist.",
        "operationId": "delete-number-pool",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the number pool id to delete.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "number_pool_id": "d4e5f6a7-b8c9-0123-4567-89abcdef0123",
                  "removed_number_assignments": 2
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Number pools"
        ],
        "summary": "Update a number pool",
        "description": "Inbound webhook — update a number pool. Rename the pool and/or replace its complete member set.\n\n400: invalid pool id, nothing to update, blank name, phone_number_ids not an array, an invalid UUID in the list, or a number you do not own. 404: pool not found. 401: missing or invalid key.",
        "operationId": "update-number-pool",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the number pool id; 404 otherwise.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "updated": true,
                  "added_numbers": 1,
                  "removed_numbers": 0,
                  "number_pool": {
                    "id": "3f1c9a2e-5b6d-4e8a-9c1f-2a3b4c5d6e7f",
                    "name": "West Coast Sales",
                    "number_count": 2,
                    "numbers": [
                      {
                        "phone_number_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                        "phone_number": "+14155550123"
                      },
                      {
                        "phone_number_id": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
                        "phone_number": "+14155550199"
                      }
                    ],
                    "created_at": "2026-05-01T12:00:00.000Z",
                    "updated_at": "2026-06-12T09:30:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Optional. New name for the pool. If present it must be non-blank (400 'name cannot be blank' otherwise)."
                  },
                  "phone_number_ids": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Optional. The COMPLETE desired set of member phone-number UUIDs (set semantics). Membership is diffed: numbers not in the list are removed, new ones added. Pass [] to clear all numbers. Must be an array (400 otherwise); each entry must be a valid UUID and a phone number you own (400 otherwise). At least one of name or phone_number_ids must be sent (400 'nothing to update' otherwise)."
                  }
                }
              },
              "example": {
                "name": "West Coast Sales",
                "phone_number_ids": [
                  "<phone_number_id_1>",
                  "<phone_number_id_2>"
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/number-pools/{id}/numbers": {
      "post": {
        "tags": [
          "Number pools"
        ],
        "summary": "Add a number to a pool",
        "description": "Inbound webhook — add a single owned number to a pool. Idempotent.\n\nTargeted counterpart to the set-based PATCH. Returns 201 on a new add. Idempotent: if the number is already in the pool it returns 200 with already_in_pool:true and adds nothing. 400: invalid pool id or phone_number_id. 404: pool or phone number not found. 401: missing or invalid key.",
        "operationId": "add-pool-number",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the number pool id; 404 otherwise.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "added": true,
                  "number_pool_id": "3f1c9a2e-5b6d-4e8a-9c1f-2a3b4c5d6e7f",
                  "phone_number_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "phone_number": "+14155550123"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "phone_number_id": {
                    "type": "string",
                    "description": "UUID of the phone number to add. Must be a valid UUID (400 otherwise) and a phone number you own (404 otherwise)."
                  }
                },
                "required": [
                  "phone_number_id"
                ]
              },
              "example": {
                "phone_number_id": "<phone_number_id>"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/number-pools/{id}/numbers/{numberId}": {
      "delete": {
        "tags": [
          "Number pools"
        ],
        "summary": "Remove a number from a pool",
        "description": "Inbound webhook — remove a single number from a pool. The number stays in your inventory.\n\nRemoves only the pool membership; the phone number remains in your inventory. 400: invalid pool id or number id. 404: pool not found, or the number is not in this pool. 401: missing or invalid key.",
        "operationId": "remove-pool-number",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the number pool id; 404 otherwise.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "numberId",
            "in": "path",
            "required": true,
            "description": "the phone number id to remove from the pool; 404 if it is not currently a member of this pool.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "removed": true,
                  "number_pool_id": "3f1c9a2e-5b6d-4e8a-9c1f-2a3b4c5d6e7f",
                  "phone_number_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/voices": {
      "get": {
        "tags": [
          "Voices"
        ],
        "summary": "List voices",
        "description": "Inbound webhook — list available AI voices.\n\nEach voice's `name` is the public identifier — pass it as voice_id when creating or updating an agent. 400 if gender is not male or female.",
        "operationId": "list-voices",
        "parameters": [
          {
            "name": "gender",
            "in": "query",
            "required": false,
            "description": "optional filter; must be 'male' or 'female' (case-insensitive). Any other value → 400.",
            "schema": {
              "type": "string",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "voice_count": 2,
                  "voices": [
                    {
                      "name": "Marissa",
                      "gender": "female",
                      "description": "Warm, friendly female voice"
                    },
                    {
                      "name": "David",
                      "gender": "male",
                      "description": "Clear, professional male voice"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/voices/preview": {
      "post": {
        "tags": [
          "Voices"
        ],
        "summary": "Preview a voice",
        "description": "Inbound webhook — preview a voice by name.\n\nOn success returns the MP3 audio INLINE (Content-Type audio/mpeg, Content-Disposition inline), not JSON. 400 if name is missing or unknown; 500 if audio generation fails; 502 if the text-to-speech service is unavailable.",
        "operationId": "preview-voice",
        "parameters": [],
        "responses": {
          "200": {
            "description": "MP3 audio",
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "friendly voice NAME, e.g. \"Marissa\" (from GET /webhooks/inbound/voices). An unknown name → 400."
                  },
                  "text": {
                    "type": "string",
                    "description": "optional custom line for the voice to speak; defaults to a built-in sample greeting if omitted."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Marissa",
                "text": "Hello, thanks for calling. How can I help you today?"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agents": {
      "post": {
        "tags": [
          "Agents"
        ],
        "summary": "Create an agent",
        "description": "Inbound webhook — create an AI agent.\n\nErrors: 400 (validation — missing or invalid required fields, or conditional rules not met), 401 (unauthenticated), 500 (insert error). Success is 201.",
        "operationId": "create-agent-webhook",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "agent": {
                    "id": "f1e2d3c4-0000-4000-8000-000000000001",
                    "name": "Front Desk Assistant",
                    "system_prompt": "You are the friendly front-desk assistant for Acme Clinic.",
                    "is_active": true,
                    "created_at": "2026-06-12T10:20:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "the agent's display name (non-empty string, required)."
                  },
                  "voice_id": {
                    "type": "string",
                    "description": "the voice — accepts a friendly voice name (e.g. \"Marissa\") or a raw voice id (e.g. \"Sulafat\"). Required, non-empty string."
                  },
                  "llm_model": {
                    "type": "string",
                    "description": "the LLM model to use (non-empty string, required)."
                  },
                  "language": {
                    "type": "string",
                    "description": "the agent language code (non-empty string, required)."
                  },
                  "max_duration": {
                    "type": "number",
                    "description": "maximum call duration (required number)."
                  },
                  "temperature": {
                    "type": "number",
                    "description": "LLM sampling temperature (required number)."
                  },
                  "silence_timeout": {
                    "type": "number",
                    "description": "silence timeout (required number)."
                  },
                  "voice_activity_timeout": {
                    "type": "number",
                    "description": "voice-activity timeout (required number). The alt spelling 'voice_activbity_timeout' is also accepted in its place."
                  },
                  "background_noise": {
                    "type": "boolean",
                    "description": "whether ambient background noise is enabled (required boolean). When true, 'background_type' is required and must be one of: call_center, coffee_shop, conventional_hall."
                  },
                  "voicemail_detection": {
                    "type": "boolean",
                    "description": "whether voicemail detection is enabled (required boolean). When true, 'voicemail_type' is required (leave_message | hangup); when voicemail_type='leave_message', 'voicemail_message' is required."
                  },
                  "webhook": {
                    "type": "boolean",
                    "description": "whether an outbound webhook is enabled (required boolean). When true, 'webhook_url' is required."
                  },
                  "recording": {
                    "type": "boolean",
                    "description": "whether call recording is enabled (required boolean)."
                  },
                  "backcchanneling": {
                    "type": "boolean",
                    "description": "whether backchanneling is enabled (required boolean). The alt spelling 'backchanneling' is also accepted in its place."
                  },
                  "background_type": {
                    "type": "string",
                    "description": "ambient noise profile; required only when background_noise=true. One of: call_center, coffee_shop, conventional_hall."
                  },
                  "voicemail_type": {
                    "type": "string",
                    "description": "voicemail action; required only when voicemail_detection=true. One of: leave_message, hangup."
                  },
                  "voicemail_message": {
                    "type": "string",
                    "description": "the message left on voicemail; required only when voicemail_type='leave_message'."
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "the outbound webhook URL; required only when webhook=true."
                  },
                  "system_prompt": {
                    "type": "string",
                    "description": "the agent's instructions; if omitted, a standard default prompt is applied."
                  },
                  "is_active": {
                    "type": "boolean",
                    "description": "whether the agent is active; defaults to true when omitted."
                  }
                },
                "required": [
                  "name",
                  "voice_id",
                  "llm_model",
                  "language",
                  "max_duration",
                  "temperature",
                  "silence_timeout",
                  "voice_activity_timeout",
                  "background_noise",
                  "voicemail_detection",
                  "webhook",
                  "recording",
                  "backcchanneling"
                ]
              },
              "example": {
                "name": "Front Desk Assistant",
                "voice_id": "Marissa",
                "llm_model": "gpt-4o",
                "language": "en",
                "system_prompt": "You are the friendly front-desk assistant for Acme Clinic.",
                "max_duration": 600,
                "temperature": 0.7,
                "silence_timeout": 10,
                "voice_activity_timeout": 5,
                "background_noise": true,
                "background_type": "call_center",
                "voicemail_detection": true,
                "voicemail_type": "leave_message",
                "voicemail_message": "Sorry we missed you — please leave a message and we'll call back.",
                "backcchanneling": true,
                "webhook": true,
                "webhook_url": "https://example.com/agent-events",
                "recording": true,
                "is_active": true
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Agents"
        ],
        "summary": "List AI agents",
        "description": "Inbound webhook — list your AI agents. Read-only; useful to discover agent_id values to pass to the make-AI-call and send-message webhooks. Optional active filter.\n\nRead-only. Agents are ordered newest first by created_at and use a safe public projection. 401 if unauthenticated; 500 on error. The example agent fields are representative.",
        "operationId": "list-agents",
        "parameters": [
          {
            "name": "active",
            "in": "query",
            "required": false,
            "description": "optional filter on is_active: true returns only active agents, false returns only paused/inactive agents. Omit to return all.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "agent_count": 2,
                  "agents": [
                    {
                      "id": "b2c3d4e5-3333-4abc-9def-000000000030",
                      "name": "Front Desk AI",
                      "is_active": true
                    },
                    {
                      "id": "b2c3d4e5-3333-4abc-9def-000000000031",
                      "name": "After Hours AI",
                      "is_active": false
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/agents/{id}": {
      "get": {
        "tags": [
          "Agents"
        ],
        "summary": "Get an agent",
        "description": "Inbound webhook — get a single AI agent by id. Returns the same safe field set as the list endpoint.\n\nRead-only. Returns the same safe public projection as the list endpoint. 401 if unauthenticated; 400 for an invalid agent id; 404 if the agent does not exist; 500 on server error. The example agent fields are representative — the exact field set matches the list endpoint.",
        "operationId": "get-agent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent id. A non-uuid returns 400; an agent that does not exist returns 404.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "agent": {
                    "id": "b2c3d4e5-3333-4abc-9def-000000000030",
                    "name": "Front Desk AI",
                    "is_active": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Agents"
        ],
        "summary": "Update an agent",
        "description": "Inbound webhook — update an AI agent.\n\nPartial update — every field is optional, but the body must contain at least one updatable field. Sending one field never wipes the rest; existing configuration is preserved for fields you omit. Errors: 400 (invalid agent id / validation failure / empty body), 401 (unauthenticated), 404 (agent not found), 500 (server error). Success is 200.",
        "operationId": "update-agent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "agent": {
                    "id": "<agent_id>",
                    "name": "Front Desk Assistant (v2)",
                    "is_active": false,
                    "updated_at": "2026-06-12T10:25:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The agent's display name; if present, must be a non-empty string."
                  },
                  "voice_id": {
                    "type": "string",
                    "description": "The voice — accepts a friendly voice name (e.g. \"Marissa\") or a raw voice id. If present, must be a non-empty string."
                  },
                  "llm_model": {
                    "type": "string",
                    "description": "The LLM model; if present, must be a non-empty string."
                  },
                  "language": {
                    "type": "string",
                    "description": "The agent language code; if present, must be a non-empty string."
                  },
                  "max_duration": {
                    "type": "number",
                    "description": "Maximum call duration; if present, must be a number."
                  },
                  "temperature": {
                    "type": "number",
                    "description": "LLM sampling temperature; if present, must be a number."
                  },
                  "silence_timeout": {
                    "type": "number",
                    "description": "Silence timeout; if present, must be a number."
                  },
                  "voice_activity_timeout": {
                    "type": "number",
                    "description": "Voice-activity timeout; if present, must be a number. The alternate spelling 'voice_activbity_timeout' is also accepted."
                  },
                  "background_noise": {
                    "type": "boolean",
                    "description": "Whether ambient background noise is enabled; if present, must be a boolean. When set to true, 'background_type' is required."
                  },
                  "background_type": {
                    "type": "string",
                    "description": "Ambient noise profile; if present, must be one of: call_center, coffee_shop, conventional_hall. Required when setting background_noise=true."
                  },
                  "voicemail_detection": {
                    "type": "boolean",
                    "description": "Whether voicemail detection is enabled; if present, must be a boolean. When set to true, 'voicemail_type' is required."
                  },
                  "voicemail_type": {
                    "type": "string",
                    "description": "Voicemail action; if present, must be one of: leave_message, hangup. Required when setting voicemail_detection=true; when 'leave_message', 'voicemail_message' is required."
                  },
                  "voicemail_message": {
                    "type": "string",
                    "description": "The message left on voicemail; if present, must be a string. Required when voicemail_type='leave_message'."
                  },
                  "backcchanneling": {
                    "type": "boolean",
                    "description": "Whether backchanneling is enabled; if present, must be a boolean. The alternate spelling 'backchanneling' is also accepted."
                  },
                  "is_active": {
                    "type": "boolean",
                    "description": "Whether the agent is active; if present, must be a boolean."
                  },
                  "webhook": {
                    "type": "boolean",
                    "description": "Whether an outbound webhook is enabled; if present, must be a boolean. When set to true, 'webhook_url' is required."
                  },
                  "webhook_url": {
                    "type": "string",
                    "description": "The outbound webhook URL; if present, must be a string. Required when setting webhook=true."
                  },
                  "recording": {
                    "type": "boolean",
                    "description": "Whether call recording is enabled; if present, must be a boolean."
                  }
                }
              },
              "example": {
                "name": "Front Desk Assistant (v2)",
                "temperature": 0.6,
                "is_active": false
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Agents"
        ],
        "summary": "Archive an agent",
        "description": "Inbound webhook — delete (archive and deactivate) an AI agent.\n\nNot a hard delete — archives the agent and deactivates it (is_active=false) so it stops taking calls, while its phone and call history remain intact. Idempotent: re-deleting an already-archived agent succeeds again. Errors: 400 (invalid agent id), 401 (unauthenticated), 404 (agent not found), 500 (server error). Success is 200.",
        "operationId": "archive-agent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "agent_id": "<agent_id>",
                  "archived_at": "2026-06-12T10:30:00.000Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/agents/{id}/duplicate": {
      "post": {
        "tags": [
          "Agents"
        ],
        "summary": "Duplicate an agent",
        "description": "Inbound webhook — duplicate an AI agent.\n\nCreates a copy of the source agent carrying its full configuration, but with a fresh id and \"(copy)\" appended to the name. No request body. Errors: 400 (invalid agent id), 401 (unauthenticated), 404 (agent not found), 500 (server error). Success is 201.",
        "operationId": "duplicate-agent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The source agent id to duplicate.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "duplicated_from": "<agent_id>",
                  "agent": {
                    "id": "c9b8a7d6-0000-4000-8000-000000000002",
                    "name": "Front Desk Assistant (copy)",
                    "is_active": true,
                    "created_at": "2026-06-12T10:35:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/agent-folders": {
      "post": {
        "tags": [
          "Agent folders"
        ],
        "summary": "Create a folder",
        "description": "Inbound webhook — create an agent folder.\n\nCreates the folder only; agents are added to it separately. Errors: 400 (folder name required / invalid color hex), 401 (unauthenticated), 500 (server error). Success is 201.",
        "operationId": "create-agent-folder",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "folder": {
                    "id": "b2c3d4e5-0000-4000-8000-000000000003",
                    "name": "Support Agents",
                    "color": "#3b82f6",
                    "created_at": "2026-06-12T10:40:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "The folder name. Required, non-empty after trimming."
                  },
                  "color": {
                    "type": "string",
                    "description": "Optional hex color, e.g. #3b82f6 (must match /^#[0-9a-fA-F]{3,8}$/). If omitted, defaults to #3b82f6."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Support Agents",
                "color": "#3b82f6"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Agent folders"
        ],
        "summary": "List folders",
        "description": "Inbound webhook — list agent folders.\n\nRead-only. Folders are returned ordered oldest first. 401 if unauthenticated; 500 on server error.",
        "operationId": "list-agent-folders",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "folder_count": 2,
                  "folders": [
                    {
                      "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                      "name": "Sales Agents",
                      "color": "#3b82f6",
                      "created_at": "2026-05-01T10:00:00.000Z",
                      "updated_at": "2026-05-10T12:30:00.000Z"
                    },
                    {
                      "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
                      "name": "Support Agents",
                      "color": "#10b981",
                      "created_at": "2026-05-02T11:00:00.000Z",
                      "updated_at": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/agent-folders/{id}": {
      "patch": {
        "tags": [
          "Agent folders"
        ],
        "summary": "Update a folder",
        "description": "Inbound webhook — update an agent folder.\n\nPartial update — both fields are optional, but send at least one (empty body → 400 'request body must contain name and/or color'). Errors: 400 (invalid folder id / empty name / bad color hex), 401 (unauthenticated), 404 (folder not found), 500 (server error).",
        "operationId": "update-agent-folder",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent folder id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "folder": {
                    "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                    "name": "Outbound Sales Agents",
                    "color": "#10b981",
                    "created_at": "2026-05-01T10:00:00.000Z",
                    "updated_at": "2026-06-12T09:15:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New folder name; if present, must be a non-empty string (empty → 400)."
                  },
                  "color": {
                    "type": "string",
                    "description": "New folder color as a hex value, e.g. #3b82f6; if present, must match /^#[0-9a-fA-F]{3,8}$/ (invalid → 400)."
                  }
                }
              },
              "example": {
                "name": "Outbound Sales Agents",
                "color": "#10b981"
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Agent folders"
        ],
        "summary": "Delete a folder",
        "description": "Inbound webhook — delete an empty agent folder.\n\nThe folder must be EMPTY: if any agents are still assigned, the delete is REFUSED with 409 (the body includes agent_count) — remove them first via DELETE …/agents/:agentId. 400 if the folder id is invalid; 404 if the folder does not exist; 409 if the folder is not empty; 500 on failure.",
        "operationId": "delete-agent-folder",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the agent folder id to delete.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "folder_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/agent-folders/{id}/agents": {
      "post": {
        "tags": [
          "Agent folders"
        ],
        "summary": "Assign an agent",
        "description": "Inbound webhook — assign an agent to a folder.\n\nAn agent belongs to at most one folder, so assigning it re-homes it from any existing folder. Errors: 400 (invalid folder id / invalid agent_id), 401 (unauthenticated), 404 (folder or agent not found), 500 (server error).",
        "operationId": "assign-agent-folder",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The folder id to assign the agent into.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "assigned": true,
                  "agent_id": "f1e2d3c4-b5a6-4978-8c9d-0e1f2a3b4c5d",
                  "folder_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent_id": {
                    "type": "string",
                    "description": "The agent to assign; must be a valid uuid. An agent belongs to at most one folder, so assigning re-homes it from any existing folder."
                  }
                },
                "required": [
                  "agent_id"
                ]
              },
              "example": {
                "agent_id": "<agent_id>"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agent-folders/{id}/agents/{agentId}": {
      "delete": {
        "tags": [
          "Agent folders"
        ],
        "summary": "Remove an agent",
        "description": "Inbound webhook — remove an agent from a folder.\n\nRemoves the agent from the folder, moving it back to root. Errors: 400 (invalid folder/agent id), 401 (unauthenticated), 404 (agent not found in that folder), 500 (server error).",
        "operationId": "remove-agent-folder",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The folder id the agent should be removed from.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "agentId",
            "in": "path",
            "required": true,
            "description": "The agent id to unassign (moves it back to root).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "removed": true,
                  "agent_id": "f1e2d3c4-b5a6-4978-8c9d-0e1f2a3b4c5d",
                  "folder_id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/agents/{id}/custom-tools": {
      "get": {
        "tags": [
          "Tools"
        ],
        "summary": "List agent custom tools",
        "description": "Inbound webhook — list an agent's custom tools.\n\nReturns only CUSTOM-tool assignments; built-in and system tools are excluded. The stored api_key secret is never returned — has_api_key indicates whether one is set. enabled defaults to true. 400 if id is not a valid uuid. 404 if the agent does not exist. 500 on error.",
        "operationId": "list-agent-custom-tools",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "agent_id": "<agent_id>",
                  "custom_tool_count": 1,
                  "custom_tools": [
                    {
                      "assignment_id": "d3b2a1c0-1111-2222-3333-444455556666",
                      "enabled": true,
                      "order_index": 0,
                      "configuration": {},
                      "tool": {
                        "id": "a1b2c3d4-5555-6666-7777-888899990000",
                        "name": "lookup_order",
                        "description": "Look up an order by id",
                        "url": "https://api.example.com/orders",
                        "method": "GET",
                        "auth_required": true,
                        "has_api_key": true,
                        "parameters": [
                          {
                            "name": "order_id",
                            "type": "string",
                            "description": "The order id",
                            "required": true
                          }
                        ],
                        "created_at": "2026-06-01T12:00:00.000Z",
                        "updated_at": "2026-06-10T09:30:00.000Z"
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Tools"
        ],
        "summary": "Assign a tool to an agent",
        "description": "Inbound webhook — assign a custom tool to an agent.\n\nAssign-only and idempotent: if the tool is already assigned, returns 200 with already_assigned:true (and the existing assignment_id, enabled, and order_index); a fresh assignment returns 201. 400 for an invalid agent id or custom_tool_id. 404 if the agent or the tool does not exist. Use DELETE …/custom-tools/:toolId to remove. 500 on failure.",
        "operationId": "assign-custom-tool",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "assigned": true,
                  "assignment_id": "d3b2a1c0-1111-2222-3333-444455556666",
                  "agent_id": "<agent_id>",
                  "custom_tool_id": "<custom_tool_id>",
                  "enabled": true,
                  "order_index": 1
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "custom_tool_id": {
                    "type": "string",
                    "description": "id of the custom tool to assign; must be a valid uuid and an existing custom tool (404 otherwise)."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "whether the tool is enabled on the agent. Defaults to true; set to true unless exactly false."
                  },
                  "configuration": {
                    "type": "object",
                    "description": "per-agent tool configuration (free-form object). Defaults to {} when omitted or not an object."
                  }
                },
                "required": [
                  "custom_tool_id"
                ]
              },
              "example": {
                "custom_tool_id": "<custom_tool_id>",
                "enabled": true,
                "configuration": {}
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agents/{id}/custom-tools/{toolId}": {
      "delete": {
        "tags": [
          "Tools"
        ],
        "summary": "Remove a tool from an agent",
        "description": "Inbound webhook — remove a custom tool from an agent.\n\nRemoves only the assignment between this agent and tool; the custom tool itself is untouched and can be re-assigned later. 400 if id or toolId is not a valid uuid. 404 if the agent does not exist, or the tool isn't assigned to it ('Custom tool <id> is not assigned to agent <id>'). 500 on failure.",
        "operationId": "remove-agent-custom-tool",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "toolId",
            "in": "path",
            "required": true,
            "description": "the custom tool id to de-assign from the agent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "removed": true,
                  "agent_id": "<agent_id>",
                  "custom_tool_id": "<custom_tool_id>"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/custom-tools": {
      "post": {
        "tags": [
          "Tools"
        ],
        "summary": "Create a custom tool",
        "description": "Inbound webhook — create a custom tool.\n\nCreates the tool UNASSIGNED — attach it to an agent separately. Returns 201 on success. 400 if name, description, url, or method is missing or blank, the method is not in the allowed set, or parameters is not an array. 500 on failure.",
        "operationId": "create-custom-tool",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "created": true,
                  "custom_tool": {
                    "id": "a1b2c3d4-5555-6666-7777-888899990000",
                    "name": "lookup_order",
                    "description": "Look up an order by id",
                    "url": "https://api.example.com/orders",
                    "method": "GET",
                    "auth_required": true,
                    "has_api_key": true,
                    "parameters": [
                      {
                        "name": "order_id",
                        "type": "string",
                        "description": "The order id",
                        "required": true
                      }
                    ],
                    "created_at": "2026-06-12T10:00:00.000Z",
                    "updated_at": "2026-06-12T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "tool display name; must be non-empty. Normalised before storage (lowercased, spaces → underscores)."
                  },
                  "description": {
                    "type": "string",
                    "description": "what the tool does; must be non-empty."
                  },
                  "url": {
                    "type": "string",
                    "description": "the target endpoint URL the tool calls; must be non-empty."
                  },
                  "method": {
                    "type": "string",
                    "description": "HTTP method; must be non-empty. Uppercased and must be one of: GET, POST, PUT, PATCH, DELETE (else 400)."
                  },
                  "auth_required": {
                    "type": "boolean",
                    "description": "whether the target endpoint requires auth. Set to true only when exactly true; defaults to false."
                  },
                  "api_key": {
                    "type": "string",
                    "description": "secret credential for the tool's target endpoint. Never echoed back (responses expose has_api_key only). Null if omitted."
                  },
                  "parameters": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "optional array of parameter objects { name:string, type:string, description:string, required:boolean }. If present, must be an array (else 400); defaults to []."
                  }
                },
                "required": [
                  "name",
                  "description",
                  "url",
                  "method"
                ]
              },
              "example": {
                "name": "Lookup Order",
                "description": "Look up an order by id",
                "url": "https://api.example.com/orders",
                "method": "GET",
                "auth_required": true,
                "api_key": "sk_live_target_endpoint_secret",
                "parameters": [
                  {
                    "name": "order_id",
                    "type": "string",
                    "description": "The order id",
                    "required": true
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/custom-tools/{id}": {
      "get": {
        "tags": [
          "Tools"
        ],
        "summary": "Get a custom tool",
        "description": "Inbound webhook — get a single custom tool.\n\nThe stored api_key secret is never returned — has_api_key is returned instead. 400 if id is not a valid uuid. 404 if the custom tool does not exist. 500 on error.",
        "operationId": "get-custom-tool",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the custom tool id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "custom_tool": {
                    "id": "a1b2c3d4-5555-6666-7777-888899990000",
                    "name": "lookup_order",
                    "description": "Look up an order by id",
                    "url": "https://api.example.com/orders",
                    "method": "GET",
                    "auth_required": true,
                    "has_api_key": true,
                    "parameters": [
                      {
                        "name": "order_id",
                        "type": "string",
                        "description": "The order id",
                        "required": true
                      }
                    ],
                    "created_at": "2026-06-01T12:00:00.000Z",
                    "updated_at": "2026-06-10T09:30:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Tools"
        ],
        "summary": "Update a custom tool",
        "description": "Inbound webhook — update a custom tool.\n\nPartial update — only fields present in the body change. All body fields are optional, but at least one updatable field must be sent (400 'no updatable fields provided'). 400 also for a blank name, url, or method, an invalid method, or non-array parameters. 404 if the custom tool does not exist. 500 on failure.",
        "operationId": "update-custom-tool",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the custom tool id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "updated": true,
                  "custom_tool": {
                    "id": "a1b2c3d4-5555-6666-7777-888899990000",
                    "name": "lookup_order",
                    "description": "Look up an order by its id and return status",
                    "url": "https://api.example.com/orders",
                    "method": "GET",
                    "auth_required": true,
                    "has_api_key": true,
                    "parameters": [
                      {
                        "name": "order_id",
                        "type": "string",
                        "description": "The order id",
                        "required": true
                      }
                    ],
                    "created_at": "2026-06-01T12:00:00.000Z",
                    "updated_at": "2026-06-12T11:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "partial update — only sent keys change. If sent, it must be non-empty (400 'name cannot be blank'); re-normalised (lowercased, spaces → underscores)."
                  },
                  "description": {
                    "type": "string",
                    "description": "if sent, replaces the description; an empty value clears it."
                  },
                  "url": {
                    "type": "string",
                    "description": "if sent, it must be non-empty (400 'url cannot be blank')."
                  },
                  "method": {
                    "type": "string",
                    "description": "if sent, it must be non-empty (400 'method cannot be blank'); uppercased and must be one of GET, POST, PUT, PATCH, DELETE (else 400)."
                  },
                  "auth_required": {
                    "type": "boolean",
                    "description": "if sent, set to true only when exactly true, otherwise false."
                  },
                  "api_key": {
                    "type": "string",
                    "description": "if sent, replaces the stored secret (an empty value clears it). Never echoed back."
                  },
                  "parameters": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "if sent, must be an array of parameter objects { name, type, description, required } (else 400)."
                  }
                }
              },
              "example": {
                "description": "Look up an order by its id and return status",
                "method": "GET",
                "auth_required": true,
                "parameters": [
                  {
                    "name": "order_id",
                    "type": "string",
                    "description": "The order id",
                    "required": true
                  }
                ]
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Tools"
        ],
        "summary": "Delete a custom tool",
        "description": "Inbound webhook — delete a custom tool.\n\nUnlinks the tool from every agent, then deletes the tool; the response reports removed_agent_assignments. 400 if id is not a valid uuid. 404 if the custom tool does not exist. 409 if the tool is registered on the voice engine — delete it from the in-app tool editor instead so the engine-side definition is also torn down. 500 on failure.",
        "operationId": "delete-custom-tool",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the custom tool id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "custom_tool_id": "<custom_tool_id>",
                  "removed_agent_assignments": 2
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/knowledge-bases": {
      "get": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "List knowledge bases",
        "description": "List your knowledge bases, newest-first, each with its document and text-entry source counts.\n\nResults are ordered by creation date, descending. 200 on success.",
        "operationId": "list-knowledge-bases",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "knowledge_base_count": 1,
                  "knowledge_bases": [
                    {
                      "id": "66666666-7777-8888-9999-000000000000",
                      "name": "Product FAQ",
                      "description": "Frequently asked questions about our product",
                      "chunk_size": 1000,
                      "chunk_overlap": 200,
                      "embedding_model": null,
                      "is_enabled": true,
                      "document_count": 3,
                      "text_entry_count": 2,
                      "source_count": 5,
                      "vector_count": 142,
                      "created_at": "2026-06-12T10:00:00.000Z",
                      "updated_at": "2026-06-12T10:05:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Create a knowledge base",
        "description": "Create a knowledge base. The knowledge base starts empty.\n\n400 if name, chunk_size, or chunk_overlap is invalid. 409 if a knowledge base with this name already exists. 502 if vector store provisioning fails (the knowledge base is not created). 201 on success.",
        "operationId": "create-knowledge-base",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "created": true,
                  "knowledge_base": {
                    "id": "66666666-7777-8888-9999-000000000000",
                    "name": "Product FAQ",
                    "description": "Frequently asked questions about our product",
                    "chunk_size": 1000,
                    "chunk_overlap": 200,
                    "embedding_model": null,
                    "is_enabled": true,
                    "document_count": 0,
                    "vector_count": 0,
                    "created_at": "2026-06-12T10:00:00.000Z",
                    "updated_at": "2026-06-12T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Knowledge base name; required (400 if empty). Must be unique (409 if a knowledge base with this name already exists)."
                  },
                  "description": {
                    "type": "string",
                    "description": "Optional description; null if omitted."
                  },
                  "chunk_size": {
                    "type": "integer",
                    "description": "Chunk size used for processing; must be a positive integer if provided (400 otherwise). Defaults to 1000."
                  },
                  "chunk_overlap": {
                    "type": "integer",
                    "description": "Chunk overlap; must be a non-negative integer if provided (400 otherwise). Defaults to 200."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Product FAQ",
                "description": "Frequently asked questions about our product",
                "chunk_size": 1000,
                "chunk_overlap": 200
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/knowledge-bases/{id}": {
      "delete": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Delete a knowledge base",
        "description": "Delete a knowledge base, including its documents and text entries.\n\nVector store teardown is best-effort; vector_store_deleted reports whether the collection was removed. 400 if the id is invalid. 404 if the knowledge base does not exist. 200 on success.",
        "operationId": "delete-knowledge-base",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The knowledge base id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "knowledge_base_id": "66666666-7777-8888-9999-000000000000",
                  "vector_store_deleted": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/knowledge-bases/{id}/enable": {
      "post": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Enable a knowledge base",
        "description": "Enable a knowledge base so its agents can retrieve from it. Idempotent.\n\nNo request body required despite POST. Idempotent (enabling an already-enabled knowledge base still succeeds). 400 if the id is invalid. 404 if the knowledge base does not exist. 200 on success.",
        "operationId": "enable-knowledge-base",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The knowledge base id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "knowledge_base_id": "66666666-7777-8888-9999-000000000000",
                  "is_enabled": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/knowledge-bases/{id}/disable": {
      "post": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Disable a knowledge base",
        "description": "Disable a knowledge base so its agents stop retrieving from it. The knowledge base and its data are kept. Idempotent.\n\nNo request body required despite POST. Idempotent (disabling an already-disabled knowledge base still succeeds). 400 if the id is invalid. 404 if the knowledge base does not exist. 200 on success.",
        "operationId": "disable-knowledge-base",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The knowledge base id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "knowledge_base_id": "66666666-7777-8888-9999-000000000000",
                  "is_enabled": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/knowledge-bases/{id}/text": {
      "post": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Add a text source",
        "description": "Add a plain-text source to a knowledge base. The entry is queued and embedded asynchronously, not inline.\n\nThe entry is queued, not embedded inline (vector_count starts at 0, status 'queued'). 400 if the id is invalid, title or content is missing, or entry_type is invalid. 404 if the knowledge base does not exist. 201 on success.",
        "operationId": "add-kb-text",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The knowledge base id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "added": true,
                  "knowledge_base_id": "66666666-7777-8888-9999-000000000000",
                  "text_entry": {
                    "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
                    "title": "Refund Policy",
                    "entry_type": "text",
                    "status": "queued",
                    "vector_count": 0,
                    "created_at": "2026-06-12T10:00:00.000Z",
                    "updated_at": "2026-06-12T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Title for the text entry; required (400 if empty)."
                  },
                  "content": {
                    "type": "string",
                    "description": "The text content to add; required (400 if empty)."
                  },
                  "entry_type": {
                    "type": "string",
                    "description": "Plain-text source type; must be 'text' or 'snippet' (case-insensitive) if provided (400 otherwise). Defaults to 'text'. For FAQ content, use the /faq endpoint."
                  }
                },
                "required": [
                  "title",
                  "content"
                ]
              },
              "example": {
                "title": "Refund Policy",
                "content": "Customers may request a refund within 30 days of purchase...",
                "entry_type": "text"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/knowledge-bases/{id}/faq": {
      "post": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Add an FAQ source",
        "description": "Add an FAQ source to a knowledge base. The faq_pairs are combined into one entry, queued, and embedded asynchronously.\n\nThe entry is queued, not embedded inline (vector_count starts at 0, status 'queued'). 400 if the id is invalid, title is missing, faq_pairs is not a non-empty array, or any pair is missing question or answer. 404 if the knowledge base does not exist. 201 on success.",
        "operationId": "add-kb-faq",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The knowledge base id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "added": true,
                  "knowledge_base_id": "66666666-7777-8888-9999-000000000000",
                  "faq_count": 2,
                  "text_entry": {
                    "id": "aaaaaaaa-bbbb-cccc-dddd-eeeeeeeeeeee",
                    "title": "Shipping FAQ",
                    "entry_type": "faq",
                    "status": "queued",
                    "vector_count": 0,
                    "created_at": "2026-06-12T10:00:00.000Z",
                    "updated_at": "2026-06-12T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Title for the FAQ entry; required (400 if empty)."
                  },
                  "faq_pairs": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Non-empty array of FAQ pairs (minItems 1). Each pair is an object requiring both 'question' (string) and 'answer' (string) — 400 if any pair is missing either. The pairs are combined into one entry."
                  }
                },
                "required": [
                  "title",
                  "faq_pairs"
                ]
              },
              "example": {
                "title": "Shipping FAQ",
                "faq_pairs": [
                  {
                    "question": "How long does shipping take?",
                    "answer": "Standard shipping takes 3-5 business days."
                  },
                  {
                    "question": "Do you ship internationally?",
                    "answer": "Yes, we ship to over 50 countries."
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/knowledge-bases/{id}/url": {
      "post": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Add a URL source",
        "description": "Add a URL source to a knowledge base. The page is fetched, chunked, and embedded asynchronously, not inline.\n\nThe source is queued, not scraped inline (vector_count and total_chunks start at 0, status 'queued'). 400 if the id is invalid, url is missing, or url is malformed. 404 if the knowledge base does not exist. 201 on success.",
        "operationId": "add-kb-url",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The knowledge base id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "added": true,
                  "knowledge_base_id": "66666666-7777-8888-9999-000000000000",
                  "document": {
                    "id": "cccccccc-dddd-eeee-ffff-000000000000",
                    "title": "Getting Started Guide",
                    "source_url": "https://example.com/docs/getting-started",
                    "file_type": "url",
                    "status": "queued",
                    "total_chunks": 0,
                    "vector_count": 0,
                    "created_at": "2026-06-12T10:00:00.000Z",
                    "updated_at": "2026-06-12T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "The URL to scrape; required (400 if empty) and must be a well-formed, parseable URL (400 'invalid URL format' otherwise)."
                  },
                  "title": {
                    "type": "string",
                    "description": "Optional label for the source; falls back to the URL when omitted."
                  }
                },
                "required": [
                  "url"
                ]
              },
              "example": {
                "url": "https://example.com/docs/getting-started",
                "title": "Getting Started Guide"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/knowledge-bases/{id}/file": {
      "post": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Add a file source",
        "description": "Inbound webhook — add a file source to a knowledge base (multipart).\n\ntxt, csv, docx, and pdf files are extracted and embedded inline (processed=true, status 'completed'); xlsx files are queued (processed=false, status 'queued') and embedded asynchronously. Errors: 400 (invalid knowledge base id, not multipart, no `file` field, unsupported file type — allowed.pdf.docx.xlsx.txt.csv, or could not read file), 401 (unauthenticated), 404 (knowledge base not found), 500 (insert error), 502 (file uploaded but inline processing failed — includes document_id and marks the source failed). 201 on success.",
        "operationId": "add-kb-file",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The knowledge base id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "added": true,
                  "processed": true,
                  "knowledge_base_id": "<knowledge_base_id>",
                  "document": {
                    "id": "a1b2c3d4-0000-4000-8000-000000000001",
                    "title": "company-handbook.pdf",
                    "filename": "company-handbook.pdf",
                    "file_type": "pdf",
                    "file_size_bytes": 482190,
                    "status": "completed",
                    "total_chunks": 42,
                    "vector_count": 42,
                    "created_at": "2026-06-12T10:15:00.000Z",
                    "updated_at": "2026-06-12T10:15:08.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "multipart/form-data": {
              "schema": {
                "type": "object",
                "properties": {
                  "file": {
                    "type": "string",
                    "format": "binary",
                    "description": "The file to add, sent as a multipart/form-data part under the field name `file`. Allowed extensions:.pdf.docx.xlsx.txt.csv. txt, csv, docx, and pdf are extracted and embedded inline; xlsx is queued and embedded asynchronously."
                  }
                },
                "required": [
                  "file"
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/knowledge-bases/{id}/sources/{sourceId}": {
      "delete": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Delete a source",
        "description": "Inbound webhook — delete a source from a knowledge base.\n\nRemoves the source's vectors from the vector store (best-effort), deletes any associated file, and removes the source. source_kind is 'document' or 'text_entry'. Errors: 400 (invalid knowledge base id or invalid source id), 401 (unauthenticated), 404 (knowledge base not found, or source not found in this knowledge base), 500 (delete error). 200 on success.",
        "operationId": "delete-kb-source",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The knowledge base id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "sourceId",
            "in": "path",
            "required": true,
            "description": "The source id to delete — either a file/URL source or a text/FAQ source belonging to this knowledge base; you need not know which kind.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "knowledge_base_id": "<knowledge_base_id>",
                  "source_id": "<source_id>",
                  "source_kind": "document",
                  "vectors_removed": 42
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/agents/{id}/knowledge-base": {
      "post": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Assign to an agent",
        "description": "Assign (connect) a knowledge base to an agent, enforcing the one-knowledge-base-per-agent policy.\n\nOne knowledge base per agent: any existing knowledge base connection for the agent is replaced. 201 on success. 400 for an invalid agent id, invalid knowledge_base_id, or bad max_results/similarity_threshold. 404 if the agent or the knowledge base does not exist.",
        "operationId": "assign-kb-to-agent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "assigned": true,
                  "connection_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "agent_id": "11111111-2222-3333-4444-555555555555",
                  "knowledge_base_id": "66666666-7777-8888-9999-000000000000",
                  "max_results": 5,
                  "similarity_threshold": 0.3,
                  "enabled": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "knowledge_base_id": {
                    "type": "string",
                    "description": "UUID of the knowledge base to assign; must be a valid UUID and reference an existing knowledge base (404 otherwise)."
                  },
                  "max_results": {
                    "type": "integer",
                    "description": "Maximum number of retrieval results. Must be a positive integer if provided (400 otherwise). Defaults to 5."
                  },
                  "similarity_threshold": {
                    "type": "number",
                    "description": "Retrieval similarity cutoff. Must be a number between 0 and 1 inclusive if provided (400 otherwise). Defaults to 0.30."
                  },
                  "enabled": {
                    "type": "boolean",
                    "description": "Whether the connection is enabled. Defaults to true."
                  }
                },
                "required": [
                  "knowledge_base_id"
                ]
              },
              "example": {
                "knowledge_base_id": "<knowledge_base_id>",
                "max_results": 5,
                "similarity_threshold": 0.3,
                "enabled": true
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agents/{id}/knowledge-base/{kbId}": {
      "delete": {
        "tags": [
          "Knowledge bases"
        ],
        "summary": "Remove from an agent",
        "description": "Remove (disconnect) a knowledge base from an agent. The knowledge base and its data are untouched; only the link is deleted.\n\n200 on success. 400 if the agent id or knowledge base id is not a valid UUID. 404 if the agent does not exist, or if the knowledge base is not connected to the agent.",
        "operationId": "remove-kb-from-agent",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "kbId",
            "in": "path",
            "required": true,
            "description": "The knowledge base id to disconnect from the agent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "removed": true,
                  "agent_id": "11111111-2222-3333-4444-555555555555",
                  "knowledge_base_id": "66666666-7777-8888-9999-000000000000"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/appointments": {
      "get": {
        "tags": [
          "Appointments"
        ],
        "summary": "List appointments (date range)",
        "description": "Inbound webhook — list appointments in a date range.\n\nLists appointments whose appointment_date falls within [from, to], ordered by appointment_date ascending. Both from and to are required. Each appointment includes its linked contact (null if none). The normalized from/to values are echoed in the response. 400 if from or to is missing or invalid, from is after to, status is invalid, or limit is invalid. 500 on an internal error. Undeclared query params are ignored.",
        "operationId": "list-appointments",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": true,
            "description": "Start of the range, ISO 8601. Include a UTC offset/Z or pass `timezone`. Must be on or before `to`. Invalid → 400.",
            "schema": {
              "type": "string",
              "default": 0
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "description": "End of the range, ISO 8601. A date-only `to` (e.g. 2026-06-20, no time component) is treated as inclusive of the whole day (extended to 23:59:59.999). Invalid → 400.",
            "schema": {
              "type": "string",
              "default": 0
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "description": "IANA timezone used to interpret bare (offset-less) from/to values.",
            "schema": {
              "type": "string",
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Optional filter; one of: scheduled, confirmed, completed, cancelled, no_show (case-insensitive). Other values → 400.",
            "schema": {
              "type": "string",
              "default": 0
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum rows to return; positive integer, capped at 500. Non-integer or <= 0 → 400.",
            "schema": {
              "type": "string",
              "default": 100
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "from": "2026-06-20T00:00:00.000Z",
                  "to": "2026-06-20T23:59:59.999Z",
                  "appointment_count": 1,
                  "appointments": [
                    {
                      "id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
                      "title": "Discovery Call",
                      "description": "Intro call to discuss requirements",
                      "appointment_date": "2026-06-20T15:00:00.000Z",
                      "duration_minutes": 30,
                      "location": "Zoom",
                      "notes": null,
                      "status": "scheduled",
                      "contact_id": "f1e2d3c4-b5a6-4978-8c9d-0e1f2a3b4c5d",
                      "contact": {
                        "id": "f1e2d3c4-b5a6-4978-8c9d-0e1f2a3b4c5d",
                        "name": "Jane Doe",
                        "email": "jane@example.com",
                        "phone": "+15551234567"
                      },
                      "calendar_id": "d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f80",
                      "created_at": "2026-06-12T09:00:00.000Z",
                      "updated_at": "2026-06-12T09:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Appointments"
        ],
        "summary": "Create an appointment",
        "description": "Inbound webhook — create an appointment.\n\nThe contact is resolved by contact_id, otherwise by email/phone (a minimal contact is created if none matches, since an appointment requires a contact). 400 for a missing title or appointment_date, a bad date, an invalid duration or status, or an invalid/missing calendar_id. 404 if a given contact_id does not exist. 500 on failure. 409 when check_availability is true and the requested time overlaps an existing non-cancelled appointment on the calendar; the response includes a conflicting array (id, title, appointment_date, duration_minutes), and nothing is created, not even a new contact.",
        "operationId": "create-appointment",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "created": true,
                  "appointment_id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
                  "contact_id": "f1e2d3c4-b5a6-4978-8c9d-0e1f2a3b4c5d"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "Appointment title; required and must be non-empty."
                  },
                  "appointment_date": {
                    "type": "string",
                    "description": "Start date/time as ISO 8601. Must include a UTC offset/Z, or pass `timezone` to interpret a bare local value; otherwise 400."
                  },
                  "calendar_id": {
                    "type": "string",
                    "description": "UUID of an existing calendar. 400 if missing, invalid, or not found."
                  },
                  "contact_id": {
                    "type": "string",
                    "description": "UUID of an existing contact. The contact is resolved by contact_id first; if absent it is resolved by email/phone (created if missing). Must be a valid UUID and exist, or 404."
                  },
                  "email": {
                    "type": "string",
                    "description": "Used to find or create the contact when contact_id is not given. At least one of contact_id, email, or phone must resolve or create a contact."
                  },
                  "phone": {
                    "type": "string",
                    "description": "Used to find or create the contact when contact_id is not given (normalized). At least one of contact_id, email, or phone must resolve or create a contact."
                  },
                  "name": {
                    "type": "string",
                    "description": "Contact display name used if a new contact must be created; falls back to first_name + last_name."
                  },
                  "first_name": {
                    "type": "string",
                    "description": "Used to build the contact name if `name` is not provided."
                  },
                  "last_name": {
                    "type": "string",
                    "description": "Used to build the contact name if `name` is not provided."
                  },
                  "description": {
                    "type": "string",
                    "description": "Optional appointment description."
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA timezone used to interpret a bare (offset-less) appointment_date, e.g. \"America/New_York\"."
                  },
                  "duration_minutes": {
                    "type": "integer",
                    "description": "Positive integer; defaults to 60. Non-integer or <= 0 → 400."
                  },
                  "location": {
                    "type": "string",
                    "description": "Optional location text."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Optional notes text."
                  },
                  "status": {
                    "type": "string",
                    "description": "One of: scheduled, confirmed, completed, cancelled, no_show (case-insensitive). Defaults to 'scheduled'. Other values → 400."
                  },
                  "check_availability": {
                    "type": "boolean",
                    "description": "When true, the booking is refused if it overlaps a non-cancelled appointment on the same calendar (existing appointments without a duration count as 60 minutes). Defaults to false (no check)."
                  }
                },
                "required": [
                  "title",
                  "appointment_date",
                  "calendar_id"
                ]
              },
              "example": {
                "title": "Discovery Call",
                "appointment_date": "2026-06-20T15:00:00Z",
                "calendar_id": "<calendar_id>",
                "contact_id": "<contact_id>",
                "duration_minutes": 30,
                "location": "Zoom",
                "notes": "Intro call to discuss requirements",
                "status": "scheduled",
                "timezone": "America/New_York"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/appointments/{id}": {
      "get": {
        "tags": [
          "Appointments"
        ],
        "summary": "Get an appointment",
        "description": "Inbound webhook — get a single appointment.\n\nReturns the appointment's core fields plus its linked contact (id/name/email/phone; `contact` is null if none). 400 for an invalid id. 404 if the appointment does not exist. 500 on an internal error.",
        "operationId": "get-appointment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The appointment id (as returned from create).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "appointment": {
                    "id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f",
                    "title": "Discovery Call",
                    "description": "Intro call to discuss requirements",
                    "appointment_date": "2026-06-20T15:00:00.000Z",
                    "duration_minutes": 30,
                    "location": "Zoom",
                    "notes": null,
                    "status": "scheduled",
                    "contact_id": "f1e2d3c4-b5a6-4978-8c9d-0e1f2a3b4c5d",
                    "contact": {
                      "id": "f1e2d3c4-b5a6-4978-8c9d-0e1f2a3b4c5d",
                      "name": "Jane Doe",
                      "email": "jane@example.com",
                      "phone": "+15551234567"
                    },
                    "calendar_id": "d4e5f6a7-b8c9-4d0e-1f2a-3b4c5d6e7f80",
                    "created_at": "2026-06-12T09:00:00.000Z",
                    "updated_at": "2026-06-12T09:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Appointments"
        ],
        "summary": "Update an appointment",
        "description": "Inbound webhook — update an appointment.\n\nOnly fields present in the body change; an empty effective update returns 400 'no updatable fields provided'. 400 for an invalid appointment id, a bad date, an invalid duration, status, or calendar_id. 404 if the appointment or a referenced contact does not exist. 500 on failure.",
        "operationId": "update-appointment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The appointment id (as returned from create).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "updated": true,
                  "appointment_id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "title": {
                    "type": "string",
                    "description": "New title; required to be non-empty, so it is only applied when a non-empty value is given (empty is ignored)."
                  },
                  "description": {
                    "type": "string",
                    "description": "Nullable text; presence of the key sets it (empty string clears it)."
                  },
                  "appointment_date": {
                    "type": "string",
                    "description": "New start as ISO 8601; if provided it must parse (include UTC offset/Z or pass `timezone`), else 400. A present-but-empty value is ignored."
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA timezone used to interpret a bare appointment_date."
                  },
                  "duration_minutes": {
                    "type": "integer",
                    "description": "Positive integer; non-integer or <= 0 → 400."
                  },
                  "location": {
                    "type": "string",
                    "description": "Nullable text; presence of the key sets it (empty clears)."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Nullable text; presence of the key sets it (empty clears)."
                  },
                  "status": {
                    "type": "string",
                    "description": "One of: scheduled, confirmed, completed, cancelled, no_show (case-insensitive). Other values → 400."
                  },
                  "calendar_id": {
                    "type": "string",
                    "description": "Reassign to another calendar; must be a valid UUID and reference an existing calendar, else 400."
                  },
                  "contact_id": {
                    "type": "string",
                    "description": "Reassign to another contact; must be a valid UUID and reference an existing contact, else 400/404."
                  }
                }
              },
              "example": {
                "title": "Discovery Call (rescheduled)",
                "appointment_date": "2026-06-21T16:00:00Z",
                "duration_minutes": 45,
                "status": "confirmed",
                "notes": "Pushed back one day"
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Appointments"
        ],
        "summary": "Delete an appointment",
        "description": "Inbound webhook — delete an appointment.\n\nPermanently deletes the appointment. 400 for an invalid id. 404 if the appointment does not exist. 409 if it has related records and cannot be deleted. 500 on failure.",
        "operationId": "delete-appointment",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The appointment id to delete.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "appointment_id": "c3d4e5f6-a7b8-4c9d-0e1f-2a3b4c5d6e7f"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/messages": {
      "post": {
        "tags": [
          "Messages"
        ],
        "summary": "Send a message to a contact",
        "description": "Inbound webhook — send a caller-supplied message to a contact on a channel. Channels: email, sms, whatsapp, facebook, instagram.\n\nThe recipient is always derived from the contact — no raw address can be passed. The message is attributed as a human-sent message. facebook/instagram require an existing conversation (no cold outbound) — 400 otherwise. 400: bad channel, invalid contact_id, missing subject for email, missing message/html for email, missing message on other channels, contact missing the needed email/phone/WhatsApp address, invalid from_number, or fb/ig cold-send. 401: missing or invalid key. 402: insufficient wallet balance. 404: contact not found. 502: send failed.",
        "operationId": "send-message",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "channel": "sms",
                  "contact_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "message_id": "SM0f3a2b1c9d8e7f6a5b4c3d2e1f0a9b8c"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channel": {
                    "type": "string",
                    "description": "The delivery channel. One of: email, sms, whatsapp, facebook, instagram (case-insensitive). 400 otherwise."
                  },
                  "contact_id": {
                    "type": "string",
                    "description": "UUID of the contact to message. Must be a valid UUID (400) and exist (404). The recipient address/number is always derived from this contact — you cannot pass a raw address."
                  },
                  "message": {
                    "type": "string",
                    "description": "The message body text. Required for sms, whatsapp, facebook, instagram. For email it is optional, but either message or html must be provided (sent as the email's text part when html is absent)."
                  },
                  "subject": {
                    "type": "string",
                    "description": "Email only. Required when channel='email' (400 'subject is required for email' otherwise). Ignored on other channels."
                  },
                  "html": {
                    "type": "string",
                    "description": "Email only. HTML body; takes precedence over message. For email, message or html is required."
                  },
                  "reply_to": {
                    "type": "string",
                    "description": "Email only. Optional Reply-To address."
                  },
                  "from_name": {
                    "type": "string",
                    "description": "Email only. Optional sender display name."
                  },
                  "from_number": {
                    "type": "string",
                    "description": "SMS only. Optional E.164 from-number override (e.g. +14155550123). 400 if provided but not valid E.164."
                  },
                  "attachment_urls": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Optional array of attachment URLs. Used by sms (MMS), whatsapp (sent as media), and facebook/instagram. Ignored where not applicable."
                  }
                },
                "required": [
                  "channel",
                  "contact_id"
                ]
              },
              "example": {
                "channel": "sms",
                "contact_id": "<contact_id>",
                "message": "Hi! Your appointment is confirmed for tomorrow at 10am.",
                "from_number": "+14155550123",
                "attachment_urls": [
                  "https://cdn.example.com/files/flyer.pdf"
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/messages/ai": {
      "post": {
        "tags": [
          "Messages"
        ],
        "summary": "Send an AI message to a contact",
        "description": "Inbound webhook — send an AI-generated message to a contact on a channel. The agent writes the text, then it is delivered.\n\nSame delivery path as POST /messages, but the body text is generated by the agent (using its prompt, tools, knowledge base, and the contact's conversation history) and then sanitized. There is no raw 'message' field — the text is generated. The message is attributed to the agent. The success response (and most error responses) include generated_message. facebook/instagram require an existing conversation (no cold send), else 400. 400: bad channel, invalid contact_id, invalid agent_id, missing subject for email, invalid from_number, contact missing the needed address, or fb/ig cold-send. 401: missing or invalid key. 402: insufficient wallet balance (returns generated_message). 404: contact or agent not found. 502: generation failed, empty generated message, or send failed.",
        "operationId": "send-ai-message",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "channel": "whatsapp",
                  "contact_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
                  "agent_id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
                  "generated_message": "Thanks so much for your order! It ships tomorrow — we'll send tracking once it's on the way.",
                  "message_id": "wamid.HBgMxxxxxxxxxxxxxxxx"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channel": {
                    "type": "string",
                    "description": "The delivery channel. One of: email, sms, whatsapp, facebook, instagram (case-insensitive). 400 otherwise."
                  },
                  "contact_id": {
                    "type": "string",
                    "description": "UUID of the contact to message. Must be a valid UUID (400) and exist (404). The recipient is always derived from this contact."
                  },
                  "agent_id": {
                    "type": "string",
                    "description": "UUID of the AI agent that generates and is attributed the message. Must be a valid UUID (400) and exist (404)."
                  },
                  "prompt": {
                    "type": "string",
                    "description": "Optional instruction steering the generation (what to say). Omit it and the agent simply replies to the contact's conversation so far, using its prompt, tools, and knowledge base."
                  },
                  "subject": {
                    "type": "string",
                    "description": "Email only. Required when channel='email' (400 'subject is required for email' otherwise). Ignored on other channels."
                  },
                  "reply_to": {
                    "type": "string",
                    "description": "Email only. Optional Reply-To address."
                  },
                  "from_name": {
                    "type": "string",
                    "description": "Email only. Optional sender display name."
                  },
                  "from_number": {
                    "type": "string",
                    "description": "SMS only. Optional E.164 from-number override (e.g. +14155550123). 400 if provided but not valid E.164."
                  }
                },
                "required": [
                  "channel",
                  "contact_id",
                  "agent_id"
                ]
              },
              "example": {
                "channel": "whatsapp",
                "contact_id": "<contact_id>",
                "agent_id": "<agent_id>",
                "prompt": "Thank them for their order and let them know it ships tomorrow."
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/messages": {
      "get": {
        "tags": [
          "Messages"
        ],
        "summary": "Fetch a contact's messages",
        "description": "Inbound webhook — fetch a contact's messages. Returns the most recent messages across every channel and conversation for the contact, in chronological order (oldest → newest).\n\nRead-only. 401 if unauthenticated; 400 for an invalid contact id; 404 if the contact does not exist; When the contact has no conversations, returns message_count 0 and an empty messages array. direction is 'inbound' for messages from the contact, otherwise 'outbound'.",
        "operationId": "fetch-messages",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the contact id. A non-uuid returns 400; a contact that does not exist returns 404.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "max number of most-recent messages to return; min 1, max 500 (values above 500 are clamped to 500). Defaults to 100 when omitted or invalid.",
            "schema": {
              "type": "integer",
              "default": 100
            }
          },
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "optional filter by channel (e.g. sms, whatsapp, facebook, instagram). Case-insensitive.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "7f1c9e2a-3b4d-4e5f-8a90-1b2c3d4e5f60",
                  "message_count": 2,
                  "messages": [
                    {
                      "id": "a1b2c3d4-0001-4abc-9def-000000000001",
                      "channel": "sms",
                      "direction": "inbound",
                      "sender_type": "contact",
                      "content": "Hi, is anyone available?",
                      "attachment_urls": [],
                      "timestamp": "2026-06-12T14:30:00.000Z"
                    },
                    {
                      "id": "a1b2c3d4-0002-4abc-9def-000000000002",
                      "channel": "sms",
                      "direction": "outbound",
                      "sender_type": "ai",
                      "content": "Hi! Yes, how can I help you today?",
                      "attachment_urls": [],
                      "timestamp": "2026-06-12T14:30:45.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/conversations": {
      "get": {
        "tags": [
          "Conversations"
        ],
        "summary": "List conversations",
        "description": "Inbound webhook — list conversations. The same set the inbox shows, newest first by updated_at, with offset-based pagination.\n\nRead-only. total is the exact count for the applied filters; has_more is true when offset + returned count < total. 401 if unauthenticated.",
        "operationId": "list-conversations",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "page size; min 1, max 100 (values above 100 are clamped to 100). Defaults to 50 when omitted or invalid.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "number of rows to skip for pagination; min 0. Defaults to 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "optional filter by channel (e.g. sms, whatsapp, facebook, instagram). Case-insensitive.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "optional filter by conversation status. Case-insensitive.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 128,
                  "limit": 50,
                  "offset": 0,
                  "has_more": true,
                  "conversations": [
                    {
                      "id": "c0a1b2c3-1111-4abc-9def-000000000010",
                      "contact_id": "7f1c9e2a-3b4d-4e5f-8a90-1b2c3d4e5f60",
                      "contact_name": "Jane Doe",
                      "contact_email": "jane@example.com",
                      "contact_phone": "+14155550123",
                      "channel": "sms",
                      "status": "open",
                      "last_message": "Thanks, see you then!",
                      "last_message_at": "2026-06-12T13:05:00.000Z",
                      "unread": false,
                      "created_at": "2026-06-10T09:00:00.000Z",
                      "updated_at": "2026-06-12T13:05:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/conversations/{id}": {
      "get": {
        "tags": [
          "Conversations"
        ],
        "summary": "Get a conversation",
        "description": "Inbound webhook — get a single conversation. Returns the same shape as a list row.\n\nRead-only. 401 if unauthenticated; 400 for an invalid conversation id; 404 if the conversation does not exist.",
        "operationId": "get-conversation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the conversation id. A non-uuid returns 400; a conversation that does not exist returns 404.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "conversation": {
                    "id": "c0a1b2c3-1111-4abc-9def-000000000010",
                    "contact_id": "7f1c9e2a-3b4d-4e5f-8a90-1b2c3d4e5f60",
                    "contact_name": "Jane Doe",
                    "contact_email": "jane@example.com",
                    "contact_phone": "+14155550123",
                    "channel": "sms",
                    "status": "open",
                    "last_message": "Thanks, see you then!",
                    "last_message_at": "2026-06-12T13:05:00.000Z",
                    "unread": false,
                    "created_at": "2026-06-10T09:00:00.000Z",
                    "updated_at": "2026-06-12T13:05:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/conversations/{id}/messages": {
      "get": {
        "tags": [
          "Conversations"
        ],
        "summary": "List messages in a conversation",
        "description": "Inbound webhook — list a conversation's messages. Returns the most recent messages for the conversation, oldest → newest.\n\nRead-only. 401 if unauthenticated; 400 for an invalid conversation id; 404 if the conversation does not exist; direction is 'inbound' for messages from the contact, otherwise 'outbound'.",
        "operationId": "conversation-messages",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the conversation id. A non-uuid returns 400; a conversation that does not exist returns 404.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "max number of most-recent messages to return; min 1, max 500 (values above 500 are clamped to 500). Defaults to 100 when omitted or invalid.",
            "schema": {
              "type": "integer",
              "default": 100
            }
          },
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "optional filter by channel (e.g. sms, whatsapp). Case-insensitive.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "conversation_id": "c0a1b2c3-1111-4abc-9def-000000000010",
                  "message_count": 2,
                  "messages": [
                    {
                      "id": "a1b2c3d4-0001-4abc-9def-000000000001",
                      "channel": "sms",
                      "direction": "inbound",
                      "sender_type": "contact",
                      "content": "Hi, is anyone available?",
                      "attachment_urls": [],
                      "timestamp": "2026-06-12T14:30:00.000Z"
                    },
                    {
                      "id": "a1b2c3d4-0002-4abc-9def-000000000002",
                      "channel": "sms",
                      "direction": "outbound",
                      "sender_type": "ai",
                      "content": "Hi! Yes, how can I help you today?",
                      "attachment_urls": [],
                      "timestamp": "2026-06-12T14:30:45.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/calls": {
      "get": {
        "tags": [
          "Calls"
        ],
        "summary": "List call history",
        "description": "Inbound webhook — list call history. The same records the Call History page shows, newest first by started_at, with offset-based pagination and resolved agent and contact names.\n\nRead-only. total is the exact count for the applied filters; has_more is true when offset + returned count < total. agent_name and contact_name are resolved where available (null if not found). 401 if unauthenticated; 400 for an invalid from/to, an invalid direction, or a non-uuid agent_id.",
        "operationId": "list-calls",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "page size; min 1, max 100 (values above 100 are clamped to 100). Defaults to 50 when omitted or invalid.",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "number of rows to skip for pagination; min 0. Defaults to 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "optional lower bound on started_at; ISO 8601 (include a UTC offset or Z). An invalid value returns 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "optional upper bound on started_at; ISO 8601 (include a UTC offset or Z). A date-only value (no time) covers the whole day (through 23:59:59.999). An invalid value returns 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "optional filter by call status (e.g. completed, no_answer, failed). Case-insensitive.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "direction",
            "in": "query",
            "required": false,
            "description": "optional filter by call direction; must be 'inbound' or 'outbound' (case-insensitive). Any other value returns 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "agent_id",
            "in": "query",
            "required": false,
            "description": "optional filter by AI agent id; must be a valid uuid or returns 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 342,
                  "limit": 50,
                  "offset": 0,
                  "has_more": true,
                  "calls": [
                    {
                      "id": "d4e5f6a7-2222-4abc-9def-000000000020",
                      "call_sid": "CA0123456789abcdef0123456789abcdef",
                      "direction": "inbound",
                      "status": "completed",
                      "from": "+14155550123",
                      "to": "+18005551234",
                      "duration_seconds": 142,
                      "has_recording": true,
                      "agent_id": "b2c3d4e5-3333-4abc-9def-000000000030",
                      "agent_name": "Front Desk AI",
                      "contact_id": "7f1c9e2a-3b4d-4e5f-8a90-1b2c3d4e5f60",
                      "contact_name": "Jane Doe",
                      "ended_reason": "completed",
                      "started_at": "2026-06-12T15:00:00.000Z",
                      "ended_at": "2026-06-12T15:02:22.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/calls/stats": {
      "get": {
        "tags": [
          "Calls"
        ],
        "summary": "Call stats for the sub-account",
        "description": "Inbound webhook — call stats. Aggregate call metrics over an optional date range (default: the last 30 days): totals by direction and status, completed vs missed, calls with recordings, and total and average talk time.\n\nRead-only. Aggregated over up to 5000 most-recent calls in the window; if that cap is hit, capped is true (narrow the range for exact figures on high-volume accounts). missed_calls counts the statuses no_answer, no-answer, and failed. average_duration_seconds is the rounded mean over calls with a positive duration. 'to' is null in the response when not supplied. 401 if unauthenticated; 400 for an invalid from/to.",
        "operationId": "call-stats",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "optional window start; ISO 8601 (include a UTC offset or Z). When omitted, defaults to 30 days ago. An invalid value returns 400.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "optional window end; ISO 8601 (include a UTC offset or Z). A date-only value (no time) covers the whole day (through 23:59:59.999). When omitted, the window runs to now. An invalid value returns 400.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "from": "2026-05-13T16:00:00.000Z",
                  "to": "2026-06-12T23:59:59.999Z",
                  "capped": false,
                  "stats": {
                    "total_calls": 342,
                    "inbound_calls": 210,
                    "outbound_calls": 132,
                    "completed_calls": 298,
                    "missed_calls": 31,
                    "calls_with_recordings": 275,
                    "total_duration_seconds": 41280,
                    "average_duration_seconds": 138,
                    "by_status": {
                      "completed": 298,
                      "no_answer": 22,
                      "failed": 9,
                      "busy": 13
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/calls/{id}": {
      "get": {
        "tags": [
          "Calls"
        ],
        "summary": "Get a call (rich detail)",
        "description": "Inbound webhook — get a single call with full detail. Extends the list shape with the heavy fields: transcript, AI summary, and recording URL or number used.\n\nRead-only. Returns the full list-shape fields plus transcript, ai_summary, number_used, created_at, and updated_at. 401 if unauthenticated; 400 for an invalid call id; 404 if the call does not exist.",
        "operationId": "get-call",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "the call id. A non-uuid returns 400; a call that does not exist returns 404.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "call": {
                    "id": "d4e5f6a7-2222-4abc-9def-000000000020",
                    "call_sid": "CA0123456789abcdef0123456789abcdef",
                    "direction": "inbound",
                    "status": "completed",
                    "from": "+14155550123",
                    "to": "+18005551234",
                    "duration_seconds": 142,
                    "has_recording": true,
                    "agent_id": "b2c3d4e5-3333-4abc-9def-000000000030",
                    "agent_name": "Front Desk AI",
                    "contact_id": "7f1c9e2a-3b4d-4e5f-8a90-1b2c3d4e5f60",
                    "contact_name": "Jane Doe",
                    "ended_reason": "completed",
                    "started_at": "2026-06-12T15:00:00.000Z",
                    "ended_at": "2026-06-12T15:02:22.000Z",
                    "transcript": "Agent: Thanks for calling, how can I help?\nCaller: I'd like to book an appointment...",
                    "ai_summary": "Caller requested to book an appointment for next Tuesday; agent confirmed availability.",
                    "number_used": "+18005551234",
                    "created_at": "2026-06-12T15:00:00.000Z",
                    "updated_at": "2026-06-12T15:02:30.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/pipelines": {
      "get": {
        "tags": [
          "Pipelines"
        ],
        "summary": "List pipelines",
        "description": "List the sub-account's pipelines, oldest first, each with its stages ordered by position.\n\nStage positions are 0-based and contiguous. 401 if unauthenticated.",
        "operationId": "list-pipelines",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "pipelines": [
                    {
                      "id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                      "name": "Sales",
                      "created_at": "2026-10-01T10:00:00.000Z",
                      "updated_at": "2026-10-01T10:00:00.000Z",
                      "stages": [
                        {
                          "id": "0b6f2c1e-3a4d-4e5f-8a9b-1c2d3e4f5a60",
                          "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                          "name": "New",
                          "position": 0,
                          "color": "#3B82F6",
                          "created_at": "2026-10-01T10:00:00.000Z",
                          "updated_at": "2026-10-01T10:00:00.000Z"
                        },
                        {
                          "id": "1c7a3d2f-4b5e-4f60-9a1b-2c3d4e5f6a71",
                          "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                          "name": "Won",
                          "position": 1,
                          "color": "#8B5CF6",
                          "created_at": "2026-10-01T10:00:00.000Z",
                          "updated_at": "2026-10-01T10:00:00.000Z"
                        }
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Pipelines"
        ],
        "summary": "Create a pipeline",
        "description": "Create a pipeline, optionally with its stages in display order. Stage colours are assigned automatically, the same way the app does.\n\nReturns 201. 400 if name is empty. If a stage can't be created the pipeline is not left behind.",
        "operationId": "create-pipeline",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "pipeline": {
                    "id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                    "name": "Sales",
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z",
                    "stages": [
                      {
                        "id": "0b6f2c1e-3a4d-4e5f-8a9b-1c2d3e4f5a60",
                        "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                        "name": "New",
                        "position": 0,
                        "color": "#3B82F6",
                        "created_at": "2026-10-01T10:00:00.000Z",
                        "updated_at": "2026-10-01T10:00:00.000Z"
                      },
                      {
                        "id": "1c7a3d2f-4b5e-4f60-9a1b-2c3d4e5f6a71",
                        "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                        "name": "Won",
                        "position": 1,
                        "color": "#8B5CF6",
                        "created_at": "2026-10-01T10:00:00.000Z",
                        "updated_at": "2026-10-01T10:00:00.000Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Pipeline name."
                  },
                  "stages": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Stage names in display order (max 50)."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Sales",
                "stages": [
                  "New",
                  "Won"
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/pipelines/{id}": {
      "get": {
        "tags": [
          "Pipelines"
        ],
        "summary": "Get a pipeline",
        "description": "Get one pipeline with its stages ordered by position.\n\n404 if the pipeline is not in this sub-account.",
        "operationId": "get-pipeline",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The pipeline id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "pipeline": {
                    "id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                    "name": "Sales",
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z",
                    "stages": [
                      {
                        "id": "0b6f2c1e-3a4d-4e5f-8a9b-1c2d3e4f5a60",
                        "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                        "name": "New",
                        "position": 0,
                        "color": "#3B82F6",
                        "created_at": "2026-10-01T10:00:00.000Z",
                        "updated_at": "2026-10-01T10:00:00.000Z"
                      },
                      {
                        "id": "1c7a3d2f-4b5e-4f60-9a1b-2c3d4e5f6a71",
                        "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                        "name": "Won",
                        "position": 1,
                        "color": "#8B5CF6",
                        "created_at": "2026-10-01T10:00:00.000Z",
                        "updated_at": "2026-10-01T10:00:00.000Z"
                      }
                    ]
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Pipelines"
        ],
        "summary": "Rename a pipeline",
        "description": "Rename a pipeline.\n\nThe response does not include stages. 400 if name is empty; 404 if the pipeline is not in this sub-account.",
        "operationId": "update-pipeline",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The pipeline id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "pipeline": {
                    "id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                    "name": "Enterprise sales",
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-04T08:15:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New pipeline name."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Enterprise sales"
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Pipelines"
        ],
        "summary": "Delete a pipeline",
        "description": "Delete a pipeline and its stages.\n\n409 with opportunity_count while the pipeline has opportunities and force is not true. 404 if the pipeline is not in this sub-account.",
        "operationId": "delete-pipeline",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The pipeline id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "Required when the pipeline still has opportunities — deleting the pipeline permanently deletes them too.",
            "schema": {
              "type": "boolean",
              "default": "false"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                  "opportunities_deleted": 0
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/pipelines/{id}/stages": {
      "post": {
        "tags": [
          "Pipelines"
        ],
        "summary": "Add a stage",
        "description": "Add a stage to a pipeline — appended to the end, or inserted at a position (later stages shift down).\n\nReturns 201. 404 if the pipeline is not in this sub-account.",
        "operationId": "create-pipeline-stage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The pipeline id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "stage": {
                    "id": "2d8b4e30-5c6f-4071-8b2c-3d4e5f6a7b82",
                    "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                    "name": "Demo booked",
                    "position": 1,
                    "color": "#8B5CF6",
                    "created_at": "2026-10-02T09:30:00.000Z",
                    "updated_at": "2026-10-02T09:30:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Stage name."
                  },
                  "position": {
                    "type": "integer",
                    "description": "0-based insert position. Defaults to the end."
                  },
                  "color": {
                    "type": "string",
                    "description": "Hex colour, e.g. #10B981. Defaults to the app's palette for that position."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Demo booked",
                "position": 1
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/pipelines/{id}/stages/{stageId}": {
      "patch": {
        "tags": [
          "Pipelines"
        ],
        "summary": "Update a stage",
        "description": "Rename, recolour or move a stage. Moving renumbers the other stages so positions stay contiguous.\n\nSend at least one field. 404 if the pipeline or stage is not in this sub-account.",
        "operationId": "update-pipeline-stage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The pipeline id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "stageId",
            "in": "path",
            "required": true,
            "description": "The stage id (must belong to the pipeline).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "stage": {
                    "id": "2d8b4e30-5c6f-4071-8b2c-3d4e5f6a7b82",
                    "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                    "name": "Demo booked",
                    "position": 1,
                    "color": "#8B5CF6",
                    "created_at": "2026-10-02T09:30:00.000Z",
                    "updated_at": "2026-10-02T09:30:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New stage name."
                  },
                  "position": {
                    "type": "integer",
                    "description": "Move the stage to this 0-based position."
                  },
                  "color": {
                    "type": "string",
                    "description": "Hex colour."
                  }
                }
              },
              "example": {
                "position": 0
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Pipelines"
        ],
        "summary": "Delete a stage",
        "description": "Delete a stage. Remaining stages are renumbered.\n\n409 with opportunity_count while the stage holds opportunities and move_to_stage_id is missing. 400 if move_to_stage_id is the same stage.",
        "operationId": "delete-pipeline-stage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The pipeline id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "stageId",
            "in": "path",
            "required": true,
            "description": "The stage id (must belong to the pipeline).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "move_to_stage_id",
            "in": "query",
            "required": false,
            "description": "Required when the stage still holds opportunities: they are moved to this stage (same pipeline) before the stage is deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "stage_id": "2d8b4e30-5c6f-4071-8b2c-3d4e5f6a7b82",
                  "opportunities_moved": 3
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/opportunities": {
      "get": {
        "tags": [
          "Opportunities"
        ],
        "summary": "List opportunities",
        "description": "List opportunities, newest first, with optional filters.\n\n400 if an id filter is not a valid UUID.",
        "operationId": "list-opportunities",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "pipeline_id",
            "in": "query",
            "required": false,
            "description": "Only opportunities in this pipeline.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "stage_id",
            "in": "query",
            "required": false,
            "description": "Only opportunities in this stage.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "contact_id",
            "in": "query",
            "required": false,
            "description": "Only opportunities for this contact.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "company_id",
            "in": "query",
            "required": false,
            "description": "Only opportunities for this company.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of open, won, lost, abandoned.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "opportunities": [
                    {
                      "id": "9e4c5f61-7d8a-4b92-8c3d-4e5f6a7b8c93",
                      "name": "Acme — annual plan",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "company_id": null,
                      "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                      "stage_id": "0b6f2c1e-3a4d-4e5f-8a9b-1c2d3e4f5a60",
                      "monetary_value": 1200,
                      "source": "website",
                      "status": "open",
                      "assigned_to": null,
                      "created_at": "2026-10-03T12:00:00.000Z",
                      "updated_at": "2026-10-03T12:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Opportunities"
        ],
        "summary": "Create an opportunity",
        "description": "Create an opportunity. Fires your “opportunity created” workflows, exactly like creating one in the app.\n\nReturns 201. 400 if stage_id isn't in the pipeline, the pipeline has no stages, or contact/company/assignee aren't in this sub-account. 404 if the pipeline is not in this sub-account.",
        "operationId": "create-opportunity",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "opportunity": {
                    "id": "9e4c5f61-7d8a-4b92-8c3d-4e5f6a7b8c93",
                    "name": "Acme — annual plan",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "company_id": null,
                    "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                    "stage_id": "0b6f2c1e-3a4d-4e5f-8a9b-1c2d3e4f5a60",
                    "monetary_value": 1200,
                    "source": "website",
                    "status": "open",
                    "assigned_to": null,
                    "created_at": "2026-10-03T12:00:00.000Z",
                    "updated_at": "2026-10-03T12:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Opportunity name."
                  },
                  "pipeline_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Pipeline the opportunity belongs to."
                  },
                  "stage_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Stage within that pipeline. Defaults to the pipeline's first stage."
                  },
                  "contact_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Linked contact; must be in this sub-account."
                  },
                  "company_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Linked company; must be in this sub-account."
                  },
                  "monetary_value": {
                    "type": "number",
                    "description": "Deal value; minimum 0. Defaults to 0."
                  },
                  "source": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Free-text lead source label."
                  },
                  "status": {
                    "type": "string",
                    "description": "One of open, won, lost, abandoned. Defaults to open."
                  },
                  "assigned_to": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "User id of a member of this sub-account."
                  }
                },
                "required": [
                  "name",
                  "pipeline_id"
                ]
              },
              "example": {
                "name": "Acme — annual plan",
                "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                "monetary_value": 1200,
                "source": "website"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/opportunities/{id}": {
      "get": {
        "tags": [
          "Opportunities"
        ],
        "summary": "Get an opportunity",
        "description": "Get one opportunity.\n\n404 if the opportunity is not in this sub-account.",
        "operationId": "get-opportunity",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The opportunity id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "opportunity": {
                    "id": "9e4c5f61-7d8a-4b92-8c3d-4e5f6a7b8c93",
                    "name": "Acme — annual plan",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "company_id": null,
                    "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                    "stage_id": "0b6f2c1e-3a4d-4e5f-8a9b-1c2d3e4f5a60",
                    "monetary_value": 1200,
                    "source": "website",
                    "status": "open",
                    "assigned_to": null,
                    "created_at": "2026-10-03T12:00:00.000Z",
                    "updated_at": "2026-10-03T12:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Opportunities"
        ],
        "summary": "Update an opportunity",
        "description": "Update an opportunity — only the fields you send change. Changing the stage fires your “pipeline stage changed” workflows.\n\nSend at least one field. 400 if stage_id isn't in the (new) pipeline or a linked id isn't in this sub-account; 404 if the opportunity is not in this sub-account.",
        "operationId": "update-opportunity",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The opportunity id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "opportunity": {
                    "id": "9e4c5f61-7d8a-4b92-8c3d-4e5f6a7b8c93",
                    "name": "Acme — annual plan",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "company_id": null,
                    "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                    "stage_id": "0b6f2c1e-3a4d-4e5f-8a9b-1c2d3e4f5a60",
                    "monetary_value": 1200,
                    "source": "website",
                    "status": "open",
                    "assigned_to": null,
                    "created_at": "2026-10-03T12:00:00.000Z",
                    "updated_at": "2026-10-03T12:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Opportunity name."
                  },
                  "pipeline_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Move to another pipeline. Without stage_id the opportunity lands in that pipeline's first stage."
                  },
                  "stage_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Stage within that pipeline."
                  },
                  "contact_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Linked contact; must be in this sub-account."
                  },
                  "company_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Linked company; must be in this sub-account."
                  },
                  "monetary_value": {
                    "type": "number",
                    "description": "Deal value; minimum 0. Defaults to 0."
                  },
                  "source": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Free-text lead source label."
                  },
                  "status": {
                    "type": "string",
                    "description": "One of open, won, lost, abandoned. Defaults to open."
                  },
                  "assigned_to": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "User id of a member of this sub-account."
                  }
                }
              },
              "example": {
                "status": "won",
                "monetary_value": 1500
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Opportunities"
        ],
        "summary": "Delete an opportunity",
        "description": "Permanently delete an opportunity.\n\n404 if the opportunity is not in this sub-account.",
        "operationId": "delete-opportunity",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The opportunity id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "opportunity_id": "9e4c5f61-7d8a-4b92-8c3d-4e5f6a7b8c93"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/opportunities/{id}/move": {
      "post": {
        "tags": [
          "Opportunities"
        ],
        "summary": "Move an opportunity to a stage",
        "description": "Move an opportunity to another stage, optionally in another pipeline. Fires your “pipeline stage changed” workflows.\n\n400 if the stage isn't in the pipeline; 404 if the opportunity or pipeline is not in this sub-account.",
        "operationId": "move-opportunity",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The opportunity id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "opportunity": {
                    "id": "9e4c5f61-7d8a-4b92-8c3d-4e5f6a7b8c93",
                    "name": "Acme — annual plan",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "company_id": null,
                    "pipeline_id": "7d2e9f10-4b3c-4a1d-9e8f-2a3b4c5d6e7f",
                    "stage_id": "0b6f2c1e-3a4d-4e5f-8a9b-1c2d3e4f5a60",
                    "monetary_value": 1200,
                    "source": "website",
                    "status": "open",
                    "assigned_to": null,
                    "created_at": "2026-10-03T12:00:00.000Z",
                    "updated_at": "2026-10-03T12:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "stage_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Target stage."
                  },
                  "pipeline_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Target pipeline, when moving across pipelines; stage_id must belong to it."
                  }
                },
                "required": [
                  "stage_id"
                ]
              },
              "example": {
                "stage_id": "1c7a3d2f-4b5e-4f60-9a1b-2c3d4e5f6a71"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/companies": {
      "get": {
        "tags": [
          "Companies"
        ],
        "summary": "List companies",
        "description": "List the sub-account's companies alphabetically by name, each with its contact count.\n\n401 if unauthenticated.",
        "operationId": "list-companies",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring match on the company name (max 200 characters).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "companies": [
                    {
                      "id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                      "name": "Acme Inc",
                      "website": "https://acme.example.com",
                      "industry": "Manufacturing",
                      "phone": "+14155550123",
                      "email": "hello@acme.example.com",
                      "address": "1 Example Way, San Francisco, CA",
                      "notes": null,
                      "contact_count": 2,
                      "created_at": "2026-10-01T10:00:00.000Z",
                      "updated_at": "2026-10-01T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Companies"
        ],
        "summary": "Create a company",
        "description": "Create a company. Values are trimmed; empty strings are stored as null.\n\nReturns 201. The response does not include contact_count. 400 if name is missing or empty.",
        "operationId": "create-company",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "company": {
                    "id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                    "name": "Acme Inc",
                    "website": "https://acme.example.com",
                    "industry": "Manufacturing",
                    "phone": "+14155550123",
                    "email": "hello@acme.example.com",
                    "address": "1 Example Way, San Francisco, CA",
                    "notes": null,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Company name (max 500 characters)."
                  },
                  "website": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Website URL."
                  },
                  "industry": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Industry label."
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Phone number (stored as given)."
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address."
                  },
                  "address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Postal address."
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Free-text notes."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Acme Inc",
                "website": "https://acme.example.com",
                "industry": "Manufacturing",
                "phone": "+14155550123",
                "email": "hello@acme.example.com",
                "address": "1 Example Way, San Francisco, CA"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/companies/{id}": {
      "get": {
        "tags": [
          "Companies"
        ],
        "summary": "Get a company",
        "description": "Get one company with its contact count.\n\n400 if the id is not a valid UUID; 404 if the company is not in this sub-account.",
        "operationId": "get-company",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The company id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "company": {
                    "id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                    "name": "Acme Inc",
                    "website": "https://acme.example.com",
                    "industry": "Manufacturing",
                    "phone": "+14155550123",
                    "email": "hello@acme.example.com",
                    "address": "1 Example Way, San Francisco, CA",
                    "notes": null,
                    "contact_count": 2,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Companies"
        ],
        "summary": "Update a company",
        "description": "Update a company — only the fields you send change. Send null (or an empty string) to clear an optional field.\n\nSend at least one field. The response does not include contact_count. 400 if the id is invalid, name is empty or no updatable field is sent; 404 if the company is not in this sub-account.",
        "operationId": "update-company",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The company id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "company": {
                    "id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                    "name": "Acme Inc",
                    "website": "https://acme.example.com",
                    "industry": "Software",
                    "phone": "+14155550123",
                    "email": "hello@acme.example.com",
                    "address": "1 Example Way, San Francisco, CA",
                    "notes": null,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-04T08:15:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New company name (max 500 characters); cannot be empty."
                  },
                  "website": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Website URL."
                  },
                  "industry": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Industry label."
                  },
                  "phone": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Phone number (stored as given)."
                  },
                  "email": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Email address."
                  },
                  "address": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Postal address."
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Free-text notes."
                  }
                }
              },
              "example": {
                "industry": "Software",
                "notes": null
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Companies"
        ],
        "summary": "Delete a company",
        "description": "Permanently delete a company. Its contacts and opportunities are kept and simply unlinked from it.\n\n400 if the id is not a valid UUID; 404 if the company is not in this sub-account.",
        "operationId": "delete-company",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The company id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "company_id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/companies/{id}/contacts": {
      "get": {
        "tags": [
          "Companies"
        ],
        "summary": "List a company's contacts",
        "description": "List the contacts linked to a company, newest first.\n\nContacts use the same shape as List contacts. 400 if the id is not a valid UUID; 404 if the company is not in this sub-account.",
        "operationId": "list-company-contacts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The company id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "company_id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "contacts": [
                    {
                      "id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "name": "Jane Doe",
                      "email": "jane@example.com",
                      "phone": "+14155550142",
                      "address": null,
                      "tags": [
                        "customer"
                      ],
                      "lead_source": "website",
                      "created_at": "2026-10-02T09:30:00.000Z",
                      "updated_at": "2026-10-02T09:30:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/smart-lists": {
      "get": {
        "tags": [
          "Smart Lists"
        ],
        "summary": "List smart lists",
        "description": "List the sub-account's smart lists in the app's display order. Smart lists are read-only through the API.\n\nSmart lists are static membership lists: conditions is returned as stored but does not decide membership. 401 if unauthenticated.",
        "operationId": "list-smart-lists",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "smart_lists": [
                    {
                      "id": "8c2d4e6f-1a3b-4c5d-9e7f-0a1b2c3d4e5f",
                      "name": "Hot leads",
                      "description": "Leads to follow up this week",
                      "conditions": {},
                      "contact_count": 12,
                      "last_campaign": null,
                      "status": "active",
                      "display_order": 0,
                      "created_at": "2026-10-01T10:00:00.000Z",
                      "updated_at": "2026-10-03T12:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/smart-lists/{id}": {
      "get": {
        "tags": [
          "Smart Lists"
        ],
        "summary": "Get a smart list",
        "description": "Get one smart list.\n\n400 if the id is not a valid UUID; 404 if the smart list is not in this sub-account.",
        "operationId": "get-smart-list",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The smart list id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "smart_list": {
                    "id": "8c2d4e6f-1a3b-4c5d-9e7f-0a1b2c3d4e5f",
                    "name": "Hot leads",
                    "description": "Leads to follow up this week",
                    "conditions": {},
                    "contact_count": 12,
                    "last_campaign": null,
                    "status": "active",
                    "display_order": 0,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-03T12:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/smart-lists/{id}/contacts": {
      "get": {
        "tags": [
          "Smart Lists"
        ],
        "summary": "List a smart list's contacts",
        "description": "List the contacts in a smart list, most recently added first — the same members the app's list view, bulk SMS/email and campaigns use.\n\nContacts use the same shape as List contacts. total and has_more count memberships, so a page can hold fewer contacts than limit if a member is no longer in this sub-account. 400 if the id is not a valid UUID; 404 if the smart list is not in this sub-account.",
        "operationId": "list-smart-list-contacts",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The smart list id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "smart_list_id": "8c2d4e6f-1a3b-4c5d-9e7f-0a1b2c3d4e5f",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "contacts": [
                    {
                      "id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "name": "Jane Doe",
                      "email": "jane@example.com",
                      "phone": "+14155550142",
                      "address": null,
                      "tags": [
                        "customer"
                      ],
                      "lead_source": "website",
                      "created_at": "2026-10-02T09:30:00.000Z",
                      "updated_at": "2026-10-02T09:30:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/custom-fields": {
      "get": {
        "tags": [
          "Custom Fields"
        ],
        "summary": "List custom fields",
        "description": "List the sub-account's contact custom field definitions, ordered by sort_order then name.\n\noptions is null for field types without choices. 400 if source is not platform or ghl.",
        "operationId": "list-custom-fields",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "source",
            "in": "query",
            "required": false,
            "description": "One of platform (created in Centerfy) or ghl (mirrored from GoHighLevel).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "include_archived",
            "in": "query",
            "required": false,
            "description": "Also return archived definitions.",
            "schema": {
              "type": "boolean",
              "default": "false"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "custom_fields": [
                    {
                      "id": "4b5c6d7e-8f90-4a1b-9c2d-3e4f5a6b7c8d",
                      "field_name": "Plan tier",
                      "field_key": "plan_tier",
                      "field_type": "select",
                      "options": [
                        "Basic",
                        "Pro",
                        "Enterprise"
                      ],
                      "is_required": false,
                      "sort_order": 0,
                      "source": "platform",
                      "external_key": null,
                      "archived_at": null,
                      "created_at": "2026-10-01T10:00:00.000Z",
                      "updated_at": "2026-10-01T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Custom Fields"
        ],
        "summary": "Create a custom field",
        "description": "Create a contact custom field. The field_key (the key values are stored under) is derived from field_name the same way the app does, unless you pass one.\n\nReturns 201. New fields always have source platform. 400 if field_name is empty, field_key has no letters or digits, or a select / multiselect field has no options. 409 if a field with that key already exists in this sub-account (including archived and GoHighLevel fields).",
        "operationId": "create-custom-field",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "custom_field": {
                    "id": "4b5c6d7e-8f90-4a1b-9c2d-3e4f5a6b7c8d",
                    "field_name": "Plan tier",
                    "field_key": "plan_tier",
                    "field_type": "select",
                    "options": [
                      "Basic",
                      "Pro",
                      "Enterprise"
                    ],
                    "is_required": false,
                    "sort_order": 0,
                    "source": "platform",
                    "external_key": null,
                    "archived_at": null,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "field_name": {
                    "type": "string",
                    "description": "Display name, 1–200 characters."
                  },
                  "field_type": {
                    "type": "string",
                    "description": "One of text, number, date, select, textarea, checkbox, multiselect."
                  },
                  "options": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string"
                    },
                    "description": "Choices for select / multiselect fields (max 200); required and non-empty for those types. Blank entries are dropped."
                  },
                  "is_required": {
                    "type": "boolean",
                    "description": "Whether the field is marked required in the app. Defaults to false."
                  },
                  "sort_order": {
                    "type": "integer",
                    "description": "Display position; minimum 0. Defaults to after the existing Centerfy fields."
                  },
                  "field_key": {
                    "type": "string",
                    "description": "Storage key (max 64). Lower-cased, with runs of other characters turned into _. Defaults to the same treatment of field_name."
                  }
                },
                "required": [
                  "field_name",
                  "field_type"
                ]
              },
              "example": {
                "field_name": "Plan tier",
                "field_type": "select",
                "options": [
                  "Basic",
                  "Pro",
                  "Enterprise"
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/custom-fields/{id}": {
      "get": {
        "tags": [
          "Custom Fields"
        ],
        "summary": "Get a custom field",
        "description": "Get one custom field definition.\n\n400 if the id is not a valid UUID; 404 if the field is not in this sub-account.",
        "operationId": "get-custom-field",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The custom field definition id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "custom_field": {
                    "id": "4b5c6d7e-8f90-4a1b-9c2d-3e4f5a6b7c8d",
                    "field_name": "Plan tier",
                    "field_key": "plan_tier",
                    "field_type": "select",
                    "options": [
                      "Basic",
                      "Pro",
                      "Enterprise"
                    ],
                    "is_required": false,
                    "sort_order": 0,
                    "source": "platform",
                    "external_key": null,
                    "archived_at": null,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Custom Fields"
        ],
        "summary": "Update a custom field",
        "description": "Update a custom field definition — only the fields you send change. field_key cannot be changed, because stored values are keyed by it.\n\nSend at least one field. Values already stored on contacts are not converted when the type or options change. 400 if field_name is empty or the resulting select / multiselect field would have no options; 404 if the field is not in this sub-account; 409 if the field is managed in GoHighLevel.",
        "operationId": "update-custom-field",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The custom field definition id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "custom_field": {
                    "id": "4b5c6d7e-8f90-4a1b-9c2d-3e4f5a6b7c8d",
                    "field_name": "Plan tier",
                    "field_key": "plan_tier",
                    "field_type": "select",
                    "options": [
                      "Basic",
                      "Pro",
                      "Enterprise",
                      "Custom"
                    ],
                    "is_required": false,
                    "sort_order": 0,
                    "source": "platform",
                    "external_key": null,
                    "archived_at": null,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-04T08:15:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "field_name": {
                    "type": "string",
                    "description": "Display name, 1–200 characters."
                  },
                  "field_type": {
                    "type": "string",
                    "description": "One of text, number, date, select, textarea, checkbox, multiselect."
                  },
                  "options": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string"
                    },
                    "description": "Choices for select / multiselect fields (max 200); required and non-empty for those types. Blank entries are dropped."
                  },
                  "is_required": {
                    "type": "boolean",
                    "description": "Whether the field is marked required in the app."
                  },
                  "sort_order": {
                    "type": "integer",
                    "description": "Display position; minimum 0."
                  }
                }
              },
              "example": {
                "options": [
                  "Basic",
                  "Pro",
                  "Enterprise",
                  "Custom"
                ]
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Custom Fields"
        ],
        "summary": "Delete a custom field",
        "description": "Permanently delete a custom field definition. Values already stored on contacts are left in place, as in the app.\n\n400 if the id is not a valid UUID; 404 if the field is not in this sub-account; 409 if the field is managed in GoHighLevel.",
        "operationId": "delete-custom-field",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The custom field definition id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "custom_field_id": "4b5c6d7e-8f90-4a1b-9c2d-3e4f5a6b7c8d"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/custom-fields": {
      "get": {
        "tags": [
          "Custom Fields"
        ],
        "summary": "Get a contact's custom field values",
        "description": "Get a contact's custom field values: the raw values keyed by field_key, plus every active (non-archived) definition paired with its value.\n\nfields is not paginated and is ordered by sort_order; value is null when the contact has no value for that field. custom_field_values may also hold keys whose definition was deleted or archived. 400 if the id is not a valid UUID; 404 if the contact is not in this sub-account.",
        "operationId": "get-contact-custom-fields",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "custom_field_values": {
                    "plan_tier": "Pro"
                  },
                  "fields": [
                    {
                      "id": "4b5c6d7e-8f90-4a1b-9c2d-3e4f5a6b7c8d",
                      "field_name": "Plan tier",
                      "field_key": "plan_tier",
                      "field_type": "select",
                      "options": [
                        "Basic",
                        "Pro",
                        "Enterprise"
                      ],
                      "is_required": false,
                      "sort_order": 0,
                      "source": "platform",
                      "external_key": null,
                      "archived_at": null,
                      "created_at": "2026-10-01T10:00:00.000Z",
                      "updated_at": "2026-10-01T10:00:00.000Z",
                      "value": "Pro"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "put": {
        "tags": [
          "Custom Fields"
        ],
        "summary": "Set a contact's custom field values",
        "description": "Merge custom field values into a contact. Keys you don't send are left unchanged; null clears a value.\n\nupdated lists the keys written; custom_field_values is the contact's full set after the merge. The request is all-or-nothing: 400 (and nothing is written) if values is empty, has more than 200 keys, names an unknown or archived field, names a field managed in GoHighLevel (read-only here), or a value has the wrong type or option. 404 if the contact is not in this sub-account.",
        "operationId": "set-contact-custom-fields",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "updated": [
                    "plan_tier"
                  ],
                  "custom_field_values": {
                    "plan_tier": "Pro"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "values": {
                    "type": "object",
                    "description": "Object keyed by field_key (1–200 keys). Each key must be an active Centerfy field in this sub-account. Values must match the field type: number → number, checkbox → boolean, date → \"YYYY-MM-DD\" string, select → one of its options, multiselect → array of its options, text / textarea → string. null clears a value."
                  }
                },
                "required": [
                  "values"
                ]
              },
              "example": {
                "values": {
                  "plan_tier": "Pro"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/dnc": {
      "put": {
        "tags": [
          "Contact Consent"
        ],
        "summary": "Set DNC bypass",
        "description": "Turn the per-contact DNC bypass on or off — the same toggle as on the contact profile. While bypass is true, the Do Not Call check is skipped before outbound AI calls to this contact.\n\nWhen the value changes, a system note is added to the contact's conversation (if it has one). Returns the contact's current consent flags. 400 if the id is not a valid UUID or the body is invalid; 404 if the contact is not in this sub-account.",
        "operationId": "set-contact-dnc",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/XWebhookSource"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "dnc_bypass": true,
                  "sms_unsubscribed": false,
                  "email_unsubscribed": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "bypass": {
                    "type": "boolean",
                    "description": "true to bypass DNC checks for this contact, false to re-enable them."
                  }
                },
                "required": [
                  "bypass"
                ]
              },
              "example": {
                "bypass": true
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/unsubscribe": {
      "post": {
        "tags": [
          "Contact Consent"
        ],
        "summary": "Unsubscribe a contact",
        "description": "Unsubscribe a contact from SMS and/or email — the same toggles as on the contact profile. Bulk SMS and email sends skip unsubscribed contacts.\n\nChannels not listed are left unchanged. For each channel that actually changes, a system note is added to the contact's conversation (if it has one). Returns the contact's current consent flags. 400 if the id is not a valid UUID or the body is invalid; 404 if the contact is not in this sub-account.",
        "operationId": "unsubscribe-contact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/XWebhookSource"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "dnc_bypass": false,
                  "sms_unsubscribed": true,
                  "email_unsubscribed": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Channels to unsubscribe from: one or both of sms, email (no duplicates)."
                  }
                },
                "required": [
                  "channels"
                ]
              },
              "example": {
                "channels": [
                  "sms",
                  "email"
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/resubscribe": {
      "post": {
        "tags": [
          "Contact Consent"
        ],
        "summary": "Re-subscribe a contact",
        "description": "Re-subscribe a contact to SMS and/or email, clearing the unsubscribe flag for those channels.\n\nChannels not listed are left unchanged. For each channel that actually changes, a system note is added to the contact's conversation (if it has one). Returns the contact's current consent flags. 400 if the id is not a valid UUID or the body is invalid; 404 if the contact is not in this sub-account.",
        "operationId": "resubscribe-contact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/XWebhookSource"
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "dnc_bypass": false,
                  "sms_unsubscribed": true,
                  "email_unsubscribed": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "channels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Channels to re-subscribe to: one or both of sms, email (no duplicates)."
                  }
                },
                "required": [
                  "channels"
                ]
              },
              "example": {
                "channels": [
                  "email"
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/lead-score-rules": {
      "get": {
        "tags": [
          "Lead Scoring"
        ],
        "summary": "List lead scoring rules",
        "description": "List the sub-account's lead scoring rules, ordered by sort_order. Rules are managed in the app (Settings) and are read-only through the API.\n\ntrigger is one of lead_responded, appointment_booked, call_completed, tag_added. condition narrows the trigger, e.g. {\"sentiment_gte\": 60}, {\"sentiment_lte\": 40} or {\"tag\": \"vip\"}; {} means always. points may be negative. max_times caps how often the rule can score per contact; null means unlimited.",
        "operationId": "list-lead-score-rules",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "enabled",
            "in": "query",
            "required": false,
            "description": "true for enabled rules only, false for disabled rules only. Omit for all.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 2,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "rules": [
                    {
                      "id": "6d7e8f90-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
                      "trigger": "lead_responded",
                      "condition": {
                        "sentiment_gte": 60
                      },
                      "points": 1,
                      "max_times": null,
                      "enabled": true,
                      "sort_order": 10,
                      "created_at": "2026-10-01T10:00:00.000Z",
                      "updated_at": "2026-10-01T10:00:00.000Z"
                    },
                    {
                      "id": "7e8f9a01-2b3c-4d4e-9f5a-6b7c8d9e0f1a",
                      "trigger": "appointment_booked",
                      "condition": {},
                      "points": 5,
                      "max_times": null,
                      "enabled": true,
                      "sort_order": 30,
                      "created_at": "2026-10-01T10:00:00.000Z",
                      "updated_at": "2026-10-01T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/lead-score": {
      "get": {
        "tags": [
          "Lead Scoring"
        ],
        "summary": "Get a contact's lead score",
        "description": "Get a contact's current lead score, lead quality and latest sentiment, plus its most recent scoring events (newest first).\n\nlead_score is max(0, lead_score_baseline + the sum of all event points). rule_label is the rule's wording when it fired; rule_id is null if the rule was later deleted. source_type is e.g. message, call_log, appointment or contact_tag. 400 if the id is not a valid UUID or limit is out of range; 404 if the contact is not in this sub-account.",
        "operationId": "get-contact-lead-score",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Number of recent events to return; minimum 1, maximum 100.",
            "schema": {
              "type": "integer",
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "lead_score": 56,
                  "lead_score_baseline": 50,
                  "lead_quality": null,
                  "latest_sentiment_score": 72,
                  "latest_sentiment_label": "positive",
                  "latest_sentiment_at": "2026-10-03T12:05:00.000Z",
                  "events": [
                    {
                      "id": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
                      "rule_id": "6d7e8f90-1a2b-4c3d-8e4f-5a6b7c8d9e0f",
                      "trigger": "lead_responded",
                      "rule_label": "Lead responded, sentiment >= 60",
                      "points": 1,
                      "source_type": "message",
                      "source_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
                      "source_channel": "sms",
                      "context": {},
                      "created_at": "2026-10-03T12:05:00.000Z"
                    },
                    {
                      "id": "8f9e0d1c-2b3a-4948-8776-5a4b3c2d1e0f",
                      "rule_id": "7e8f9a01-2b3c-4d4e-9f5a-6b7c8d9e0f1a",
                      "trigger": "appointment_booked",
                      "rule_label": "Appointment booked",
                      "points": 5,
                      "source_type": "appointment",
                      "source_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "source_channel": null,
                      "context": {},
                      "created_at": "2026-10-02T15:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/lead-score/recompute": {
      "post": {
        "tags": [
          "Lead Scoring"
        ],
        "summary": "Recompute a lead score",
        "description": "Rebuild a contact's lead score from its baseline and all of its scoring events, and save it on the contact.\n\nNo request body. Useful as a repair if the stored score drifted; normally the score updates on its own as events are recorded. 400 if the id is not a valid UUID; 404 if the contact is not in this sub-account.",
        "operationId": "recompute-contact-lead-score",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "previous_lead_score": 54,
                  "lead_score": 56
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/analytics/overview": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Insights overview",
        "description": "The Reports › Insights overview: wallet balance, cost per message compared with the previous period of equal length, a daily series (spend, contacts touched, spend per contact, tool uses) and a bookings heatmap.\n\ndaily has one entry per day in the range. Money values are rounded to 4 decimal places; value, prev_value and spend_per_contact are null when there is nothing to divide by. wallet.yesterday_end is the balance at the end of yesterday (null if unknown). Message spend covers SMS, email, WhatsApp, Facebook and Instagram; messages counts agent-sent messages. heatmap lists only non-empty cells: dow is the ISO weekday (1 = Monday … 7 = Sunday) and hour is the local hour 0–23; cancelled appointments are counted. Days run from local midnight to midnight in tz. capped is true when a data source exceeded 20,000 rows — narrow the range for exact figures. 400 if from/to aren't real YYYY-MM-DD dates, from is after to, the range is over 366 days, or the timezone is unknown. 401 if unauthenticated.",
        "operationId": "analytics-overview",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "First day of the range (inclusive, local date). Defaults to 29 days before to (a 30-day range).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Last day of the range (inclusive, local date). Defaults to today. The range can span at most 366 days.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "description": "IANA time zone for day boundaries, e.g. America/New_York (max 64 characters). Defaults to your company time zone, else UTC.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "bookings_by",
            "in": "query",
            "required": false,
            "description": "Bucket the bookings heatmap by when appointments were created or when they are scheduled for. One of created, scheduled.",
            "schema": {
              "type": "string",
              "default": "created"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "tz": "America/New_York",
                  "from": "2026-10-04",
                  "to": "2026-10-05",
                  "bookings_by": "created",
                  "capped": false,
                  "wallet": {
                    "balance": 42.5,
                    "yesterday_end": 45.1
                  },
                  "cost_per_message": {
                    "spend": 1.2,
                    "messages": 60,
                    "value": 0.02,
                    "prev_spend": 0.9,
                    "prev_messages": 50,
                    "prev_value": 0.018
                  },
                  "daily": [
                    {
                      "day": "2026-10-04",
                      "spend_total": 3.1,
                      "contacts_touched": 12,
                      "spend_per_contact": 0.2583,
                      "tool_uses": 8
                    },
                    {
                      "day": "2026-10-05",
                      "spend_total": 0,
                      "contacts_touched": 0,
                      "spend_per_contact": null,
                      "tool_uses": 0
                    }
                  ],
                  "tool_uses_total": 8,
                  "bookings_total": 3,
                  "heatmap": [
                    {
                      "dow": 1,
                      "hour": 10,
                      "count": 2
                    },
                    {
                      "dow": 3,
                      "hour": 15,
                      "count": 1
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/analytics/agents": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Agent performance",
        "description": "Per-agent performance: contacts first touched in the range, how many became leads, calls, spend and cost per lead. Agents with no activity are left out.\n\nagents is sorted by most leads, then lowest cost per lead, then most contacts. cpl (spend ÷ leads) is null when there are no leads. lead_rule echoes the rule that was applied. touches_since is when first-touch tracking began for this sub-account (null if never) — contacts touched earlier aren't attributed. Ops-manager agents are excluded. 400 if lead_rule=tag without lead_rule_tag. Days run from local midnight to midnight in tz. capped is true when a data source exceeded 20,000 rows — narrow the range for exact figures. 400 if from/to aren't real YYYY-MM-DD dates, from is after to, the range is over 366 days, or the timezone is unknown. 401 if unauthenticated.",
        "operationId": "analytics-agents",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "First day of the range (inclusive, local date). Defaults to 29 days before to (a 30-day range).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Last day of the range (inclusive, local date). Defaults to today. The range can span at most 366 days.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "description": "IANA time zone for day boundaries, e.g. America/New_York (max 64 characters). Defaults to your company time zone, else UTC.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lead_rule",
            "in": "query",
            "required": false,
            "description": "What counts as a lead: appointment (booked a non-cancelled appointment after the first touch), tag (gained lead_rule_tag after the first touch) or lead_score (crossed lead_rule_threshold after the first touch). Defaults to your saved Insights lead rule, else appointment.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lead_rule_tag",
            "in": "query",
            "required": false,
            "description": "Tag to look for when lead_rule=tag (required then; case-insensitive; max 200 characters).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "lead_rule_threshold",
            "in": "query",
            "required": false,
            "description": "Lead score threshold when lead_rule=lead_score; 0–1000. Defaults to 70.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "tz": "America/New_York",
                  "from": "2026-09-06",
                  "to": "2026-10-05",
                  "lead_rule": {
                    "type": "appointment"
                  },
                  "capped": false,
                  "agents": [
                    {
                      "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                      "agent_name": "Front desk",
                      "is_active": true,
                      "contacts": 40,
                      "leads": 8,
                      "calls": 52,
                      "spend": 18.4,
                      "cpl": 2.3
                    }
                  ],
                  "touches_since": "2026-06-01T09:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/analytics/usage": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Usage summary",
        "description": "Resource counts (contacts, active users, phone numbers and agents) plus usage billed in the range, grouped by service type at your sub-account prices.\n\ncounts are current totals, not limited to the range. by_service_type is sorted by total, highest first; quantity and total are rounded to 4 decimal places. Days run from local midnight to midnight in tz. capped is true when a data source exceeded 20,000 rows — narrow the range for exact figures. 400 if from/to aren't real YYYY-MM-DD dates, from is after to, the range is over 366 days, or the timezone is unknown. 401 if unauthenticated.",
        "operationId": "analytics-usage",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "First day of the range (inclusive, local date). Defaults to 29 days before to (a 30-day range).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Last day of the range (inclusive, local date). Defaults to today. The range can span at most 366 days.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "description": "IANA time zone for day boundaries, e.g. America/New_York (max 64 characters). Defaults to your company time zone, else UTC.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "tz": "America/New_York",
                  "from": "2026-09-06",
                  "to": "2026-10-05",
                  "capped": false,
                  "counts": {
                    "contacts": 1250,
                    "users": 4,
                    "phone_numbers": 3,
                    "agents": 5
                  },
                  "total_spend": 96.42,
                  "by_service_type": [
                    {
                      "service_type": "ai_phone_call",
                      "records": 310,
                      "quantity": 912.5,
                      "total": 82.12
                    },
                    {
                      "service_type": "sms",
                      "records": 715,
                      "quantity": 715,
                      "total": 14.3
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/analytics/email": {
      "get": {
        "tags": [
          "Analytics"
        ],
        "summary": "Email performance",
        "description": "Outbound emails sent in the range and their tracking events (delivered, opened, clicked, bounced, complained, unsubscribed), with rates.\n\nevents counts every event; unique_emails counts distinct emails per event. Rates are unique_emails ÷ sent as fractions (0.475 = 47.5%), rounded to 4 decimal places, and null when nothing was sent. Events are only recorded for email sent through a tracked provider, so rates are a lower bound. Days run from local midnight to midnight in tz. capped is true when a data source exceeded 20,000 rows — narrow the range for exact figures. 400 if from/to aren't real YYYY-MM-DD dates, from is after to, the range is over 366 days, or the timezone is unknown. 401 if unauthenticated.",
        "operationId": "analytics-email",
        "parameters": [
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "First day of the range (inclusive, local date). Defaults to 29 days before to (a 30-day range).",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Last day of the range (inclusive, local date). Defaults to today. The range can span at most 366 days.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "description": "IANA time zone for day boundaries, e.g. America/New_York (max 64 characters). Defaults to your company time zone, else UTC.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "tz": "America/New_York",
                  "from": "2026-09-06",
                  "to": "2026-10-05",
                  "capped": false,
                  "sent": 200,
                  "events": {
                    "delivered": 190,
                    "opened": 120,
                    "clicked": 30,
                    "bounced": 6,
                    "complained": 0,
                    "unsubscribed": 2
                  },
                  "unique_emails": {
                    "delivered": 188,
                    "opened": 95,
                    "clicked": 22,
                    "bounced": 6,
                    "complained": 0,
                    "unsubscribed": 2
                  },
                  "rates": {
                    "delivery_rate": 0.94,
                    "open_rate": 0.475,
                    "click_rate": 0.11,
                    "bounce_rate": 0.03,
                    "complaint_rate": 0,
                    "unsubscribe_rate": 0.01
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/calls/{id}/recording": {
      "get": {
        "tags": [
          "Call Recordings"
        ],
        "summary": "Get a recording URL",
        "description": "Get a short-lived signed URL to play or download a call's recording.\n\nThe URL expires after 1 hour, or 5 minutes when the sub-account has HIPAA mode on — request a fresh one each time; don't store it. Responses are sent with Cache-Control: no-store. content_type is audio/mpeg, audio/mp4, audio/wav or audio/ogg. Some older recordings return their original URL with content_type, expires_in_seconds and expires_at set to null. 400 if the id is empty or longer than 200 characters; 404 if the call is not in this sub-account or has no recording; 500 if the URL can't be generated.",
        "operationId": "get-call-recording",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The call id (UUID) or its call_sid.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "download",
            "in": "query",
            "required": false,
            "description": "When true, the URL makes the browser download the file (call-recording-<call_sid>.<ext>) instead of playing it.",
            "schema": {
              "type": "boolean",
              "default": "false"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "MP3 audio",
            "content": {
              "audio/mpeg": {
                "schema": {
                  "type": "string",
                  "format": "binary"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/wallet": {
      "get": {
        "tags": [
          "Wallet"
        ],
        "summary": "Get the wallet balance",
        "description": "Get the sub-account's current wallet balance — the same figure the app shows. Read-only.\n\nbalance is rounded to 2 decimal places. last_activity_at is the time of the latest wallet ledger entry, or null if there is none. 401 if unauthenticated.",
        "operationId": "get-wallet",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "wallet": {
                    "sub_account_id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                    "balance": 42.5,
                    "last_activity_at": "2026-10-05T16:20:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/wallet/usage": {
      "get": {
        "tags": [
          "Wallet"
        ],
        "summary": "List billed usage",
        "description": "List the usage records billed to the wallet (calls, messages, etc.), newest first, at your sub-account prices.\n\nfrom and to are echoed back normalised to UTC (null when not sent). 400 if from or to isn't a valid date, or from is after to. 401 if unauthenticated.",
        "operationId": "list-wallet-usage",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "from",
            "in": "query",
            "required": false,
            "description": "Only records created at or after this date or datetime.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "to",
            "in": "query",
            "required": false,
            "description": "Only records created at or before this date or datetime. A plain YYYY-MM-DD covers that whole UTC day.",
            "schema": {
              "type": "string",
              "format": "date-time"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "Only records of this service type, e.g. ai_phone_call or sms (max 100 characters).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "from": "2026-10-01T00:00:00.000Z",
                  "to": "2026-10-05T23:59:59.999Z",
                  "usage": [
                    {
                      "id": "8c2d4e6f-1a3b-4c5d-9e7f-0a1b2c3d4e5f",
                      "service_type": "ai_phone_call",
                      "quantity": 3.5,
                      "unit_price": 0.12,
                      "total": 0.42,
                      "description": "AI phone call",
                      "reference_id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
                      "reference_type": "call",
                      "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                      "created_at": "2026-10-05T16:20:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/calendars": {
      "get": {
        "tags": [
          "Calendars"
        ],
        "summary": "List calendars",
        "description": "List the sub-account's calendars, sorted by name.\n\nIntegration fields (calendar_integration_id, HubSpot and Salesforce ids) are read-only and set by the app's sync. 401 if unauthenticated.",
        "operationId": "list-calendars",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "is_active",
            "in": "query",
            "required": false,
            "description": "Only active (true) or inactive (false) calendars.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "calendars": [
                    {
                      "id": "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b",
                      "name": "Consultations",
                      "description": "30-minute intro call",
                      "color": "#3B82F6",
                      "is_default": true,
                      "is_active": true,
                      "booking_duration_minutes": 30,
                      "booking_location_type": "phone_call",
                      "meeting_interval_minutes": null,
                      "minimum_scheduling_notice_minutes": 60,
                      "date_range_days": 30,
                      "pre_buffer_minutes": 0,
                      "post_buffer_minutes": 10,
                      "max_bookings_per_day": null,
                      "max_bookings_per_slot": 1,
                      "confirmation_type": "show_details",
                      "confirmation_redirect_url": null,
                      "user_id": null,
                      "calendar_integration_id": null,
                      "is_hubspot_calendar": false,
                      "hubspot_scheduler_id": null,
                      "hubspot_scheduler_slug": null,
                      "is_salesforce_calendar": false,
                      "salesforce_calendar_id": null,
                      "created_at": "2026-10-01T10:00:00.000Z",
                      "updated_at": "2026-10-01T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Calendars"
        ],
        "summary": "Create a calendar",
        "description": "Create a calendar. Integration fields can't be set through the API.\n\nReturns 201. 400 if name is empty, a field is out of range, confirmation_redirect_url isn't an http(s) URL, or confirmation_type is redirect without a confirmation_redirect_url.",
        "operationId": "create-calendar",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "calendar": {
                    "id": "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b",
                    "name": "Consultations",
                    "description": "30-minute intro call",
                    "color": "#3B82F6",
                    "is_default": true,
                    "is_active": true,
                    "booking_duration_minutes": 30,
                    "booking_location_type": "phone_call",
                    "meeting_interval_minutes": null,
                    "minimum_scheduling_notice_minutes": 60,
                    "date_range_days": 30,
                    "pre_buffer_minutes": 0,
                    "post_buffer_minutes": 10,
                    "max_bookings_per_day": null,
                    "max_bookings_per_slot": 1,
                    "confirmation_type": "show_details",
                    "confirmation_redirect_url": null,
                    "user_id": null,
                    "calendar_integration_id": null,
                    "is_hubspot_calendar": false,
                    "hubspot_scheduler_id": null,
                    "hubspot_scheduler_slug": null,
                    "is_salesforce_calendar": false,
                    "salesforce_calendar_id": null,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Calendar name."
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Description."
                  },
                  "color": {
                    "type": "string",
                    "description": "Hex colour in #RRGGBB form, e.g. #3B82F6."
                  },
                  "is_default": {
                    "type": "boolean",
                    "description": "Make this the default calendar. Setting true clears the flag on every other calendar in the sub-account."
                  },
                  "is_active": {
                    "type": "boolean",
                    "description": "Whether the calendar accepts bookings. Defaults to true."
                  },
                  "booking_duration_minutes": {
                    "type": "integer",
                    "description": "Length of a booking in minutes; 1–1440. Defaults to 30."
                  },
                  "meeting_interval_minutes": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Minutes between slot start times; 1–1440."
                  },
                  "minimum_scheduling_notice_minutes": {
                    "type": "integer",
                    "description": "How far ahead a booking must be made, in minutes; minimum 0. Defaults to 0."
                  },
                  "date_range_days": {
                    "type": "integer",
                    "description": "How many days ahead can be booked; minimum 1. Defaults to 30."
                  },
                  "pre_buffer_minutes": {
                    "type": "integer",
                    "description": "Free time kept before each booking; minimum 0. Defaults to 0."
                  },
                  "post_buffer_minutes": {
                    "type": "integer",
                    "description": "Free time kept after each booking; minimum 0. Defaults to 0."
                  },
                  "max_bookings_per_day": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Daily booking cap; minimum 1. null for no cap."
                  },
                  "max_bookings_per_slot": {
                    "type": "integer",
                    "description": "Bookings allowed in the same slot; minimum 1. Defaults to 1."
                  },
                  "confirmation_type": {
                    "type": "string",
                    "description": "What the public booking page does after booking. One of show_details, redirect. Defaults to show_details."
                  },
                  "confirmation_redirect_url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "http(s) URL to send people to; required when confirmation_type is redirect."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Consultations",
                "description": "30-minute intro call",
                "color": "#3B82F6",
                "booking_duration_minutes": 30,
                "minimum_scheduling_notice_minutes": 60,
                "post_buffer_minutes": 10,
                "is_default": true
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/calendars/{id}": {
      "get": {
        "tags": [
          "Calendars"
        ],
        "summary": "Get a calendar",
        "description": "Get one calendar.\n\n404 if the calendar is not in this sub-account.",
        "operationId": "get-calendar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The calendar id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "calendar": {
                    "id": "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b",
                    "name": "Consultations",
                    "description": "30-minute intro call",
                    "color": "#3B82F6",
                    "is_default": true,
                    "is_active": true,
                    "booking_duration_minutes": 30,
                    "booking_location_type": "phone_call",
                    "meeting_interval_minutes": null,
                    "minimum_scheduling_notice_minutes": 60,
                    "date_range_days": 30,
                    "pre_buffer_minutes": 0,
                    "post_buffer_minutes": 10,
                    "max_bookings_per_day": null,
                    "max_bookings_per_slot": 1,
                    "confirmation_type": "show_details",
                    "confirmation_redirect_url": null,
                    "user_id": null,
                    "calendar_integration_id": null,
                    "is_hubspot_calendar": false,
                    "hubspot_scheduler_id": null,
                    "hubspot_scheduler_slug": null,
                    "is_salesforce_calendar": false,
                    "salesforce_calendar_id": null,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Calendars"
        ],
        "summary": "Update a calendar",
        "description": "Update a calendar — only the fields you send change.\n\nSend at least one field. 400 if name is sent empty, a field is out of range, confirmation_redirect_url isn't an http(s) URL, or the result would be confirmation_type redirect with no confirmation_redirect_url. 404 if the calendar is not in this sub-account.",
        "operationId": "update-calendar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The calendar id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "calendar": {
                    "id": "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b",
                    "name": "Consultations",
                    "description": "30-minute intro call",
                    "color": "#3B82F6",
                    "is_default": true,
                    "is_active": true,
                    "booking_duration_minutes": 30,
                    "booking_location_type": "phone_call",
                    "meeting_interval_minutes": null,
                    "minimum_scheduling_notice_minutes": 60,
                    "date_range_days": 30,
                    "pre_buffer_minutes": 0,
                    "post_buffer_minutes": 10,
                    "max_bookings_per_day": null,
                    "max_bookings_per_slot": 1,
                    "confirmation_type": "show_details",
                    "confirmation_redirect_url": null,
                    "user_id": null,
                    "calendar_integration_id": null,
                    "is_hubspot_calendar": false,
                    "hubspot_scheduler_id": null,
                    "hubspot_scheduler_slug": null,
                    "is_salesforce_calendar": false,
                    "salesforce_calendar_id": null,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Calendar name."
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Description."
                  },
                  "color": {
                    "type": "string",
                    "description": "Hex colour in #RRGGBB form, e.g. #3B82F6."
                  },
                  "is_default": {
                    "type": "boolean",
                    "description": "Make this the default calendar. Setting true clears the flag on every other calendar in the sub-account."
                  },
                  "is_active": {
                    "type": "boolean",
                    "description": "Whether the calendar accepts bookings. Defaults to true."
                  },
                  "booking_duration_minutes": {
                    "type": "integer",
                    "description": "Length of a booking in minutes; 1–1440. Defaults to 30."
                  },
                  "meeting_interval_minutes": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Minutes between slot start times; 1–1440."
                  },
                  "minimum_scheduling_notice_minutes": {
                    "type": "integer",
                    "description": "How far ahead a booking must be made, in minutes; minimum 0. Defaults to 0."
                  },
                  "date_range_days": {
                    "type": "integer",
                    "description": "How many days ahead can be booked; minimum 1. Defaults to 30."
                  },
                  "pre_buffer_minutes": {
                    "type": "integer",
                    "description": "Free time kept before each booking; minimum 0. Defaults to 0."
                  },
                  "post_buffer_minutes": {
                    "type": "integer",
                    "description": "Free time kept after each booking; minimum 0. Defaults to 0."
                  },
                  "max_bookings_per_day": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "Daily booking cap; minimum 1. null for no cap."
                  },
                  "max_bookings_per_slot": {
                    "type": "integer",
                    "description": "Bookings allowed in the same slot; minimum 1. Defaults to 1."
                  },
                  "confirmation_type": {
                    "type": "string",
                    "description": "What the public booking page does after booking. One of show_details, redirect. Defaults to show_details."
                  },
                  "confirmation_redirect_url": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "http(s) URL to send people to; required when confirmation_type is redirect."
                  }
                }
              },
              "example": {
                "booking_duration_minutes": 45,
                "max_bookings_per_day": 8
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Calendars"
        ],
        "summary": "Delete a calendar",
        "description": "Permanently delete a calendar. Its appointments are kept but no longer linked to a calendar.\n\n409 if the calendar is synced from an integration (Google/Outlook, HubSpot, Salesforce) — delete those in the app — or is still referenced elsewhere, e.g. assigned to an agent (remove those links first). 404 if the calendar is not in this sub-account.",
        "operationId": "delete-calendar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The calendar id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "calendar_id": "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b",
                  "appointments_unlinked": 12
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/calendars/{id}/free-slots": {
      "get": {
        "tags": [
          "Calendars"
        ],
        "summary": "List free slots",
        "description": "List open booking slots on a calendar between two dates, using the same availability check your AI agents use (business hours, existing appointments and connected calendars).\n\nsource is centerfy, ghl or hubspot depending on where the calendar's availability lives. 400 if start/end aren't valid YYYY-MM-DD dates, end is before start, the range is over 31 days, or the timezone is unknown. 404 if the calendar is not in this sub-account. 409 if business hours aren't configured for this sub-account — set them in Settings → Company first. 502 if the availability check fails.",
        "operationId": "calendar-free-slots",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The calendar id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "start",
            "in": "query",
            "required": true,
            "description": "First day to search.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "end",
            "in": "query",
            "required": true,
            "description": "Last day to search (inclusive); at most 31 days after start counting both ends.",
            "schema": {
              "type": "string",
              "format": "date"
            }
          },
          {
            "name": "timezone",
            "in": "query",
            "required": false,
            "description": "IANA time zone the days are read in and slots are returned in, e.g. America/New_York. Defaults to your company time zone.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "duration_minutes",
            "in": "query",
            "required": false,
            "description": "Slot length in minutes; 1–1440. Defaults to the calendar's booking_duration_minutes.",
            "schema": {
              "type": "integer"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "calendar_id": "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b",
                  "start": "2026-10-07",
                  "end": "2026-10-07",
                  "timezone": "America/New_York",
                  "source": "centerfy",
                  "total": 2,
                  "slots": [
                    {
                      "start_time": "2026-10-07T09:00:00-04:00",
                      "end_time": "2026-10-07T09:30:00-04:00",
                      "duration_minutes": 30,
                      "day_of_week": "wednesday"
                    },
                    {
                      "start_time": "2026-10-07T09:30:00-04:00",
                      "end_time": "2026-10-07T10:00:00-04:00",
                      "duration_minutes": 30,
                      "day_of_week": "wednesday"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/agents/{id}/calendars": {
      "post": {
        "tags": [
          "Calendars"
        ],
        "summary": "Assign a calendar to an agent",
        "description": "Give an agent a calendar it can check and book on, the same as adding it in the agent editor. Posting an already-assigned calendar updates its tool name/description.\n\nReturns 201 with created: true for a new assignment, 200 with created: false when updating an existing one. 400 if calendar_id isn't a UUID or tool_name has invalid characters; 404 if the agent or calendar is not in this sub-account; 409 if tool_name is already used by another calendar on this agent.",
        "operationId": "assign-agent-calendar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "assigned": true,
                  "created": true,
                  "assignment": {
                    "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
                    "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                    "calendar_id": "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b",
                    "calendar_name": "Consultations",
                    "tool_name": "calendar_1",
                    "tool_description": "Book an appointment in Consultations",
                    "is_active": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "calendar_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Calendar to assign; must be in this sub-account."
                  },
                  "tool_name": {
                    "type": "string",
                    "description": "Name the agent sees for this calendar's booking tool: letters, digits, _ and - only, max 64 characters, unique per agent. Defaults to calendar_N for a new assignment."
                  },
                  "tool_description": {
                    "type": "string",
                    "description": "Tells the agent when to use this calendar (max 1000 characters). Defaults to \"Book an appointment in <calendar name>\" for a new assignment."
                  }
                },
                "required": [
                  "calendar_id"
                ]
              },
              "example": {
                "calendar_id": "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agents/{id}/calendars/{calendarId}": {
      "delete": {
        "tags": [
          "Calendars"
        ],
        "summary": "Remove a calendar from an agent",
        "description": "Remove a calendar from an agent. The agent can no longer check or book on it; the calendar itself is not deleted.\n\n400 if calendarId isn't a UUID; 404 if the agent is not in this sub-account or the calendar isn't assigned to it.",
        "operationId": "remove-agent-calendar",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "calendarId",
            "in": "path",
            "required": true,
            "description": "The calendar to remove.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "removed": true,
                  "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                  "calendar_id": "4e5f6a7b-8c9d-4e0f-9a1b-2c3d4e5f6a7b"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/scheduled-calls": {
      "post": {
        "tags": [
          "Scheduled Calls"
        ],
        "summary": "Schedule a call",
        "description": "Schedule an outbound AI call: once scheduled_at passes, the agent calls the contact's phone number on file. The call is billed like any outbound call when it runs.\n\nReturns 201. The status moves pending → processing → completed or failed. 400 if context is missing or too long, scheduled_at is malformed or not in the future, an id isn't a UUID, or the contact has no phone number. 404 if the agent or contact is not in this sub-account.",
        "operationId": "create-scheduled-call",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "scheduled_call": {
                    "id": "1842",
                    "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "context": "Follow up on the quote sent last week and offer a demo.",
                    "scheduled_at": "2026-10-08T14:00:00.000Z",
                    "status": "pending"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Agent that will act; must be in this sub-account."
                  },
                  "contact_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Contact to reach; must be in this sub-account."
                  },
                  "scheduled_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When to run, in the future, with a UTC offset or Z, e.g. 2026-10-08T14:00:00Z."
                  },
                  "context": {
                    "type": "string",
                    "description": "What it's about — the agent uses this to know what to say (max 4000 characters)."
                  }
                },
                "required": [
                  "agent_id",
                  "contact_id",
                  "scheduled_at",
                  "context"
                ]
              },
              "example": {
                "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                "scheduled_at": "2026-10-08T14:00:00Z",
                "context": "Follow up on the quote sent last week and offer a demo."
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Scheduled Calls"
        ],
        "summary": "List scheduled calls",
        "description": "List scheduled calls, soonest first, with optional filters.\n\n400 if contact_id or agent_id isn't a UUID, or status isn't one of the allowed values.",
        "operationId": "list-scheduled-calls",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of pending, processing, completed, failed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contact_id",
            "in": "query",
            "required": false,
            "description": "Only rows for this contact.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "agent_id",
            "in": "query",
            "required": false,
            "description": "Only rows for this agent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "scheduled_calls": [
                    {
                      "id": "1842",
                      "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "context": "Follow up on the quote sent last week and offer a demo.",
                      "scheduled_at": "2026-10-08T14:00:00.000Z",
                      "status": "pending"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/scheduled-calls/{id}": {
      "get": {
        "tags": [
          "Scheduled Calls"
        ],
        "summary": "Get a scheduled call",
        "description": "Get one scheduled call.\n\n404 if the scheduled call doesn't exist or its agent or contact is not in this sub-account.",
        "operationId": "get-scheduled-call",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The scheduled call id (a numeric string, e.g. 1842).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "scheduled_call": {
                    "id": "1842",
                    "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "context": "Follow up on the quote sent last week and offer a demo.",
                    "scheduled_at": "2026-10-08T14:00:00.000Z",
                    "status": "pending"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "tags": [
          "Scheduled Calls"
        ],
        "summary": "Cancel a scheduled call",
        "description": "Cancel a scheduled call that hasn't started yet. The row is deleted.\n\nOnly pending calls can be cancelled: 409 (with scheduled_call_status) once the call is processing, completed or failed, or if it started while you were cancelling. 404 if the scheduled call is not in this sub-account.",
        "operationId": "cancel-scheduled-call",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The scheduled call id (a numeric string, e.g. 1842).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "cancelled": true,
                  "scheduled_call_id": "1842"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/scheduled-messages": {
      "post": {
        "tags": [
          "Scheduled Messages"
        ],
        "summary": "Schedule a message",
        "description": "Schedule an AI-written follow-up: once scheduled_at passes, the agent writes a message from context and sends it to the contact through your connected messaging account. Messages are billed when sent.\n\nReturns 201. The status moves pending → processing → completed or failed; after sending, sent_message, sent_channel and sent_at are filled in, and last_error explains a failure. 400 if context is missing or too long, scheduled_at is malformed or not in the future, an id isn't a UUID, preferred_channel isn't allowed, or the contact has neither a phone number nor an email. 404 if the agent or contact is not in this sub-account.",
        "operationId": "create-scheduled-message",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "scheduled_message": {
                    "id": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
                    "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "context": "Remind them about tomorrow's appointment and ask them to confirm.",
                    "preferred_channel": "sms",
                    "scheduled_at": "2026-10-08T14:00:00.000Z",
                    "status": "pending",
                    "last_error": null,
                    "sent_message": null,
                    "sent_channel": null,
                    "sent_at": null,
                    "created_at": "2026-10-06T09:00:00.000Z",
                    "updated_at": "2026-10-06T09:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "agent_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Agent that will act; must be in this sub-account."
                  },
                  "contact_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Contact to reach; must be in this sub-account."
                  },
                  "scheduled_at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When to run, in the future, with a UTC offset or Z, e.g. 2026-10-08T14:00:00Z."
                  },
                  "context": {
                    "type": "string",
                    "description": "What it's about — the agent uses this to know what to say (max 4000 characters)."
                  },
                  "preferred_channel": {
                    "type": "string",
                    "description": "Channel to use when the contact's conversation history doesn't decide it. One of sms, email, whatsapp, instagram, facebook, live_chat. Otherwise SMS or email is used."
                  }
                },
                "required": [
                  "agent_id",
                  "contact_id",
                  "scheduled_at",
                  "context"
                ]
              },
              "example": {
                "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                "scheduled_at": "2026-10-08T14:00:00Z",
                "context": "Remind them about tomorrow's appointment and ask them to confirm.",
                "preferred_channel": "sms"
              }
            }
          }
        }
      },
      "get": {
        "tags": [
          "Scheduled Messages"
        ],
        "summary": "List scheduled messages",
        "description": "List scheduled messages, soonest first, with optional filters.\n\n400 if contact_id or agent_id isn't a UUID, or status isn't one of the allowed values.",
        "operationId": "list-scheduled-messages",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of pending, processing, completed, failed.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contact_id",
            "in": "query",
            "required": false,
            "description": "Only rows for this contact.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "agent_id",
            "in": "query",
            "required": false,
            "description": "Only rows for this agent.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "scheduled_messages": [
                    {
                      "id": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
                      "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "context": "Remind them about tomorrow's appointment and ask them to confirm.",
                      "preferred_channel": "sms",
                      "scheduled_at": "2026-10-08T14:00:00.000Z",
                      "status": "pending",
                      "last_error": null,
                      "sent_message": null,
                      "sent_channel": null,
                      "sent_at": null,
                      "created_at": "2026-10-06T09:00:00.000Z",
                      "updated_at": "2026-10-06T09:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/scheduled-messages/{id}": {
      "get": {
        "tags": [
          "Scheduled Messages"
        ],
        "summary": "Get a scheduled message",
        "description": "Get one scheduled message, including what was sent once it has run.\n\n404 if the scheduled message is not in this sub-account.",
        "operationId": "get-scheduled-message",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The scheduled message id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "scheduled_message": {
                    "id": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d",
                    "agent_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "context": "Remind them about tomorrow's appointment and ask them to confirm.",
                    "preferred_channel": "sms",
                    "scheduled_at": "2026-10-08T14:00:00.000Z",
                    "status": "pending",
                    "last_error": null,
                    "sent_message": null,
                    "sent_channel": null,
                    "sent_at": null,
                    "created_at": "2026-10-06T09:00:00.000Z",
                    "updated_at": "2026-10-06T09:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "delete": {
        "tags": [
          "Scheduled Messages"
        ],
        "summary": "Cancel a scheduled message",
        "description": "Cancel a scheduled message that hasn't started yet. The row is deleted.\n\nOnly pending messages can be cancelled: 409 (with scheduled_message_status) once the message is processing, completed or failed, or if it started while you were cancelling. 404 if the scheduled message is not in this sub-account.",
        "operationId": "cancel-scheduled-message",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The scheduled message id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "cancelled": true,
                  "scheduled_message_id": "9a0b1c2d-3e4f-4a5b-8c6d-7e8f9a0b1c2d"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/workflows": {
      "get": {
        "tags": [
          "Workflows"
        ],
        "summary": "List workflows",
        "description": "List the sub-account's workflows, newest first. Deleted workflows are not returned.\n\ntrigger_type is the first of trigger_types (null when the workflow has no trigger). With include=definition each workflow also has a definition object { trigger_config, workflow_data }. 400 if folder_id is not a UUID. 401 if unauthenticated.",
        "operationId": "list-workflows",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Set to definition to also return the workflow's definition (trigger_config and workflow_data).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "is_active",
            "in": "query",
            "required": false,
            "description": "Only active (true) or inactive (false) workflows.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "folder_id",
            "in": "query",
            "required": false,
            "description": "Only workflows in this folder.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "workflows": [
                    {
                      "id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                      "name": "New lead follow-up",
                      "description": "Text and call new leads within 5 minutes",
                      "folder_id": "6b2c3d4e-5f6a-4b7c-9d8e-0f1a2b3c4d5e",
                      "folder_name": "Lead nurture",
                      "trigger_type": "contact_created",
                      "trigger_types": [
                        "contact_created"
                      ],
                      "is_active": true,
                      "deactivated_at": null,
                      "created_at": "2026-09-20T10:00:00.000Z",
                      "updated_at": "2026-10-01T08:30:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/workflows/{id}": {
      "get": {
        "tags": [
          "Workflows"
        ],
        "summary": "Get a workflow",
        "description": "Get one workflow, optionally with its definition.\n\ndefinition is only present with include=definition. 404 if the workflow is not in this sub-account or has been deleted.",
        "operationId": "get-workflow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workflow id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "include",
            "in": "query",
            "required": false,
            "description": "Set to definition to also return the workflow's definition (trigger_config and workflow_data).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "workflow": {
                    "id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                    "name": "New lead follow-up",
                    "description": "Text and call new leads within 5 minutes",
                    "folder_id": null,
                    "folder_name": null,
                    "trigger_type": "contact_created",
                    "trigger_types": [
                      "contact_created"
                    ],
                    "is_active": true,
                    "deactivated_at": null,
                    "created_at": "2026-09-20T10:00:00.000Z",
                    "updated_at": "2026-10-01T08:30:00.000Z",
                    "definition": {
                      "trigger_config": {
                        "type": "contact_created"
                      },
                      "workflow_data": {
                        "nodes": [],
                        "edges": []
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/workflows/{id}/enroll": {
      "post": {
        "tags": [
          "Workflows"
        ],
        "summary": "Enroll contacts",
        "description": "Enroll up to 100 contacts into an active workflow. Enrollment is asynchronous: each contact gets a pending execution that the workflow engine then runs. Running the workflow can send emails and texts and place AI calls, which cost money.\n\nReturns 202 (also when every contact was skipped — enrolled is then 0). Skipping already-enrolled contacts makes retries safe. 400 if a contact id is not a UUID (invalid_contact_ids) or not in this sub-account (missing_contact_ids); nothing is enrolled in that case. 404 if the workflow is not in this sub-account. 409 if the workflow is not active.",
        "operationId": "enroll-workflow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workflow id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "enrolled": 1,
                  "skipped_contact_ids": [
                    "8d4e5f6a-7b8c-4d9e-9f0a-2b3c4d5e6f7a"
                  ],
                  "executions": [
                    {
                      "id": "7c3d4e5f-6a7b-4c8d-8e9f-1a2b3c4d5e6f",
                      "workflow_id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "status": "pending",
                      "started_at": "2026-10-05T14:00:00.000Z",
                      "completed_at": null,
                      "error_message": null,
                      "current_step_id": null,
                      "next_step_at": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contact_ids": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "1 to 100 contact ids in this sub-account. Duplicates are ignored."
                  },
                  "allow_duplicates": {
                    "type": "boolean",
                    "description": "Enroll contacts even if they are already in this workflow. Defaults to false: contacts with a pending, running, waiting, waiting_for_reply or paused execution are skipped."
                  }
                },
                "required": [
                  "contact_ids"
                ]
              },
              "example": {
                "contact_ids": [
                  "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "8d4e5f6a-7b8c-4d9e-9f0a-2b3c4d5e6f7a"
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/workflows/{id}/remove-contact": {
      "post": {
        "tags": [
          "Workflows"
        ],
        "summary": "Remove a contact",
        "description": "Remove a contact from a workflow by cancelling all of its live executions (pending, running, waiting, waiting_for_reply or paused). No further steps run for that contact.\n\nremoved is 0 when the contact has no live execution in this workflow. 400 if contact_id is not a UUID. 404 if the workflow or contact is not in this sub-account. 502 if a cancellation fails; removed and execution_ids then list the executions already cancelled.",
        "operationId": "workflow-remove-contact",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workflow id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "removed": 1,
                  "execution_ids": [
                    "7c3d4e5f-6a7b-4c8d-8e9f-1a2b3c4d5e6f"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contact_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "The contact to remove."
                  },
                  "reason": {
                    "type": "string",
                    "description": "Recorded with the cancellation (max 500 characters). Defaults to \"Contact removed from workflow via API\"."
                  }
                },
                "required": [
                  "contact_id"
                ]
              },
              "example": {
                "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                "reason": "Customer booked a call"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/workflows/{id}/activate": {
      "post": {
        "tags": [
          "Workflows"
        ],
        "summary": "Activate a workflow",
        "description": "Activate a workflow. Executions paused by a deactivation resume, so messages and calls may go out right away.\n\nIdempotent: changed is false if the workflow was already active. No request body. 404 if the workflow is not in this sub-account.",
        "operationId": "activate-workflow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workflow id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "changed": true,
                  "workflow": {
                    "id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                    "name": "New lead follow-up",
                    "description": "Text and call new leads within 5 minutes",
                    "folder_id": "6b2c3d4e-5f6a-4b7c-9d8e-0f1a2b3c4d5e",
                    "folder_name": "Lead nurture",
                    "trigger_type": "contact_created",
                    "trigger_types": [
                      "contact_created"
                    ],
                    "is_active": true,
                    "deactivated_at": null,
                    "created_at": "2026-09-20T10:00:00.000Z",
                    "updated_at": "2026-10-01T08:30:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/workflows/{id}/deactivate": {
      "post": {
        "tags": [
          "Workflows"
        ],
        "summary": "Deactivate a workflow",
        "description": "Deactivate a workflow. Its live executions are paused and new enrollments are refused until it is activated again.\n\nIdempotent: changed is false if the workflow was already inactive. No request body. 404 if the workflow is not in this sub-account.",
        "operationId": "deactivate-workflow",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workflow id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "changed": true,
                  "workflow": {
                    "id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                    "name": "New lead follow-up",
                    "description": "Text and call new leads within 5 minutes",
                    "folder_id": null,
                    "folder_name": null,
                    "trigger_type": "contact_created",
                    "trigger_types": [
                      "contact_created"
                    ],
                    "is_active": false,
                    "deactivated_at": "2026-10-05T16:00:00.000Z",
                    "created_at": "2026-09-20T10:00:00.000Z",
                    "updated_at": "2026-10-05T16:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/workflows/{id}/executions": {
      "get": {
        "tags": [
          "Workflows"
        ],
        "summary": "List executions",
        "description": "List a workflow's executions (one per enrollment), most recently started first.\n\n400 if contact_id is not a UUID or status is not one of the listed values. 404 if the workflow is not in this sub-account.",
        "operationId": "list-workflow-executions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The workflow id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "contact_id",
            "in": "query",
            "required": false,
            "description": "Only executions for this contact.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of pending, running, completed, failed, cancelled, waiting, waiting_for_reply, skipped, paused.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "executions": [
                    {
                      "id": "7c3d4e5f-6a7b-4c8d-8e9f-1a2b3c4d5e6f",
                      "workflow_id": "5a1b2c3d-4e5f-4a6b-8c7d-9e0f1a2b3c4d",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "status": "pending",
                      "started_at": "2026-10-05T14:00:00.000Z",
                      "completed_at": null,
                      "error_message": null,
                      "current_step_id": null,
                      "next_step_at": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/reminder-templates": {
      "get": {
        "tags": [
          "Reminder Templates"
        ],
        "summary": "List reminder templates",
        "description": "List the sub-account's appointment reminder templates, ordered by name.\n\nreminder_time is in minutes before the appointment. 401 if unauthenticated.",
        "operationId": "list-reminder-templates",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "reminder_templates": [
                    {
                      "id": "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7b",
                      "name": "Day-before reminder",
                      "message": "Hi! Just a reminder about your appointment tomorrow.",
                      "channels": [
                        "email",
                        "sms"
                      ],
                      "reminder_time": 1440,
                      "timezone": "America/New_York",
                      "use_contact_timezone": false,
                      "created_by": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
                      "created_at": "2026-10-01T10:00:00.000Z",
                      "updated_at": "2026-10-01T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Reminder Templates"
        ],
        "summary": "Create a reminder template",
        "description": "Create an appointment reminder template. created_by is set to the user who created the API key.\n\nReturns 201. 400 if name or message is empty, timezone is not a valid IANA name, or a field is out of range.",
        "operationId": "create-reminder-template",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "reminder_template": {
                    "id": "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7b",
                    "name": "Day-before reminder",
                    "message": "Hi! Just a reminder about your appointment tomorrow.",
                    "channels": [
                      "email",
                      "sms"
                    ],
                    "reminder_time": 1440,
                    "timezone": "America/New_York",
                    "use_contact_timezone": false,
                    "created_by": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Template name (max 200 characters)."
                  },
                  "message": {
                    "type": "string",
                    "description": "Reminder text (max 5000 characters)."
                  },
                  "channels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Any of email, sms, without repeats. Defaults to []."
                  },
                  "reminder_time": {
                    "type": "integer",
                    "description": "Minutes before the appointment; 0 to 43200 (30 days). Defaults to 1440 (24 hours)."
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA timezone name, e.g. America/New_York. Defaults to UTC."
                  },
                  "use_contact_timezone": {
                    "type": "boolean",
                    "description": "Use the contact's timezone instead of timezone. Defaults to false."
                  }
                },
                "required": [
                  "name",
                  "message"
                ]
              },
              "example": {
                "name": "Day-before reminder",
                "message": "Hi! Just a reminder about your appointment tomorrow.",
                "channels": [
                  "email",
                  "sms"
                ],
                "reminder_time": 1440,
                "timezone": "America/New_York"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/reminder-templates/{id}": {
      "get": {
        "tags": [
          "Reminder Templates"
        ],
        "summary": "Get a reminder template",
        "description": "Get one reminder template.\n\n404 if the template is not in this sub-account.",
        "operationId": "get-reminder-template",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The reminder template id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "reminder_template": {
                    "id": "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7b",
                    "name": "Day-before reminder",
                    "message": "Hi! Just a reminder about your appointment tomorrow.",
                    "channels": [
                      "email",
                      "sms"
                    ],
                    "reminder_time": 1440,
                    "timezone": "America/New_York",
                    "use_contact_timezone": false,
                    "created_by": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Reminder Templates"
        ],
        "summary": "Update a reminder template",
        "description": "Update a reminder template. Only the fields you send change.\n\nSend at least one field. 400 if name or message is empty, or timezone is not a valid IANA name. 404 if the template is not in this sub-account.",
        "operationId": "update-reminder-template",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The reminder template id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "reminder_template": {
                    "id": "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7b",
                    "name": "Day-before reminder",
                    "message": "Hi! Just a reminder about your appointment tomorrow.",
                    "channels": [
                      "sms"
                    ],
                    "reminder_time": 120,
                    "timezone": "America/New_York",
                    "use_contact_timezone": false,
                    "created_by": "9a8b7c6d-5e4f-4a3b-8c2d-1e0f9a8b7c6d",
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-04T09:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New name (max 200 characters); cannot be empty."
                  },
                  "message": {
                    "type": "string",
                    "description": "New reminder text (max 5000 characters); cannot be empty."
                  },
                  "channels": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Any of email, sms, without repeats. Replaces the current list."
                  },
                  "reminder_time": {
                    "type": "integer",
                    "description": "Minutes before the appointment; 0 to 43200 (30 days)."
                  },
                  "timezone": {
                    "type": "string",
                    "description": "IANA timezone name, e.g. America/New_York."
                  },
                  "use_contact_timezone": {
                    "type": "boolean",
                    "description": "Use the contact's timezone instead of timezone."
                  }
                }
              },
              "example": {
                "reminder_time": 120,
                "channels": [
                  "sms"
                ]
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Reminder Templates"
        ],
        "summary": "Delete a reminder template",
        "description": "Permanently delete a reminder template.\n\n404 if the template is not in this sub-account.",
        "operationId": "delete-reminder-template",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The reminder template id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "reminder_template_id": "4e5f6a7b-8c9d-4e0f-8a1b-2c3d4e5f6a7b"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/forms": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "List forms",
        "description": "List the sub-account's forms and surveys, newest first. Fields and settings are not included — get a form for those.\n\n400 if type is not form or survey. 401 if unauthenticated.",
        "operationId": "list-forms",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "One of form, survey.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "Only forms with this status, e.g. active.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "forms": [
                    {
                      "id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                      "name": "Free consultation",
                      "description": "Website contact form",
                      "status": "active",
                      "type": "form",
                      "submissions_count": 42,
                      "created_at": "2026-09-01T12:00:00.000Z",
                      "updated_at": "2026-09-15T08:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/forms/{id}": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "Get a form",
        "description": "Get one form with its fields and settings. Use the field labels as keys when submitting.\n\nfields and settings are returned exactly as configured in the form builder. 404 if the form is not in this sub-account.",
        "operationId": "get-form",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The form id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "form": {
                    "id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                    "name": "Free consultation",
                    "description": "Website contact form",
                    "status": "active",
                    "type": "form",
                    "submissions_count": 42,
                    "created_at": "2026-09-01T12:00:00.000Z",
                    "updated_at": "2026-09-15T08:00:00.000Z",
                    "fields": [
                      {
                        "id": "field_1",
                        "type": "full_name",
                        "label": "Full name",
                        "required": true
                      },
                      {
                        "id": "field_2",
                        "type": "email",
                        "label": "Email",
                        "required": true
                      },
                      {
                        "id": "field_3",
                        "type": "phone",
                        "label": "Phone",
                        "required": false
                      }
                    ],
                    "settings": {
                      "notificationEmail": "owner@example.com"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/forms/{id}/submissions": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "List form submissions",
        "description": "List a form's submissions, newest first.\n\ndata is keyed by field label. contact_id is null when the submission had no email or phone. 404 if the form is not in this sub-account.",
        "operationId": "list-form-submissions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The form id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "submissions": [
                    {
                      "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "form_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "data": {
                        "Full name": "Jane Doe",
                        "Email": "jane.doe@example.com",
                        "Phone": "+14155550123"
                      },
                      "metadata": {
                        "ip": "",
                        "userAgent": "curl/8.0",
                        "referrerUrl": "",
                        "pageUrl": "",
                        "submittedAt": "2026-10-05T15:00:00.000Z",
                        "via": "public_api"
                      },
                      "status": "complete",
                      "source": "platform",
                      "created_at": "2026-10-05T15:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/forms/{id}/submit": {
      "post": {
        "tags": [
          "Forms"
        ],
        "summary": "Submit a form",
        "description": "Submit a form on a lead's behalf, exactly like a visitor would. The contact is matched by email, then phone, or created (lead source form). The submission fires the form's \"form submitted\" workflows, which can send messages and place AI calls that cost money, and sends the form's notification email if one is set.\n\nReturns 201. An existing contact is not updated. With no email or phone the submission is saved without a contact (contact_id null). Answers to fields linked to custom fields are saved on the contact. 400 if data is not an object, or with missing_fields when required fields are missing. 404 if the form is not in this sub-account. 409 if the form is not active, or is a survey (surveys can't be submitted through this endpoint).",
        "operationId": "submit-form",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The form id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "$ref": "#/components/parameters/XWebhookSource"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "submission": {
                    "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                    "form_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "data": {
                      "Full name": "Jane Doe",
                      "Email": "jane.doe@example.com",
                      "Phone": "+14155550123"
                    },
                    "metadata": {
                      "ip": "",
                      "userAgent": "curl/8.0",
                      "referrerUrl": "",
                      "pageUrl": "",
                      "submittedAt": "2026-10-05T15:00:00.000Z",
                      "via": "public_api"
                    },
                    "status": "complete",
                    "source": "platform",
                    "created_at": "2026-10-05T15:00:00.000Z"
                  },
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "contact_created": true,
                  "notification_sent": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "data": {
                    "type": "object",
                    "description": "Answers keyed by field label (field ids also work). Required fields must be present and non-empty. Consent fields take true, under the field's label or \"GDPR Consent\" / \"A2P-10DLC Consent\"."
                  },
                  "contact": {
                    "type": "object",
                    "description": "Overrides the contact details read from the form's typed fields."
                  },
                  "contact.email": {
                    "type": "string",
                    "description": "Matched case-insensitively."
                  },
                  "contact.phone": {
                    "type": "string",
                    "description": "E.164, e.g. +14155550123. Other formats are ignored."
                  },
                  "contact.name": {
                    "type": "string",
                    "description": "Full name for a newly created contact."
                  },
                  "contact.first_name": {
                    "type": "string",
                    "description": "First name for a newly created contact."
                  },
                  "contact.last_name": {
                    "type": "string",
                    "description": "Last name for a newly created contact."
                  }
                },
                "required": [
                  "data"
                ]
              },
              "example": {
                "data": {
                  "Full name": "Jane Doe",
                  "Email": "jane.doe@example.com",
                  "Phone": "+14155550123"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/contacts/{id}/form-submissions": {
      "get": {
        "tags": [
          "Forms"
        ],
        "summary": "List a contact's submissions",
        "description": "List every form submission linked to a contact, newest first.\n\n404 if the contact is not in this sub-account.",
        "operationId": "list-contact-form-submissions",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contact id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "submissions": [
                    {
                      "id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                      "form_id": "2b3c4d5e-6f7a-4b8c-9d0e-1f2a3b4c5d6e",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "data": {
                        "Full name": "Jane Doe",
                        "Email": "jane.doe@example.com",
                        "Phone": "+14155550123"
                      },
                      "metadata": {
                        "ip": "",
                        "userAgent": "curl/8.0",
                        "referrerUrl": "",
                        "pageUrl": "",
                        "submittedAt": "2026-10-05T15:00:00.000Z",
                        "via": "public_api"
                      },
                      "status": "complete",
                      "source": "platform",
                      "created_at": "2026-10-05T15:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/agents/{id}/widget": {
      "get": {
        "tags": [
          "Voice Widget"
        ],
        "summary": "Get widget settings",
        "description": "Get an agent's website voice widget settings (orb or button), with defaults filled in.\n\nAn agent that was never configured returns the default orb shown above. Missing orb settings are filled with the defaults. 404 if the agent is not in this sub-account.",
        "operationId": "get-agent-widget",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "agent_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
                  "name": "Front desk",
                  "widget_config": {
                    "type": "orb",
                    "orb": {
                      "icon": "mic",
                      "color": "#00c8ff",
                      "size": 60,
                      "show_border": true,
                      "show_text_below": false,
                      "cta_text": "Chat with us!",
                      "text_color": "#ffffff",
                      "background_style": "dark"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Voice Widget"
        ],
        "summary": "Update widget settings",
        "description": "Update an agent's widget settings. Top-level keys you send replace the stored ones; orb, button and sandbox (including sandbox.style) are merged one level deep, so you can change a single colour. Changes apply to the live widget on every site where it's embedded.\n\nSend at least one field; unknown fields are ignored. 400 if the body is empty, a value is out of range, or type is button and no button object is stored or sent. 404 if the agent is not in this sub-account.",
        "operationId": "update-agent-widget",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "agent_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
                  "name": "Front desk",
                  "widget_config": {
                    "type": "orb",
                    "orb": {
                      "icon": "mic",
                      "color": "#7c3aed",
                      "size": 60,
                      "show_border": true,
                      "show_text_below": false,
                      "cta_text": "Talk to us",
                      "text_color": "#ffffff",
                      "background_style": "dark"
                    }
                  },
                  "updated_at": "2026-10-05T11:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "type": {
                    "type": "string",
                    "description": "One of orb, button."
                  },
                  "position": {
                    "type": "string",
                    "description": "Widget position on the page."
                  },
                  "orb": {
                    "type": "object",
                    "description": "Orb settings: icon (string), color (string), size (number, 20–400), show_border (boolean), show_text_below (boolean), cta_text (string, max 200), text_color (string), background_style (dark, light or transparent), cta_background (string), cta_background_opacity (number, 0–1), position (string)."
                  },
                  "button": {
                    "type": "object",
                    "description": "Button settings: text (string, max 200), color (string), text_color (string), border_radius (number, min 0), font_size (number, min 1), show_icon (boolean), icon (string), position (string)."
                  },
                  "sandbox": {
                    "type": "object",
                    "description": "enabled (boolean) and style { headline, subheadline, button_label, brand_color, text_color, background_color } (all strings)."
                  },
                  "live-avatar": {
                    "description": "Live avatar widget settings, stored as given."
                  },
                  "demo": {
                    "description": "Phone demo widget settings, stored as given."
                  }
                }
              },
              "example": {
                "orb": {
                  "color": "#7c3aed",
                  "cta_text": "Talk to us"
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agents/{id}/widget/embed": {
      "get": {
        "tags": [
          "Voice Widget"
        ],
        "summary": "Get embed code",
        "description": "Get the HTML snippet to paste on a website to show the agent's widget — the same iframe code the agent page copies.\n\nThe snippet only contains the agent id — no token or secret; the widget authenticates itself when it loads. The orb height follows the orb size. The widget asks visitors for microphone access. 400 if type is not one of the listed values. 404 if the agent is not in this sub-account.",
        "operationId": "get-agent-widget-embed",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The agent id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "One of orb, button, demo, live-avatar. Defaults to the widget's configured type (orb if unset).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "agent_id": "0f1e2d3c-4b5a-4968-8776-655443322110",
                  "type": "orb",
                  "widget_url": "https://app.example.com/widget?agent=0f1e2d3c-4b5a-4968-8776-655443322110&type=orb",
                  "embed_code": "<!-- Centerfy AI Voice Widget (Orb) -->\n  <iframe\n    src=\"https://app.example.com/widget?agent=0f1e2d3c-4b5a-4968-8776-655443322110&type=orb\"\n    allow=\"microphone\"\n    style=\"background: transparent; width: 100%; height: 120px; border: none;\"\n    id=\"centerfyOrbWidget\"\n  ></iframe>"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/outbound-webhooks": {
      "get": {
        "tags": [
          "Outbound Webhook Subscriptions"
        ],
        "summary": "List subscriptions",
        "description": "List the sub-account's outbound webhook subscriptions, oldest first. Secrets are never returned here — only has_secret.\n\nIncludes subscriptions made in Settings → Webhooks (one event each). 400 if event is not a supported event. 401 if unauthenticated.",
        "operationId": "list-outbound-webhooks",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "event",
            "in": "query",
            "required": false,
            "description": "Only subscriptions that include this event. One of contact.created, contact.updated, contact.deleted, contact.tag_added, contact.tag_removed, contact.unsubscribed, contact.resubscribed, contact.assigned, contact.lead_score_changed, contact.custom_fields_updated, appointment.created, appointment.updated, appointment.deleted, appointment.confirmed, appointment.cancelled, appointment.no_show, appointment.completed, opportunity.created, opportunity.updated, opportunity.stage_changed, opportunity.status_changed, opportunity.deleted, message.received, message.sent, note.created, note.updated, note.deleted, conversation.created, contract.created, contract.sent, contract.viewed, contract.signed, contract.completed, contract.voided, invoice.created, invoice.sent, invoice.paid, invoice.voided, quote.created, quote.sent, quote.accepted, quote.declined, user.invited, user.joined, user.removed (see Part 4 for what each one sends).",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "is_active",
            "in": "query",
            "required": false,
            "description": "Only active (true) or paused (false) subscriptions.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "webhooks": [
                    {
                      "id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
                      "name": "CRM sync",
                      "url": "https://hooks.example.com/centerfy",
                      "events": [
                        "contact.created",
                        "contact.updated"
                      ],
                      "is_active": true,
                      "source": "my-crm",
                      "has_secret": true,
                      "created_at": "2026-10-01T10:00:00.000Z",
                      "updated_at": "2026-10-01T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Outbound Webhook Subscriptions"
        ],
        "summary": "Create a subscription",
        "description": "Subscribe a URL to one or more events. From then on, each matching change in this sub-account is POSTed to the URL as JSON. A secret token is generated and returned in this response only — store it now; it is sent with every delivery as Authorization: Bearer <token> and X-Webhook-Token.\n\nReturns 201. secret_token is a 64-character hex string, shown only here and by rotate-secret. Subscriptions with more than one event are delivered normally but don't appear in Settings → Webhooks. 400 if the url is not a public http(s) URL or an event is not supported.",
        "operationId": "create-outbound-webhook",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "webhook": {
                    "id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
                    "name": "CRM sync",
                    "url": "https://hooks.example.com/centerfy",
                    "events": [
                      "contact.created",
                      "contact.updated"
                    ],
                    "is_active": true,
                    "source": "my-crm",
                    "has_secret": true,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  },
                  "secret_token": "0000000000000000000000000000000000000000000000000000000000000000"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "Public http(s) URL (max 2048 characters). Private, loopback and internal hosts and URLs with credentials are rejected."
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "At least one, without repeats. Any of contact.created, contact.updated, contact.deleted, contact.tag_added, contact.tag_removed, contact.unsubscribed, contact.resubscribed, contact.assigned, contact.lead_score_changed, contact.custom_fields_updated, appointment.created, appointment.updated, appointment.deleted, appointment.confirmed, appointment.cancelled, appointment.no_show, appointment.completed, opportunity.created, opportunity.updated, opportunity.stage_changed, opportunity.status_changed, opportunity.deleted, message.received, message.sent, note.created, note.updated, note.deleted, conversation.created, contract.created, contract.sent, contract.viewed, contract.signed, contract.completed, contract.voided, invoice.created, invoice.sent, invoice.paid, invoice.voided, quote.created, quote.sent, quote.accepted, quote.declined, user.invited, user.joined, user.removed (see Part 4 for what each one sends)."
                  },
                  "name": {
                    "type": "string",
                    "description": "Label (max 200 characters). Defaults to \"Contact webhook\"."
                  },
                  "is_active": {
                    "type": "boolean",
                    "description": "Defaults to true. false creates the subscription paused."
                  },
                  "source": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Your system's label (max 100 characters). Changes you make with the X-Webhook-Source header set to this value are not sent back to this URL."
                  },
                  "generate_secret": {
                    "type": "boolean",
                    "description": "Defaults to true. false creates the subscription without a secret: secret_token is null and deliveries carry no token headers."
                  }
                },
                "required": [
                  "url",
                  "events"
                ]
              },
              "example": {
                "name": "CRM sync",
                "url": "https://hooks.example.com/centerfy",
                "events": [
                  "contact.created",
                  "contact.updated"
                ],
                "source": "my-crm"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/outbound-webhooks/{id}": {
      "get": {
        "tags": [
          "Outbound Webhook Subscriptions"
        ],
        "summary": "Get a subscription",
        "description": "Get one subscription. The secret is not returned — only has_secret.\n\n404 if the subscription is not in this sub-account.",
        "operationId": "get-outbound-webhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The outbound webhook subscription id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "webhook": {
                    "id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
                    "name": "CRM sync",
                    "url": "https://hooks.example.com/centerfy",
                    "events": [
                      "contact.created",
                      "contact.updated"
                    ],
                    "is_active": true,
                    "source": "my-crm",
                    "has_secret": true,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Outbound Webhook Subscriptions"
        ],
        "summary": "Update a subscription",
        "description": "Update a subscription. Only the fields you send change. Set is_active to false to pause deliveries without deleting the subscription.\n\nThe secret can't be changed here — use rotate-secret. 400 if no field is sent, name is empty, the url is not a public http(s) URL, or an event is not supported. 404 if the subscription is not in this sub-account.",
        "operationId": "update-outbound-webhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The outbound webhook subscription id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "webhook": {
                    "id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
                    "name": "CRM sync",
                    "url": "https://hooks.example.com/centerfy",
                    "events": [
                      "contact.created",
                      "contact.updated"
                    ],
                    "is_active": false,
                    "source": "my-crm",
                    "has_secret": true,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-04T12:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "New label (max 200 characters); cannot be empty."
                  },
                  "url": {
                    "type": "string",
                    "description": "New public http(s) URL (max 2048 characters)."
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Replaces the event list. At least one, without repeats."
                  },
                  "is_active": {
                    "type": "boolean",
                    "description": "Pause (false) or resume (true) deliveries."
                  },
                  "source": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Loop-prevention label (max 100 characters); null or empty clears it."
                  }
                }
              },
              "example": {
                "is_active": false
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Outbound Webhook Subscriptions"
        ],
        "summary": "Delete a subscription",
        "description": "Delete a subscription. Deliveries stop immediately and its delivery history is deleted too.\n\n404 if the subscription is not in this sub-account.",
        "operationId": "delete-outbound-webhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The outbound webhook subscription id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "webhook_id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/outbound-webhooks/{id}/rotate-secret": {
      "post": {
        "tags": [
          "Outbound Webhook Subscriptions"
        ],
        "summary": "Rotate the secret",
        "description": "Replace the subscription's secret token with a new one. The new token is returned in this response only, and the old one stops being sent immediately — update your receiver first.\n\nAlso adds a secret to a subscription created without one. No request body. 404 if the subscription is not in this sub-account.",
        "operationId": "rotate-outbound-webhook-secret",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The outbound webhook subscription id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "webhook": {
                    "id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
                    "name": "CRM sync",
                    "url": "https://hooks.example.com/centerfy",
                    "events": [
                      "contact.created",
                      "contact.updated"
                    ],
                    "is_active": true,
                    "source": "my-crm",
                    "has_secret": true,
                    "created_at": "2026-10-01T10:00:00.000Z",
                    "updated_at": "2026-10-01T10:00:00.000Z"
                  },
                  "secret_token": "1111111111111111111111111111111111111111111111111111111111111111"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/outbound-webhooks/{id}/test": {
      "post": {
        "tags": [
          "Outbound Webhook Subscriptions"
        ],
        "summary": "Send a test event",
        "description": "Send an example payload for an event to the subscription's URL right now, with the same token headers real deliveries carry plus X-Webhook-Test: true. Works even while the subscription is paused.\n\ndelivered is true when your endpoint answers 2xx within 10 seconds; error is only present when it isn't. Redirects are not followed. The test is not recorded in deliveries. The request itself returns 200 either way. 400 if no valid event is given or the stored url is not a public http(s) URL. 404 if the subscription is not in this sub-account.",
        "operationId": "test-outbound-webhook",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The outbound webhook subscription id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "webhook_id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
                  "event": "contact.created",
                  "delivered": false,
                  "response_status": 500,
                  "error": "endpoint responded 500"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "event": {
                    "type": "string",
                    "description": "Event to simulate; any supported event. Defaults to the subscription's first event."
                  }
                }
              },
              "example": {
                "event": "contact.created"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/outbound-webhooks/{id}/deliveries": {
      "get": {
        "tags": [
          "Outbound Webhook Subscriptions"
        ],
        "summary": "List deliveries",
        "description": "List the subscription's delivery attempts, newest first, with the payload sent and your endpoint's response.\n\nFailed deliveries are retried automatically until attempts reaches max_attempts; next_retry_at is when the next retry is due. 400 if status is not one of the listed values. 404 if the subscription is not in this sub-account.",
        "operationId": "list-outbound-webhook-deliveries",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The outbound webhook subscription id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of pending, success, failed, unknown.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "deliveries": [
                    {
                      "id": "8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e",
                      "webhook_id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
                      "event": "contact.created",
                      "target_url": "https://hooks.example.com/centerfy",
                      "payload": {
                        "event": "contact.created",
                        "occurred_at": "2026-10-05T09:00:00.000Z",
                        "sub_account_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
                        "organization_id": "ffffffff-1111-4222-8333-444444444444",
                        "contact": {
                          "id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                          "name": "Jane Doe",
                          "email": "jane.doe@example.com"
                        }
                      },
                      "status": "failed",
                      "response_status": 500,
                      "response_body": "Internal Server Error",
                      "error": null,
                      "attempts": 7,
                      "max_attempts": 7,
                      "next_retry_at": null,
                      "created_at": "2026-10-05T09:00:00.000Z",
                      "delivered_at": null
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/outbound-webhooks/deliveries/{id}/redeliver": {
      "post": {
        "tags": [
          "Outbound Webhook Subscriptions"
        ],
        "summary": "Redeliver a delivery",
        "description": "Queue a failed or unknown delivery to be sent again within a few seconds. The stored payload is re-sent to the stored URL with the subscription's current secret. If the delivery had used all its attempts, one more is allowed.\n\nReturns 202; the send itself happens asynchronously — list deliveries to see the outcome. No request body. 404 if the delivery is not in this sub-account. 409 if the delivery is not failed or unknown, or changed while being queued (fetch it and try again).",
        "operationId": "redeliver-outbound-webhook-delivery",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The delivery id (from List deliveries).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "queued": true,
                  "delivery": {
                    "id": "8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e",
                    "webhook_id": "6f7a8b9c-0d1e-4f2a-8b3c-4d5e6f7a8b9c",
                    "event": "contact.created",
                    "target_url": "https://hooks.example.com/centerfy",
                    "payload": {
                      "event": "contact.created",
                      "occurred_at": "2026-10-05T09:00:00.000Z",
                      "sub_account_id": "aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee",
                      "organization_id": "ffffffff-1111-4222-8333-444444444444",
                      "contact": {
                        "id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                        "name": "Jane Doe",
                        "email": "jane.doe@example.com"
                      }
                    },
                    "status": "failed",
                    "response_status": 500,
                    "response_body": "Internal Server Error",
                    "error": null,
                    "attempts": 7,
                    "max_attempts": 8,
                    "next_retry_at": "2026-10-05T12:00:00.000Z",
                    "created_at": "2026-10-05T09:00:00.000Z",
                    "delivered_at": null
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/users": {
      "get": {
        "tags": [
          "Team Users"
        ],
        "summary": "List team members",
        "description": "List the sub-account's active team members, oldest first, with their role and profile name and email.\n\nOnly active members are listed. email, first_name and last_name are null if the user has no profile. 401 if unauthenticated.",
        "operationId": "list-team-users",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "users": [
                    {
                      "user_id": "8c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
                      "email": "owner@example.com",
                      "first_name": "Alex",
                      "last_name": "Morgan",
                      "role": "sub_account_owner",
                      "is_active": true,
                      "joined_at": "2026-09-01T09:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/users/{id}/role": {
      "patch": {
        "tags": [
          "Team Users"
        ],
        "summary": "Change a member's role",
        "description": "Change a member's sub-account role.\n\nIf the member already has the role, returns changed: false without previous_role. 400 if role is not one of the four values. 403 if the member holds an agency-level role (manage it from the agency workspace). 409 if this would demote the last active sub_account_owner. 404 if the user is not an active member of this sub-account.",
        "operationId": "update-team-user-role",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The member's user id (user_id from List team members).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "user_id": "8c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
                  "role": "sub_account_admin",
                  "previous_role": "sub_account_user",
                  "changed": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "role": {
                    "type": "string",
                    "description": "New role. One of sub_account_owner, sub_account_admin, sub_account_user, sub_account_read_only."
                  }
                },
                "required": [
                  "role"
                ]
              },
              "example": {
                "role": "sub_account_admin"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/users/{id}": {
      "delete": {
        "tags": [
          "Team Users"
        ],
        "summary": "Remove a member",
        "description": "Remove a member from the sub-account. Their login itself is not deleted — they just lose access to this sub-account.\n\n403 if the member holds an agency-level role. 409 if they are the last active sub_account_owner. 404 if the user is not an active member of this sub-account.",
        "operationId": "remove-team-user",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The member's user id (user_id from List team members).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "removed": true,
                  "user_id": "8c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/users/invite": {
      "post": {
        "tags": [
          "Team Users"
        ],
        "summary": "Invite a user",
        "description": "Invite someone to the sub-account. Sends an invitation email that is valid for 7 days. Inviting an email that already has an invitation here refreshes that invitation (new link, new 7-day expiry, new role) and emails it again.\n\nReturns 201; resent is true when an earlier invitation for the email was refreshed. The invitation link token is never returned. 400 if email is not a valid address or role is invalid. 403 with current_users and max_users when the sub-account's user limit is reached. 409 with user_id if the email already belongs to an active member; 409 if the API key has no owning user. 502 with invitation if the invitation was saved but the email could not be sent — retry with Resend an invitation.",
        "operationId": "invite-team-user",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "invitation": {
                    "id": "5a1e2b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b",
                    "email": "jamie@example.com",
                    "role": "sub_account_user",
                    "first_name": "Jamie",
                    "last_name": "Rivera",
                    "status": "pending",
                    "expires_at": "2026-10-13T10:00:00.000Z",
                    "accepted_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z"
                  },
                  "resent": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Email to invite; max 320 characters. Stored lowercased."
                  },
                  "role": {
                    "type": "string",
                    "description": "Role granted on acceptance. One of sub_account_owner, sub_account_admin, sub_account_user, sub_account_read_only."
                  },
                  "first_name": {
                    "type": "string",
                    "description": "Invitee's first name; max 100 characters."
                  },
                  "last_name": {
                    "type": "string",
                    "description": "Invitee's last name; max 100 characters."
                  }
                },
                "required": [
                  "email",
                  "role"
                ]
              },
              "example": {
                "email": "jamie@example.com",
                "role": "sub_account_user",
                "first_name": "Jamie",
                "last_name": "Rivera"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/invitations": {
      "get": {
        "tags": [
          "Team Users"
        ],
        "summary": "List invitations",
        "description": "List invitations that have not been accepted yet, newest first.\n\nstatus is pending or expired (accepted invitations are not listed). 401 if unauthenticated.",
        "operationId": "list-team-invitations",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "include_expired",
            "in": "query",
            "required": false,
            "description": "Set to false to list only invitations that have not expired.",
            "schema": {
              "type": "boolean",
              "default": "true"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "invitations": [
                    {
                      "id": "5a1e2b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b",
                      "email": "jamie@example.com",
                      "role": "sub_account_user",
                      "first_name": "Jamie",
                      "last_name": "Rivera",
                      "status": "pending",
                      "expires_at": "2026-10-13T10:00:00.000Z",
                      "accepted_at": null,
                      "created_at": "2026-10-06T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/invitations/{id}/resend": {
      "post": {
        "tags": [
          "Team Users"
        ],
        "summary": "Resend an invitation",
        "description": "Send the invitation email again. An expired invitation first gets a new link and a fresh 7-day expiry.\n\n409 if the invitation was already accepted. 502 with invitation if the email could not be sent. 404 if the invitation is not in this sub-account.",
        "operationId": "resend-team-invitation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invitation id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "invitation": {
                    "id": "5a1e2b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b",
                    "email": "jamie@example.com",
                    "role": "sub_account_user",
                    "first_name": "Jamie",
                    "last_name": "Rivera",
                    "status": "pending",
                    "expires_at": "2026-10-13T10:00:00.000Z",
                    "accepted_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z"
                  },
                  "resent": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/invitations/{id}": {
      "delete": {
        "tags": [
          "Team Users"
        ],
        "summary": "Cancel an invitation",
        "description": "Cancel an invitation. Its link stops working immediately.\n\nWorks on accepted invitations too (it deletes the record; the member keeps access). 404 if the invitation is not in this sub-account.",
        "operationId": "cancel-team-invitation",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invitation id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "invitation_id": "5a1e2b3c-4d5e-4f60-8a7b-9c0d1e2f3a4b"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/campaigns": {
      "get": {
        "tags": [
          "Campaigns"
        ],
        "summary": "List campaigns",
        "description": "List SMS and email campaigns, newest first.\n\nOnly sms and email campaigns are returned. subject is null for SMS campaigns. 400 if status or campaign_type is not an allowed value.",
        "operationId": "list-campaigns",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of draft, scheduled, running, completed, failed, paused.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "campaign_type",
            "in": "query",
            "required": false,
            "description": "One of sms, email.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "campaigns": [
                    {
                      "id": "4b7e1c2d-3a4f-4e5d-9c8b-7a6f5e4d3c2b",
                      "name": "October promo",
                      "campaign_type": "email",
                      "status": "draft",
                      "subject": "20% off this week",
                      "message_content": "<p>Hi there, our October sale is on.</p>",
                      "contact_ids": [
                        "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                        "6a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                      ],
                      "smart_list_id": null,
                      "start_date": null,
                      "business_hours_enabled": false,
                      "business_hours_config": null,
                      "progress": 0,
                      "contacted": 0,
                      "success": 0,
                      "pending": 0,
                      "opened_count": 0,
                      "clicked_count": 0,
                      "unsubscribed_count": 0,
                      "created_at": "2026-10-06T10:00:00.000Z",
                      "updated_at": "2026-10-06T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Campaigns"
        ],
        "summary": "Create a campaign",
        "description": "Create a campaign. It is always created as a draft — nothing is sent until you start or schedule it.\n\nReturns 201. 400 if name or message_content is empty, subject is missing (email) or present (SMS), both contact_ids and smart_list_id are given, a contact or the smart list is not in this sub-account, or business_hours_config is invalid.",
        "operationId": "create-campaign",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "campaign": {
                    "id": "4b7e1c2d-3a4f-4e5d-9c8b-7a6f5e4d3c2b",
                    "name": "October promo",
                    "campaign_type": "email",
                    "status": "draft",
                    "subject": "20% off this week",
                    "message_content": "<p>Hi there, our October sale is on.</p>",
                    "contact_ids": [
                      "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "6a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    ],
                    "smart_list_id": null,
                    "start_date": null,
                    "business_hours_enabled": false,
                    "business_hours_config": null,
                    "progress": 0,
                    "contacted": 0,
                    "success": 0,
                    "pending": 0,
                    "opened_count": 0,
                    "clicked_count": 0,
                    "unsubscribed_count": 0,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Campaign name; max 200 characters."
                  },
                  "campaign_type": {
                    "type": "string",
                    "description": "One of sms, email. Cannot be changed later."
                  },
                  "message_content": {
                    "type": "string",
                    "description": "SMS text, or the email body (HTML); max 100,000 characters."
                  },
                  "subject": {
                    "type": "string",
                    "description": "Required for email campaigns — a single line, max 500 characters. 400 if sent for an SMS campaign."
                  },
                  "contact_ids": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Recipients — contacts in this sub-account; max 10,000, duplicates removed. Use this or smart_list_id, not both."
                  },
                  "smart_list_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Send to the members of this smart list (resolved when the campaign is sent). Use this or contact_ids, not both."
                  },
                  "business_hours_enabled": {
                    "type": "boolean",
                    "description": "Only send within business_hours_config. Defaults to false."
                  },
                  "business_hours_config": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "description": "{ timezone, monday … sunday }. timezone is an IANA name (max 64 chars); each day is { enabled: boolean, start: \"HH:MM\", end: \"HH:MM\" } and start/end are required and start must be before end when enabled."
                  }
                },
                "required": [
                  "name",
                  "campaign_type",
                  "message_content"
                ]
              },
              "example": {
                "name": "October promo",
                "campaign_type": "email",
                "subject": "20% off this week",
                "message_content": "<p>Hi there, our October sale is on.</p>",
                "contact_ids": [
                  "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                  "6a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/campaigns/{id}": {
      "get": {
        "tags": [
          "Campaigns"
        ],
        "summary": "Get a campaign",
        "description": "Get one SMS or email campaign, including its delivery counters.\n\n404 if the campaign is not in this sub-account.",
        "operationId": "get-campaign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "campaign": {
                    "id": "4b7e1c2d-3a4f-4e5d-9c8b-7a6f5e4d3c2b",
                    "name": "October promo",
                    "campaign_type": "email",
                    "status": "draft",
                    "subject": "20% off this week",
                    "message_content": "<p>Hi there, our October sale is on.</p>",
                    "contact_ids": [
                      "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "6a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    ],
                    "smart_list_id": null,
                    "start_date": null,
                    "business_hours_enabled": false,
                    "business_hours_config": null,
                    "progress": 0,
                    "contacted": 0,
                    "success": 0,
                    "pending": 0,
                    "opened_count": 0,
                    "clicked_count": 0,
                    "unsubscribed_count": 0,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Campaigns"
        ],
        "summary": "Update a campaign",
        "description": "Update a draft or scheduled campaign — only the fields you send change. Set contact_ids or smart_list_id to null to switch recipient source.\n\nSend at least one field. campaign_type cannot be changed. 409 unless the campaign is draft or scheduled. 400 if a field is empty, subject is sent for an SMS campaign, the result would have both contact_ids and smart_list_id, a contact or smart list is not in this sub-account, or business_hours_config is invalid. 404 if the campaign is not in this sub-account.",
        "operationId": "update-campaign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "campaign": {
                    "id": "4b7e1c2d-3a4f-4e5d-9c8b-7a6f5e4d3c2b",
                    "name": "October promo",
                    "campaign_type": "email",
                    "status": "draft",
                    "subject": "20% off this week",
                    "message_content": "<p>Hi there, our October sale is on.</p>",
                    "contact_ids": [
                      "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "6a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    ],
                    "smart_list_id": null,
                    "start_date": null,
                    "business_hours_enabled": false,
                    "business_hours_config": null,
                    "progress": 0,
                    "contacted": 0,
                    "success": 0,
                    "pending": 0,
                    "opened_count": 0,
                    "clicked_count": 0,
                    "unsubscribed_count": 0,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Campaign name; max 200 characters."
                  },
                  "message_content": {
                    "type": "string",
                    "description": "SMS text, or the email body (HTML); max 100,000 characters."
                  },
                  "subject": {
                    "type": "string",
                    "description": "Email campaigns only — a single line, max 500 characters. 400 if sent for an SMS campaign."
                  },
                  "contact_ids": {
                    "type": [
                      "array",
                      "null"
                    ],
                    "items": {
                      "type": "string",
                      "format": "uuid"
                    },
                    "description": "Recipients — contacts in this sub-account; max 10,000, duplicates removed. Use this or smart_list_id, not both."
                  },
                  "smart_list_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Send to the members of this smart list (resolved when the campaign is sent). Use this or contact_ids, not both."
                  },
                  "business_hours_enabled": {
                    "type": "boolean",
                    "description": "Only send within business_hours_config. Defaults to false."
                  },
                  "business_hours_config": {
                    "type": [
                      "object",
                      "null"
                    ],
                    "description": "{ timezone, monday … sunday }. timezone is an IANA name (max 64 chars); each day is { enabled: boolean, start: \"HH:MM\", end: \"HH:MM\" } and start/end are required and start must be before end when enabled."
                  }
                }
              },
              "example": {
                "subject": "Last chance: 20% off",
                "business_hours_enabled": true,
                "business_hours_config": {
                  "timezone": "America/Los_Angeles",
                  "monday": {
                    "enabled": true,
                    "start": "09:00",
                    "end": "17:00"
                  }
                }
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Campaigns"
        ],
        "summary": "Delete a campaign",
        "description": "Permanently delete a campaign.\n\n409 while the campaign is running. 404 if the campaign is not in this sub-account.",
        "operationId": "delete-campaign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "campaign_id": "4b7e1c2d-3a4f-4e5d-9c8b-7a6f5e4d3c2b"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/campaigns/{id}/start": {
      "post": {
        "tags": [
          "Campaigns"
        ],
        "summary": "Send a campaign now",
        "description": "Start sending a draft or scheduled campaign immediately to its contact_ids, or to the smart list's current members. Real messages go out and usage is charged to the wallet.\n\nReturns 200 with the updated campaign and a send summary (summary may be null) when the send finishes quickly. A large send that is still going after about 8 seconds returns 202 with { campaign_id, campaign_status: \"running\", recipients, accepted: true } and continues in the background — poll Get a campaign or Get campaign stats. 400 if the content (subject/body) is missing or there are no recipients. 402 if the wallet balance is too low (nothing is sent and the campaign is left as it was). 409 unless the campaign is draft or scheduled, or if it changed state while starting. 502 with campaign_id if the send failed; the campaign is marked failed. 404 if the campaign is not in this sub-account.",
        "operationId": "start-campaign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "202": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "campaign": {
                    "id": "4b7e1c2d-3a4f-4e5d-9c8b-7a6f5e4d3c2b",
                    "name": "October promo",
                    "campaign_type": "email",
                    "status": "completed",
                    "subject": "20% off this week",
                    "message_content": "<p>Hi there, our October sale is on.</p>",
                    "contact_ids": [
                      "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "6a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    ],
                    "smart_list_id": null,
                    "start_date": "2026-10-06T10:05:00.000Z",
                    "business_hours_enabled": false,
                    "business_hours_config": null,
                    "progress": 100,
                    "contacted": 2,
                    "success": 2,
                    "pending": 0,
                    "opened_count": 0,
                    "clicked_count": 0,
                    "unsubscribed_count": 0,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  },
                  "summary": {
                    "total": 2,
                    "sent": 2,
                    "failed": 0
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/campaigns/{id}/schedule": {
      "post": {
        "tags": [
          "Campaigns"
        ],
        "summary": "Schedule a campaign",
        "description": "Schedule a draft campaign, or reschedule a scheduled one, to send automatically at a future time. Messages are sent and charged to the wallet when it goes out.\n\n400 if start_date is not a valid datetime or is not in the future (use Send a campaign now instead), the content is missing, or neither contact_ids nor smart_list_id is set. 409 unless the campaign is draft or scheduled, or if it changed state meanwhile. 404 if the campaign is not in this sub-account.",
        "operationId": "schedule-campaign",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "campaign": {
                    "id": "4b7e1c2d-3a4f-4e5d-9c8b-7a6f5e4d3c2b",
                    "name": "October promo",
                    "campaign_type": "email",
                    "status": "scheduled",
                    "subject": "20% off this week",
                    "message_content": "<p>Hi there, our October sale is on.</p>",
                    "contact_ids": [
                      "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "6a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                    ],
                    "smart_list_id": null,
                    "start_date": "2026-10-10T16:00:00.000Z",
                    "business_hours_enabled": false,
                    "business_hours_config": null,
                    "progress": 0,
                    "contacted": 0,
                    "success": 0,
                    "pending": 0,
                    "opened_count": 0,
                    "clicked_count": 0,
                    "unsubscribed_count": 0,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "start_date": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When to send; must be in the future."
                  }
                },
                "required": [
                  "start_date"
                ]
              },
              "example": {
                "start_date": "2026-10-10T16:00:00.000Z"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/campaigns/{id}/stats": {
      "get": {
        "tags": [
          "Campaigns"
        ],
        "summary": "Get campaign stats",
        "description": "Delivery and engagement stats for a campaign. Rates are percentages rounded to one decimal.\n\nsuccess_rate is delivered ÷ recipients and delivery_rate is contacted ÷ recipients. engagement is null for SMS campaigns; for email its rates are relative to delivered. 404 if the campaign is not in this sub-account.",
        "operationId": "get-campaign-stats",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The campaign id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "campaign_id": "4b7e1c2d-3a4f-4e5d-9c8b-7a6f5e4d3c2b",
                  "campaign_type": "email",
                  "campaign_status": "completed",
                  "progress": 100,
                  "recipients": 2,
                  "contacted": 2,
                  "delivered": 2,
                  "failed": 0,
                  "pending": 0,
                  "success_rate": 100,
                  "delivery_rate": 100,
                  "engagement": {
                    "opened": 1,
                    "clicked": 1,
                    "unsubscribed": 0,
                    "open_rate": 50,
                    "click_rate": 50,
                    "unsubscribe_rate": 0
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contract-templates": {
      "get": {
        "tags": [
          "Contract Templates"
        ],
        "summary": "List contract templates",
        "description": "List contract templates, newest first. Use Get a contract template for the content and field placements.\n\nsource_type is builder, pdf or docx. recipient_roles are the placeholders you fill with recipients[].role_key when creating a contract. has_prepared_pdf must be true for contracts from the template to be sendable via the API. 401 if unauthenticated.",
        "operationId": "list-contract-templates",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "templates": [
                    {
                      "id": "2e4f6a8b-1c3d-4e5f-9a7b-8c9d0e1f2a3b",
                      "name": "Service agreement",
                      "description": "Standard 12-month service agreement",
                      "source_type": "pdf",
                      "recipient_roles": [
                        {
                          "key": "client",
                          "label": "Client",
                          "role": "signer",
                          "signing_order": 1
                        },
                        {
                          "key": "manager",
                          "label": "Account manager",
                          "role": "cc",
                          "signing_order": 2
                        }
                      ],
                      "has_prepared_pdf": true,
                      "created_at": "2026-09-20T10:00:00.000Z",
                      "updated_at": "2026-09-20T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contract-templates/{id}": {
      "get": {
        "tags": [
          "Contract Templates"
        ],
        "summary": "Get a contract template",
        "description": "Get one contract template, including its builder content and field placements.\n\nField coordinates (x, y, w, h) are percentages of the PDF page and page is 1-based; type is signature, initials, date_signed, date, text or checkbox. On templates, recipientId is a role key. 404 if the template is not in this sub-account.",
        "operationId": "get-contract-template",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contract template id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "template": {
                    "id": "2e4f6a8b-1c3d-4e5f-9a7b-8c9d0e1f2a3b",
                    "name": "Service agreement",
                    "description": "Standard 12-month service agreement",
                    "source_type": "pdf",
                    "recipient_roles": [
                      {
                        "key": "client",
                        "label": "Client",
                        "role": "signer",
                        "signing_order": 1
                      },
                      {
                        "key": "manager",
                        "label": "Account manager",
                        "role": "cc",
                        "signing_order": 2
                      }
                    ],
                    "has_prepared_pdf": true,
                    "content": [],
                    "fields": [
                      {
                        "id": "a1b2c3d4-e5f6-4a7b-8c9d-0e1f2a3b4c5d",
                        "type": "signature",
                        "page": 1,
                        "x": 12.5,
                        "y": 80,
                        "w": 25,
                        "h": 6,
                        "recipientId": "client",
                        "required": true
                      }
                    ],
                    "created_at": "2026-09-20T10:00:00.000Z",
                    "updated_at": "2026-09-20T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contracts": {
      "get": {
        "tags": [
          "Contracts"
        ],
        "summary": "List contracts",
        "description": "List contracts, newest first, each with its recipients in signing order.\n\nList items omit content and fields. Signing links and signer field values are never returned. 400 if contact_id or template_id is not a UUID, or status is not an allowed value.",
        "operationId": "list-contracts",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of draft, sent, viewed, partially_signed, completed, declined, voided.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contact_id",
            "in": "query",
            "required": false,
            "description": "Only contracts for this contact.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "template_id",
            "in": "query",
            "required": false,
            "description": "Only contracts created from this template.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "contracts": [
                    {
                      "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
                      "title": "Service agreement — Jamie Rivera",
                      "contract_number": "CTR-0042",
                      "status": "draft",
                      "source_type": "pdf",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "template_id": "2e4f6a8b-1c3d-4e5f-9a7b-8c9d0e1f2a3b",
                      "sequential_signing": false,
                      "has_prepared_pdf": true,
                      "has_signed_pdf": false,
                      "verification_code": null,
                      "expires_at": null,
                      "sent_at": null,
                      "completed_at": null,
                      "declined_at": null,
                      "voided_at": null,
                      "recipients": [
                        {
                          "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
                          "name": "Jamie Rivera",
                          "email": "jamie@example.com",
                          "role": "signer",
                          "signing_order": 1,
                          "status": "pending",
                          "viewed_at": null,
                          "signed_at": null,
                          "declined_at": null,
                          "decline_reason": null
                        },
                        {
                          "id": "8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e",
                          "name": "Account manager",
                          "email": "",
                          "role": "cc",
                          "signing_order": 2,
                          "status": "pending",
                          "viewed_at": null,
                          "signed_at": null,
                          "declined_at": null,
                          "decline_reason": null
                        }
                      ],
                      "created_at": "2026-10-06T10:00:00.000Z",
                      "updated_at": "2026-10-06T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Contracts"
        ],
        "summary": "Create a contract",
        "description": "Create a draft contract from a template: copies its title, content, field placements and prepared PDF, and creates one recipient per template role. Nothing is emailed until you send it.\n\nReturns 201. blockers lists what still prevents sending (no prepared PDF, no signers, a signer without an email or without a signature field) and ready_to_send is true when it is empty. Builder templates without a prepared PDF can only be sent from the app. 400 if template_id or contact_id is not a UUID, a role_key isn't one of the template's roles, or a recipient's name or email is missing/invalid. 404 if the template or contact is not in this sub-account.",
        "operationId": "create-contract",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contract": {
                    "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
                    "title": "Service agreement — Jamie Rivera",
                    "contract_number": "CTR-0042",
                    "status": "draft",
                    "source_type": "pdf",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "template_id": "2e4f6a8b-1c3d-4e5f-9a7b-8c9d0e1f2a3b",
                    "sequential_signing": false,
                    "has_prepared_pdf": true,
                    "has_signed_pdf": false,
                    "verification_code": null,
                    "expires_at": null,
                    "sent_at": null,
                    "completed_at": null,
                    "declined_at": null,
                    "voided_at": null,
                    "content": [],
                    "fields": [
                      {
                        "id": "f0e1d2c3-b4a5-4968-8776-655443322110",
                        "type": "signature",
                        "page": 1,
                        "x": 12.5,
                        "y": 80,
                        "w": 25,
                        "h": 6,
                        "recipientId": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
                        "required": true
                      }
                    ],
                    "recipients": [
                      {
                        "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
                        "name": "Jamie Rivera",
                        "email": "jamie@example.com",
                        "role": "signer",
                        "signing_order": 1,
                        "status": "pending",
                        "viewed_at": null,
                        "signed_at": null,
                        "declined_at": null,
                        "decline_reason": null
                      },
                      {
                        "id": "8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e",
                        "name": "Account manager",
                        "email": "",
                        "role": "cc",
                        "signing_order": 2,
                        "status": "pending",
                        "viewed_at": null,
                        "signed_at": null,
                        "declined_at": null,
                        "decline_reason": null
                      }
                    ],
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  },
                  "ready_to_send": true,
                  "blockers": []
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "template_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Template to create the contract from."
                  },
                  "title": {
                    "type": "string",
                    "description": "Contract title. Defaults to the template name."
                  },
                  "contact_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "CRM contact in this sub-account. Also fills the first signer role (name and email) unless that role is given in recipients."
                  },
                  "recipients": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Fill template roles (max 50). role_key must match a recipient_roles[].key; name and email are required. Roles you don't fill keep the role label as name and an empty email."
                  }
                },
                "required": [
                  "template_id"
                ]
              },
              "example": {
                "template_id": "2e4f6a8b-1c3d-4e5f-9a7b-8c9d0e1f2a3b",
                "title": "Service agreement — Jamie Rivera",
                "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                "recipients": [
                  {
                    "role_key": "client",
                    "name": "Jamie Rivera",
                    "email": "jamie@example.com"
                  }
                ]
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/contracts/{id}": {
      "get": {
        "tags": [
          "Contracts"
        ],
        "summary": "Get a contract",
        "description": "Get one contract with its content, field placements, recipients and signing status.\n\nready_to_send and blockers are included only while the contract is a draft. Recipient status is pending, viewed, signed or declined. 404 if the contract is not in this sub-account.",
        "operationId": "get-contract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contract id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contract": {
                    "id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
                    "title": "Service agreement — Jamie Rivera",
                    "contract_number": "CTR-0042",
                    "status": "draft",
                    "source_type": "pdf",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "template_id": "2e4f6a8b-1c3d-4e5f-9a7b-8c9d0e1f2a3b",
                    "sequential_signing": false,
                    "has_prepared_pdf": true,
                    "has_signed_pdf": false,
                    "verification_code": null,
                    "expires_at": null,
                    "sent_at": null,
                    "completed_at": null,
                    "declined_at": null,
                    "voided_at": null,
                    "content": [],
                    "fields": [
                      {
                        "id": "f0e1d2c3-b4a5-4968-8776-655443322110",
                        "type": "signature",
                        "page": 1,
                        "x": 12.5,
                        "y": 80,
                        "w": 25,
                        "h": 6,
                        "recipientId": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
                        "required": true
                      }
                    ],
                    "recipients": [
                      {
                        "id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
                        "name": "Jamie Rivera",
                        "email": "jamie@example.com",
                        "role": "signer",
                        "signing_order": 1,
                        "status": "pending",
                        "viewed_at": null,
                        "signed_at": null,
                        "declined_at": null,
                        "decline_reason": null
                      },
                      {
                        "id": "8b9c0d1e-2f3a-4b4c-9d5e-6f7a8b9c0d1e",
                        "name": "Account manager",
                        "email": "",
                        "role": "cc",
                        "signing_order": 2,
                        "status": "pending",
                        "viewed_at": null,
                        "signed_at": null,
                        "declined_at": null,
                        "decline_reason": null
                      }
                    ],
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  },
                  "ready_to_send": true,
                  "blockers": []
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contracts/{id}/send": {
      "post": {
        "tags": [
          "Contracts"
        ],
        "summary": "Send for signature",
        "description": "Send the contract for signature: marks it sent and emails a signing link to every signer who hasn't signed (with sequential signing, only the first signing-order group). Can also re-send a contract that is already sent.\n\nThe body is optional. Only draft or sent contracts can be sent (409 otherwise). 400 if base_url is not a valid https URL, or the contract has no prepared PDF, no signers, a field assigned to a deleted recipient, or a signer without a signature field. 502 with results if no email could be delivered — the contract is still marked sent, so use Remind signers to retry. 404 if the contract is not in this sub-account.",
        "operationId": "send-contract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contract id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contract_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
                  "action": "send",
                  "results": [
                    {
                      "email": "jamie@example.com",
                      "success": true
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "message": {
                    "type": "string",
                    "description": "Personal message included in the email; max 2000 characters."
                  },
                  "base_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Origin for the signing links, e.g. your white-label app domain. Only the origin is used; must be https. Defaults to the Centerfy app URL."
                  }
                }
              },
              "example": {
                "message": "Hi Jamie, please review and sign when you get a chance."
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/contracts/{id}/remind": {
      "post": {
        "tags": [
          "Contracts"
        ],
        "summary": "Remind signers",
        "description": "Email a reminder to signers who haven't signed yet (with sequential signing, only the current signing-order group), or to one recipient.\n\nThe body is optional. Check results — a reminder can return 200 even if an individual email failed. 409 unless the contract is sent, viewed or partially_signed, if no signers are pending, or if that recipient can't be reminded right now (already signed or waiting on an earlier signer). 400 if recipient_id is not a UUID or base_url is invalid. 404 if the contract or recipient is not in this sub-account.",
        "operationId": "remind-contract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contract id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contract_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
                  "action": "remind",
                  "results": [
                    {
                      "email": "jamie@example.com",
                      "success": true
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "recipient_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Remind only this recipient."
                  },
                  "base_url": {
                    "type": "string",
                    "format": "uri",
                    "description": "Origin for the signing links, e.g. your white-label app domain. Only the origin is used; must be https. Defaults to the origin used when the contract was sent."
                  }
                }
              },
              "example": {
                "recipient_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/contracts/{id}/void": {
      "post": {
        "tags": [
          "Contracts"
        ],
        "summary": "Void a contract",
        "description": "Void a contract. Its signing links stop working. This can't be undone.\n\nNo emails are sent. 409 if the contract is already completed or voided. 404 if the contract is not in this sub-account.",
        "operationId": "void-contract",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contract id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contract_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
                  "action": "void"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contracts/{id}/audit": {
      "get": {
        "tags": [
          "Contracts"
        ],
        "summary": "Get the audit trail",
        "description": "Get the contract's audit trail, oldest event first.\n\nevent_type is one of created, updated, sent, reminder_sent, link_opened, viewed, signed, declined, completed, voided, downloaded. ip_address, user_agent and location are recorded for signer actions. 404 if the contract is not in this sub-account.",
        "operationId": "get-contract-audit",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contract id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contract_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
                  "total": 2,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "events": [
                    {
                      "id": "d4e5f6a7-b8c9-4d0e-8f1a-2b3c4d5e6f70",
                      "event_type": "created",
                      "recipient_id": null,
                      "actor": "api",
                      "ip_address": null,
                      "user_agent": null,
                      "location": null,
                      "metadata": {
                        "via": "api",
                        "template_id": "2e4f6a8b-1c3d-4e5f-9a7b-8c9d0e1f2a3b"
                      },
                      "created_at": "2026-10-06T10:00:00.000Z"
                    },
                    {
                      "id": "e5f6a7b8-c9d0-4e1f-9a2b-3c4d5e6f7081",
                      "event_type": "sent",
                      "recipient_id": "7a8b9c0d-1e2f-4a3b-8c4d-5e6f7a8b9c0d",
                      "actor": "api",
                      "ip_address": null,
                      "user_agent": null,
                      "location": null,
                      "metadata": {
                        "email_success": true
                      },
                      "created_at": "2026-10-06T10:05:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/contracts/{id}/pdf": {
      "get": {
        "tags": [
          "Contracts"
        ],
        "summary": "Download the signed PDF",
        "description": "Get a short-lived download link for the signed PDF.\n\nThe link expires after 300 seconds — request a new one each time. 409 if there is no signed PDF yet (it is created once every signer has signed). 404 if the contract is not in this sub-account.",
        "operationId": "get-contract-pdf",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The contract id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "contract_id": "c1d2e3f4-a5b6-4c7d-8e9f-0a1b2c3d4e5f",
                  "url": "https://storage.example.com/contracts/signed-contract.pdf?token=example",
                  "expires_in": 300,
                  "expires_at": "2026-10-06T12:05:00.000Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/reviews": {
      "get": {
        "tags": [
          "Reviews"
        ],
        "summary": "List reviews",
        "description": "List reviews imported from your connected review platforms, most recently reviewed first. Read-only.\n\nReplying to reviews is not available through the API. 400 if a filter is not an allowed value.",
        "operationId": "list-reviews",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "platform",
            "in": "query",
            "required": false,
            "description": "One of google, facebook, trustpilot.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "rating",
            "in": "query",
            "required": false,
            "description": "Only reviews with this star rating, 1–5.",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of new, replied, ignored.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "replied",
            "in": "query",
            "required": false,
            "description": "true for status replied only; false for everything else.",
            "schema": {
              "type": "boolean"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "reviews": [
                    {
                      "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
                      "platform": "google",
                      "reviewer_name": "Sam Lee",
                      "reviewer_avatar_url": null,
                      "rating": 5,
                      "review_text": "Fast, friendly and on time. Highly recommend!",
                      "review_url": "https://reviews.example.com/r/12345",
                      "reviewed_at": "2026-10-04T15:20:00.000Z",
                      "status": "replied",
                      "ai_draft": null,
                      "reply_text": "Thanks so much, Sam!",
                      "replied_at": "2026-10-05T09:00:00.000Z",
                      "ai_generated": false,
                      "posted_to_platform": true,
                      "created_at": "2026-10-04T16:00:00.000Z",
                      "updated_at": "2026-10-05T09:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/reviews/{id}": {
      "get": {
        "tags": [
          "Reviews"
        ],
        "summary": "Get a review",
        "description": "Get one review.\n\n404 if the review is not in this sub-account.",
        "operationId": "get-review",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The review id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "review": {
                    "id": "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e",
                    "platform": "google",
                    "reviewer_name": "Sam Lee",
                    "reviewer_avatar_url": null,
                    "rating": 5,
                    "review_text": "Fast, friendly and on time. Highly recommend!",
                    "review_url": "https://reviews.example.com/r/12345",
                    "reviewed_at": "2026-10-04T15:20:00.000Z",
                    "status": "replied",
                    "ai_draft": null,
                    "reply_text": "Thanks so much, Sam!",
                    "replied_at": "2026-10-05T09:00:00.000Z",
                    "ai_generated": false,
                    "posted_to_platform": true,
                    "created_at": "2026-10-04T16:00:00.000Z",
                    "updated_at": "2026-10-05T09:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/review-requests": {
      "get": {
        "tags": [
          "Review Requests"
        ],
        "summary": "List review requests",
        "description": "List review requests that were sent or attempted (from the app or the API), newest first.\n\nstatus is sent or failed; failed attempts carry an error_message. 400 if contact_id is not a UUID or channel is not an allowed value.",
        "operationId": "list-review-requests",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "contact_id",
            "in": "query",
            "required": false,
            "description": "Only requests sent to this contact.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "channel",
            "in": "query",
            "required": false,
            "description": "One of sms, email.",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "review_requests": [
                    {
                      "id": "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "contact_name": "Jamie Rivera",
                      "channel": "sms",
                      "status": "sent",
                      "message_content": "Hi Jamie, thanks for choosing Example Plumbing! We'd love your feedback — it only takes a minute: https://reviews.example.com/write",
                      "review_link": "https://reviews.example.com/write",
                      "error_message": null,
                      "sent_by": "8c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
                      "created_at": "2026-10-06T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Review Requests"
        ],
        "summary": "Send a review request",
        "description": "Send a contact a “please review us” SMS or email right away, using the review link and message templates from your Reviews settings. A real message is sent; SMS usage is charged to the wallet.\n\nReturns 201. The review link is your custom link from Reviews settings, else the preferred (or any) connected platform's review page. Every attempt is logged in review requests. 400 if contact_id is not a UUID, no review link is configured, or the contact has no phone/email or has unsubscribed from that channel. 402 with review_request if the wallet balance is too low. 502 with review_request (status failed) if the message could not be sent. 404 if the contact is not in this sub-account.",
        "operationId": "send-review-request",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "review_request": {
                    "id": "c3d4e5f6-a7b8-4c9d-8e0f-2a3b4c5d6e7f",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "contact_name": "Jamie Rivera",
                    "channel": "sms",
                    "status": "sent",
                    "message_content": "Hi Jamie, thanks for choosing Example Plumbing! We'd love your feedback — it only takes a minute: https://reviews.example.com/write",
                    "review_link": "https://reviews.example.com/write",
                    "error_message": null,
                    "sent_by": "8c2d3e4f-5a6b-4c7d-8e9f-0a1b2c3d4e5f",
                    "created_at": "2026-10-06T10:00:00.000Z"
                  },
                  "channel": "sms",
                  "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contact_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Contact in this sub-account to send to."
                  },
                  "channel": {
                    "type": "string",
                    "description": "One of sms, email."
                  }
                },
                "required": [
                  "contact_id",
                  "channel"
                ]
              },
              "example": {
                "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                "channel": "sms"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/invoices": {
      "get": {
        "tags": [
          "Invoices"
        ],
        "summary": "List invoices",
        "description": "List invoices, newest first, with optional filters.\n\nAmounts are in dollars. 400 if contact_id or quote_id is not a UUID, or status is not an allowed value.",
        "operationId": "list-invoices",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of pending, paid, void, refunded.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contact_id",
            "in": "query",
            "required": false,
            "description": "Only invoices for this contact.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "quote_id",
            "in": "query",
            "required": false,
            "description": "Only invoices generated from this quote.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "invoices": [
                    {
                      "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                      "invoice_number": "INV-2026-0012",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "quote_id": null,
                      "amount": 1575,
                      "status": "pending",
                      "issued_at": "2026-10-06T10:00:00.000Z",
                      "due_date": "2026-11-05",
                      "paid_at": null,
                      "payment_method": null,
                      "notes": "Net 30",
                      "items": [
                        {
                          "item": "Website redesign",
                          "quantity": 1,
                          "amount": 1500
                        },
                        {
                          "item": "Monthly hosting",
                          "quantity": 3,
                          "amount": 25
                        }
                      ],
                      "payment_url": null,
                      "payment_link_expires_at": null,
                      "created_at": "2026-10-06T10:00:00.000Z",
                      "updated_at": "2026-10-06T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Create an invoice",
        "description": "Create a pending invoice from line items. The invoice number (INV-YYYY-NNNN) is assigned automatically. Nothing is emailed.\n\nReturns 201. 400 if contact_id is not a UUID, an item is invalid, or due_date is not a valid date. 404 if the contact is not in this sub-account. Payment links for invoices can't be created through the API.",
        "operationId": "create-invoice",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "invoice": {
                    "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                    "invoice_number": "INV-2026-0012",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "quote_id": null,
                    "amount": 1575,
                    "status": "pending",
                    "issued_at": "2026-10-06T10:00:00.000Z",
                    "due_date": "2026-11-05",
                    "paid_at": null,
                    "payment_method": null,
                    "notes": "Net 30",
                    "items": [
                      {
                        "item": "Website redesign",
                        "quantity": 1,
                        "amount": 1500
                      },
                      {
                        "item": "Monthly hosting",
                        "quantity": 3,
                        "amount": 25
                      }
                    ],
                    "payment_url": null,
                    "payment_link_expires_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contact_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Contact in this sub-account to bill."
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Line items (1–200). item: non-empty string; quantity: integer ≥ 1; amount: unit price, number ≥ 0.01. The total is the sum of amount × quantity, rounded to cents."
                  },
                  "due_date": {
                    "type": "string",
                    "format": "date",
                    "description": "Due date. Defaults to 30 days from today."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Notes shown on the invoice."
                  }
                },
                "required": [
                  "contact_id",
                  "items"
                ]
              },
              "example": {
                "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                "items": [
                  {
                    "item": "Website redesign",
                    "quantity": 1,
                    "amount": 1500
                  },
                  {
                    "item": "Monthly hosting",
                    "quantity": 3,
                    "amount": 25
                  }
                ],
                "due_date": "2026-11-05",
                "notes": "Net 30"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/invoices/{id}": {
      "get": {
        "tags": [
          "Invoices"
        ],
        "summary": "Get an invoice",
        "description": "Get one invoice.\n\n404 if the invoice is not in this sub-account.",
        "operationId": "get-invoice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "invoice": {
                    "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                    "invoice_number": "INV-2026-0012",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "quote_id": null,
                    "amount": 1575,
                    "status": "pending",
                    "issued_at": "2026-10-06T10:00:00.000Z",
                    "due_date": "2026-11-05",
                    "paid_at": null,
                    "payment_method": null,
                    "notes": "Net 30",
                    "items": [
                      {
                        "item": "Website redesign",
                        "quantity": 1,
                        "amount": 1500
                      },
                      {
                        "item": "Monthly hosting",
                        "quantity": 3,
                        "amount": 25
                      }
                    ],
                    "payment_url": null,
                    "payment_link_expires_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Invoices"
        ],
        "summary": "Update an invoice",
        "description": "Update an invoice — only the fields you send change. Changing the line items recalculates the amount, and a changed amount clears the existing payment link.\n\npayment_link_cleared is true when a changed amount removed an existing payment link. 400 if nothing to update, contact_id is not a UUID, an item is invalid or due_date is not a valid date. 409 if you try to set a paid invoice back to pending. 404 if the invoice or contact is not in this sub-account.",
        "operationId": "update-invoice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "invoice": {
                    "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                    "invoice_number": "INV-2026-0012",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "quote_id": null,
                    "amount": 1575,
                    "status": "void",
                    "issued_at": "2026-10-06T10:00:00.000Z",
                    "due_date": "2026-11-05",
                    "paid_at": null,
                    "payment_method": null,
                    "notes": "Net 30",
                    "items": [
                      {
                        "item": "Website redesign",
                        "quantity": 1,
                        "amount": 1500
                      },
                      {
                        "item": "Monthly hosting",
                        "quantity": 3,
                        "amount": 25
                      }
                    ],
                    "payment_url": null,
                    "payment_link_expires_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  },
                  "payment_link_cleared": false
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contact_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Bill a different contact in this sub-account."
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Replaces all line items. Line items (1–200). item: non-empty string; quantity: integer ≥ 1; amount: unit price, number ≥ 0.01. The total is the sum of amount × quantity, rounded to cents."
                  },
                  "due_date": {
                    "type": "string",
                    "format": "date",
                    "description": "New due date."
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Notes; null or empty clears them."
                  },
                  "status": {
                    "type": "string",
                    "description": "One of pending, void, refunded. Use Mark an invoice paid to set paid."
                  }
                }
              },
              "example": {
                "status": "void"
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Invoices"
        ],
        "summary": "Delete an invoice",
        "description": "Permanently delete a void invoice. A quote it was generated from stays, unlinked from the invoice.\n\n409 unless the invoice status is void — set it to void with Update an invoice first. 404 if the invoice is not in this sub-account.",
        "operationId": "delete-invoice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "invoice_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/invoices/{id}/mark-paid": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Mark an invoice paid",
        "description": "Record a payment received outside Centerfy: sets the invoice to paid and adds its amount to the contact's revenue. No money is moved.\n\nThe body is optional. contact_revenue_updated is false if the invoice has no contact or the revenue update failed (the invoice is still marked paid). 409 if the invoice is already paid or is void. 404 if the invoice is not in this sub-account.",
        "operationId": "mark-invoice-paid",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "invoice": {
                    "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                    "invoice_number": "INV-2026-0012",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "quote_id": null,
                    "amount": 1575,
                    "status": "paid",
                    "issued_at": "2026-10-06T10:00:00.000Z",
                    "due_date": "2026-11-05",
                    "paid_at": "2026-10-08T14:00:00.000Z",
                    "payment_method": "check",
                    "notes": "Net 30",
                    "items": [
                      {
                        "item": "Website redesign",
                        "quantity": 1,
                        "amount": 1500
                      },
                      {
                        "item": "Monthly hosting",
                        "quantity": 3,
                        "amount": 25
                      }
                    ],
                    "payment_url": null,
                    "payment_link_expires_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  },
                  "contact_revenue_updated": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "payment_method": {
                    "type": "string",
                    "description": "How it was paid, e.g. cash or check. Defaults to manual."
                  }
                }
              },
              "example": {
                "payment_method": "check"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/invoices/{id}/send": {
      "post": {
        "tags": [
          "Invoices"
        ],
        "summary": "Email an invoice",
        "description": "Email the invoice to its contact, including its line items, total, due date and payment link (if any). The invoice's status is not changed.\n\n409 if the invoice is void. 400 if the invoice's contact has no email address. 502 if the email could not be sent. 404 if the invoice is not in this sub-account.",
        "operationId": "send-invoice",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The invoice id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "sent": true,
                  "invoice_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                  "to": "jamie@example.com"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/quotes": {
      "get": {
        "tags": [
          "Quotes"
        ],
        "summary": "List quotes",
        "description": "List quotes and estimates, newest first.\n\ntype is quote or estimate (estimates are numbered EST-…). 400 if contact_id is not a UUID or status is not an allowed value.",
        "operationId": "list-quotes",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "status",
            "in": "query",
            "required": false,
            "description": "One of open, sent, accepted, won, rejected, archived.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contact_id",
            "in": "query",
            "required": false,
            "description": "Only quotes for this contact.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 1,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "quotes": [
                    {
                      "id": "f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c",
                      "quote_number": "QUO-2026-007",
                      "type": "quote",
                      "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                      "title": "Website project",
                      "description": "Redesign and three months of hosting",
                      "status": "open",
                      "total_amount": 1575,
                      "valid_until": "2026-11-05",
                      "items": [
                        {
                          "item": "Website redesign",
                          "quantity": 1,
                          "amount": 1500
                        },
                        {
                          "item": "Monthly hosting",
                          "quantity": 3,
                          "amount": 25
                        }
                      ],
                      "notes": null,
                      "payment_link": null,
                      "invoice_id": null,
                      "invoiced_at": null,
                      "sent_at": null,
                      "accepted_at": null,
                      "rejected_at": null,
                      "created_at": "2026-10-06T10:00:00.000Z",
                      "updated_at": "2026-10-06T10:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "post": {
        "tags": [
          "Quotes"
        ],
        "summary": "Create a quote",
        "description": "Create an open quote or estimate from line items. The number (QUO-YYYY-NNN or EST-YYYY-NNN) is assigned automatically. Nothing is emailed.\n\nReturns 201. 400 if title is empty, contact_id is not a UUID, an item is invalid, or valid_until is not a valid date. 404 if the contact is not in this sub-account.",
        "operationId": "create-quote",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "quote": {
                    "id": "f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c",
                    "quote_number": "QUO-2026-007",
                    "type": "quote",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "title": "Website project",
                    "description": "Redesign and three months of hosting",
                    "status": "open",
                    "total_amount": 1575,
                    "valid_until": "2026-11-05",
                    "items": [
                      {
                        "item": "Website redesign",
                        "quantity": 1,
                        "amount": 1500
                      },
                      {
                        "item": "Monthly hosting",
                        "quantity": 3,
                        "amount": 25
                      }
                    ],
                    "notes": null,
                    "payment_link": null,
                    "invoice_id": null,
                    "invoiced_at": null,
                    "sent_at": null,
                    "accepted_at": null,
                    "rejected_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contact_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Contact in this sub-account."
                  },
                  "title": {
                    "type": "string",
                    "description": "Quote title."
                  },
                  "description": {
                    "type": "string",
                    "description": "Longer description."
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Line items (1–200). item: non-empty string; quantity: integer ≥ 1; amount: unit price, number ≥ 0.01. The total is the sum of amount × quantity, rounded to cents."
                  },
                  "valid_until": {
                    "type": "string",
                    "format": "date",
                    "description": "Expiry date. Defaults to 30 days from today."
                  },
                  "notes": {
                    "type": "string",
                    "description": "Notes shown on the quote."
                  },
                  "type": {
                    "type": "string",
                    "description": "quote or estimate. Defaults to quote."
                  }
                },
                "required": [
                  "contact_id",
                  "title",
                  "items"
                ]
              },
              "example": {
                "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                "title": "Website project",
                "description": "Redesign and three months of hosting",
                "items": [
                  {
                    "item": "Website redesign",
                    "quantity": 1,
                    "amount": 1500
                  },
                  {
                    "item": "Monthly hosting",
                    "quantity": 3,
                    "amount": 25
                  }
                ],
                "valid_until": "2026-11-05"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/quotes/{id}": {
      "get": {
        "tags": [
          "Quotes"
        ],
        "summary": "Get a quote",
        "description": "Get one quote or estimate.\n\n404 if the quote is not in this sub-account.",
        "operationId": "get-quote",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The quote or estimate id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "quote": {
                    "id": "f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c",
                    "quote_number": "QUO-2026-007",
                    "type": "quote",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "title": "Website project",
                    "description": "Redesign and three months of hosting",
                    "status": "open",
                    "total_amount": 1575,
                    "valid_until": "2026-11-05",
                    "items": [
                      {
                        "item": "Website redesign",
                        "quantity": 1,
                        "amount": 1500
                      },
                      {
                        "item": "Monthly hosting",
                        "quantity": 3,
                        "amount": 25
                      }
                    ],
                    "notes": null,
                    "payment_link": null,
                    "invoice_id": null,
                    "invoiced_at": null,
                    "sent_at": null,
                    "accepted_at": null,
                    "rejected_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      },
      "patch": {
        "tags": [
          "Quotes"
        ],
        "summary": "Update a quote",
        "description": "Update a quote — only the fields you send change. Setting status to accepted or won creates a pending invoice for the quote total (if it doesn't have one yet) and fires your quote status workflows. A changed total clears the payment link.\n\ninvoice is the generated invoice, or null when none was created. If the quote saved but invoice generation failed, you get 200 with invoice: null and a warning. Sending nothing that changes the quote returns it unchanged. 400 if title is empty, contact_id is not a UUID, an item is invalid or valid_until is not a valid date. 404 if the quote or contact is not in this sub-account.",
        "operationId": "update-quote",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The quote or estimate id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "quote": {
                    "id": "f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c",
                    "quote_number": "QUO-2026-007",
                    "type": "quote",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "title": "Website project",
                    "description": "Redesign and three months of hosting",
                    "status": "accepted",
                    "total_amount": 1575,
                    "valid_until": "2026-11-05",
                    "items": [
                      {
                        "item": "Website redesign",
                        "quantity": 1,
                        "amount": 1500
                      },
                      {
                        "item": "Monthly hosting",
                        "quantity": 3,
                        "amount": 25
                      }
                    ],
                    "notes": null,
                    "payment_link": null,
                    "invoice_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                    "invoiced_at": "2026-10-07T09:00:00.000Z",
                    "sent_at": null,
                    "accepted_at": "2026-10-07T09:00:00.000Z",
                    "rejected_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  },
                  "invoice": {
                    "id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                    "invoice_number": "INV-2026-0012",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "quote_id": "f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c",
                    "amount": 1575,
                    "status": "pending",
                    "issued_at": "2026-10-06T10:00:00.000Z",
                    "due_date": null,
                    "paid_at": null,
                    "payment_method": null,
                    "notes": null,
                    "items": [],
                    "payment_url": null,
                    "payment_link_expires_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contact_id": {
                    "type": "string",
                    "format": "uuid",
                    "description": "Move the quote to another contact in this sub-account."
                  },
                  "title": {
                    "type": "string",
                    "description": "Quote title; cannot be empty."
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Description; null or empty clears it."
                  },
                  "items": {
                    "type": "array",
                    "items": {
                      "type": "object"
                    },
                    "description": "Replaces all line items. Line items (1–200). item: non-empty string; quantity: integer ≥ 1; amount: unit price, number ≥ 0.01. The total is the sum of amount × quantity, rounded to cents."
                  },
                  "valid_until": {
                    "type": "string",
                    "format": "date",
                    "description": "New expiry date."
                  },
                  "notes": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "description": "Notes; null or empty clears them."
                  },
                  "status": {
                    "type": "string",
                    "description": "One of open, sent, accepted, won, rejected, archived. accepted, rejected and sent also stamp accepted_at, rejected_at or sent_at."
                  }
                }
              },
              "example": {
                "status": "accepted"
              }
            }
          }
        }
      },
      "delete": {
        "tags": [
          "Quotes"
        ],
        "summary": "Delete a quote",
        "description": "Permanently delete a quote. Deleting a quote also deletes every invoice generated from it.\n\n409 with invoice_count while the quote has invoices and force is not true. 404 if the quote is not in this sub-account.",
        "operationId": "delete-quote",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The quote or estimate id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "force",
            "in": "query",
            "required": false,
            "description": "Required when the quote has invoices — they are permanently deleted with it.",
            "schema": {
              "type": "boolean",
              "default": "false"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "deleted": true,
                  "quote_id": "f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c",
                  "invoices_deleted": 0
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/quotes/{id}/payment-link": {
      "post": {
        "tags": [
          "Quotes"
        ],
        "summary": "Create a payment link",
        "description": "Create a Stripe checkout link for the quote total on the sub-account's connected Stripe account. Every call also creates a new pending invoice. The link is saved on the quote and included when you email it.\n\nCall it once per quote — repeated calls create extra invoices. 400 if the quote total isn't greater than zero, no payment account is connected, or the connection has expired. 502 if the link could not be created. 404 if the quote is not in this sub-account.",
        "operationId": "create-quote-payment-link",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The quote or estimate id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "quote_id": "f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c",
                  "payment_url": "https://checkout.example.com/pay/cs_test_example",
                  "invoice_id": "e1f2a3b4-c5d6-4e7f-8a9b-0c1d2e3f4a5b",
                  "invoice_number": "INV-2026-0013"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/quotes/{id}/send": {
      "post": {
        "tags": [
          "Quotes"
        ],
        "summary": "Email a quote",
        "description": "Email the quote or estimate to its contact (with its payment link, if any) and set its status to sent.\n\n409 if the quote is archived. 400 if the quote has no contact in this sub-account or the contact has no email address. 502 if the email could not be sent (the status is not changed). 404 if the quote is not in this sub-account.",
        "operationId": "send-quote",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The quote or estimate id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "sent": true,
                  "quote": {
                    "id": "f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c",
                    "quote_number": "QUO-2026-007",
                    "type": "quote",
                    "contact_id": "3f1c2a9e-7b4d-4c8a-9e2f-1a2b3c4d5e6f",
                    "title": "Website project",
                    "description": "Redesign and three months of hosting",
                    "status": "sent",
                    "total_amount": 1575,
                    "valid_until": "2026-11-05",
                    "items": [
                      {
                        "item": "Website redesign",
                        "quantity": 1,
                        "amount": 1500
                      },
                      {
                        "item": "Monthly hosting",
                        "quantity": 3,
                        "amount": 25
                      }
                    ],
                    "notes": null,
                    "payment_link": null,
                    "invoice_id": null,
                    "invoiced_at": null,
                    "sent_at": "2026-10-06T10:30:00.000Z",
                    "accepted_at": null,
                    "rejected_at": null,
                    "created_at": "2026-10-06T10:00:00.000Z",
                    "updated_at": "2026-10-06T10:00:00.000Z"
                  },
                  "to": "jamie@example.com"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/quotes/{id}/pdf": {
      "get": {
        "tags": [
          "Quotes"
        ],
        "summary": "Get the printable quote",
        "description": "Get the printable quote document as an HTML string — the same page the app opens to print or save as PDF.\n\nDespite the path, the response is JSON containing HTML, not a PDF file. 502 if the document could not be rendered. 404 if the quote is not in this sub-account.",
        "operationId": "get-quote-document",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The quote or estimate id.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "quote_id": "f2a3b4c5-d6e7-4f8a-9b0c-1d2e3f4a5b6c",
                  "format": "html",
                  "html": "<!DOCTYPE html><html>…</html>"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/phone-numbers/available": {
      "get": {
        "tags": [
          "Phone Number Purchasing"
        ],
        "summary": "Search available numbers",
        "description": "Search US local numbers you can buy. Read-only — nothing is reserved or charged. Numbers already on the platform are left out.\n\nAvailability can change at any moment — a number listed here may be gone when you buy it. 400 if type is toll_free or a parameter is malformed. 502 if the search failed; 503 if number search is not available.",
        "operationId": "search-available-phone-numbers",
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "description": "Only US is supported.",
            "schema": {
              "type": "string",
              "default": "US"
            }
          },
          {
            "name": "area_code",
            "in": "query",
            "required": false,
            "description": "Area code, 1–3 digits, e.g. 415.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "contains",
            "in": "query",
            "required": false,
            "description": "Digits the number must contain, 1–10 digits.",
            "schema": {
              "type": "string"
            }
          },
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "local or toll_free. Toll-free numbers are not sold (400).",
            "schema": {
              "type": "string",
              "default": "local"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Maximum results; 1–100.",
            "schema": {
              "type": "integer",
              "default": 20
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "count": 2,
                  "available_numbers": [
                    {
                      "phone_number": "+14155550123",
                      "type": "local",
                      "locality": "San Francisco",
                      "region": "CA",
                      "features": [
                        "sms",
                        "voice"
                      ]
                    },
                    {
                      "phone_number": "+14155550188",
                      "type": "local",
                      "locality": "San Francisco",
                      "region": "CA",
                      "features": [
                        "voice"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        }
      }
    },
    "/webhooks/inbound/phone-numbers/purchase": {
      "post": {
        "tags": [
          "Phone Number Purchasing"
        ],
        "summary": "Buy a phone number",
        "description": "Buy a phone number for this sub-account. This costs money: the purchase is charged to the wallet (your saved card may be charged to cover it), and the number renews and is billed every 30 days until released. Optionally assign it to an agent in the same call.\n\nReturns 201. agent_assignment is present only when agent_id was sent; if assignment fails the number is still bought (assigned: false with an error) — retry with POST /webhooks/inbound/phone-numbers/:id/agent. Idempotency: always send a new Idempotency-Key per purchase and reuse it only to retry that same purchase. Same key + same body after completion replays the stored response (201 or 502) with Idempotent-Replayed: true and no new charge. 409 if the key was already used with a different body, or a request with that key is still in progress. Other errors: 400 if the Idempotency-Key header is missing or invalid, phone_number is not a US E.164 number, or agent_id is not a UUID. 402 if the wallet balance is insufficient (nothing charged). 403 if the sub-account's phone number limit is reached (with current and max) or the API key has no owning user. 404 if the agent is not in this sub-account. 409 if the number is not available to buy. 502 if availability or billing could not be confirmed (not charged), or the order failed after charging — the response says whether the charge was refunded (refunded: true/false); if it says contact support, do not retry. 503 if purchasing is not available.",
        "operationId": "purchase-phone-number",
        "parameters": [
          {
            "$ref": "#/components/parameters/IdempotencyKey"
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "purchased": true,
                  "phone_number": {
                    "id": "9f8e7d6c-5b4a-4392-8170-6f5e4d3c2b1a",
                    "phone_number": "+14155550123",
                    "purchased": true,
                    "agent_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d",
                    "is_active": true,
                    "purchased_at": "2026-10-06T10:00:00.000Z",
                    "next_billing_date": "2026-11-05T10:00:00.000Z"
                  },
                  "agent_assignment": {
                    "assigned": true,
                    "agent_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "$ref": "#/components/responses/PaymentRequired"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "phone_number": {
                    "type": "string",
                    "description": "A US local number in E.164 format, e.g. +14155550123, as returned by Search available numbers."
                  },
                  "agent_id": {
                    "type": [
                      "string",
                      "null"
                    ],
                    "format": "uuid",
                    "description": "Agent in this sub-account to assign the number to after purchase."
                  }
                },
                "required": [
                  "phone_number"
                ]
              },
              "example": {
                "phone_number": "+14155550123",
                "agent_id": "1a2b3c4d-5e6f-4a7b-8c9d-0e1f2a3b4c5d"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/phone-numbers/import": {
      "post": {
        "tags": [
          "Phone Number Purchasing"
        ],
        "summary": "Import a number (SIP)",
        "description": "Bring a number you already own from another provider, connected over SIP. Not charged. The SIP credentials are stored for outbound calling and are never returned.\n\nReturns 201. 400 if phone_number is not E.164 or the import request is invalid. 403 with current and max if the sub-account's phone number limit is reached. 409 if the number already exists on the platform. 502 if the import failed.",
        "operationId": "import-phone-number",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "imported": true,
                  "phone_number": {
                    "id": "8e7d6c5b-4a39-4281-9706-5f4e3d2c1b0a",
                    "phone_number": "+14155550142",
                    "purchased": false,
                    "agent_id": null,
                    "is_active": true,
                    "purchased_at": null,
                    "next_billing_date": null
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "phone_number": {
                    "type": "string",
                    "description": "The number in E.164 format, e.g. +14155550123."
                  },
                  "termination_uri": {
                    "type": "string",
                    "description": "Your SIP termination URI; 1–255 characters."
                  },
                  "username": {
                    "type": "string",
                    "description": "SIP username; 1–255 characters."
                  },
                  "password": {
                    "type": "string",
                    "description": "SIP password; 1–255 characters."
                  },
                  "trunk_name": {
                    "type": "string",
                    "description": "Label for the SIP trunk; max 100 characters. Defaults to Imported-<timestamp>."
                  }
                },
                "required": [
                  "phone_number",
                  "termination_uri",
                  "username",
                  "password"
                ]
              },
              "example": {
                "phone_number": "+14155550142",
                "termination_uri": "sip.example.com",
                "username": "example-user",
                "password": "example-password",
                "trunk_name": "Main office line"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agency/sub-accounts": {
      "get": {
        "tags": [
          "Agency: Sub-Accounts"
        ],
        "summary": "List sub-accounts",
        "description": "List the agency's sub-accounts, newest first. Deleted sub-accounts are never returned.\n\nList rows omit features and settings — use Get a sub-account for those. 400 if a query value fails validation. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-list-sub-accounts",
        "parameters": [
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          },
          {
            "name": "archived",
            "in": "query",
            "required": false,
            "description": "true = only archived, false = only not archived. Omit for both.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "paused",
            "in": "query",
            "required": false,
            "description": "true = only paused (any reason), false = only not paused. Omit for both.",
            "schema": {
              "type": "boolean"
            }
          },
          {
            "name": "search",
            "in": "query",
            "required": false,
            "description": "Case-insensitive substring match on the name (max 200 characters; % _ and \\ are ignored).",
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 2,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "sub_accounts": [
                    {
                      "id": "5a1f3c2e-8b4d-4e6f-9a7b-1c2d3e4f5a01",
                      "name": "Acme Roofing",
                      "slug": "acme-roofing",
                      "parent_sub_account_id": null,
                      "is_active": true,
                      "archived_at": null,
                      "paused_reason": null,
                      "paused_at": null,
                      "paused_note": null,
                      "subscription_plan": "starter",
                      "subscription_status": "active",
                      "max_users": 10,
                      "max_contacts": 1000,
                      "created_at": "2026-09-14T15:20:00.000Z",
                      "updated_at": "2026-10-01T09:00:00.000Z"
                    },
                    {
                      "id": "7c3b5e40-ad6f-4081-9c2d-3e4f5a6b7c03",
                      "name": "Bayview Dental",
                      "slug": "bayview-dental",
                      "parent_sub_account_id": null,
                      "is_active": true,
                      "archived_at": null,
                      "paused_reason": "manual_agency",
                      "paused_at": "2026-10-03T08:00:00.000Z",
                      "paused_note": "Invoice overdue",
                      "subscription_plan": null,
                      "subscription_status": null,
                      "max_users": 10,
                      "max_contacts": 1000,
                      "created_at": "2026-08-20T10:00:00.000Z",
                      "updated_at": "2026-10-03T08:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      },
      "post": {
        "tags": [
          "Agency: Sub-Accounts"
        ],
        "summary": "Create a sub-account",
        "description": "Provision a sub-account and email its owner a sub_account_owner invitation (valid 7 days). Limits and features come from the agency defaults, or from the named agency pricing plan; the agency's signup credit (if any) is added to the new wallet.\n\n201 on success. A failed invitation email does not fail the call: email_sent is false and email_error explains why. 400 if name is blank or email is invalid. 403 if the agency is inactive or has reached its sub-account limit (max_sub_accounts, default 5). 404 if plan does not match an active plan of this agency. 500 'Sub-account created but invitation failed' includes sub_account_id. GHL locationID auto-connect is not supported here — use the legacy create-subaccount-webhook for that. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-create-sub-account",
        "parameters": [],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "sub_account_id": "5a1f3c2e-8b4d-4e6f-9a7b-1c2d3e4f5a01",
                  "sub_account_slug": "acme-roofing",
                  "invitation_token": "c18e0395-f2b4-45d6-8172-8d9eafb0c108",
                  "plan": "starter",
                  "email_sent": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Sub-account name, 1–200 characters. Also used to build the slug (a base-36 suffix is added if the slug is taken)."
                  },
                  "email": {
                    "type": "string",
                    "description": "Owner email (3–320 characters); receives the invitation. Lower-cased."
                  },
                  "plan": {
                    "type": "string",
                    "description": "Name of an active agency pricing plan; sets limits, features and rebilling pricing. Omit to use the agency defaults."
                  }
                },
                "required": [
                  "name",
                  "email"
                ]
              },
              "example": {
                "name": "Acme Roofing",
                "email": "owner@example.com",
                "plan": "starter"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agency/sub-accounts/{id}": {
      "get": {
        "tags": [
          "Agency: Sub-Accounts"
        ],
        "summary": "Get a sub-account",
        "description": "Get one sub-account, including its features and settings.\n\nsettings never includes wallet_balance — read the balance from Get sub-account usage. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-get-sub-account",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "sub_account": {
                    "id": "5a1f3c2e-8b4d-4e6f-9a7b-1c2d3e4f5a01",
                    "name": "Acme Roofing",
                    "slug": "acme-roofing",
                    "parent_sub_account_id": null,
                    "is_active": true,
                    "archived_at": null,
                    "paused_reason": null,
                    "paused_at": null,
                    "paused_note": null,
                    "subscription_plan": "starter",
                    "subscription_status": "active",
                    "max_users": 10,
                    "max_contacts": 1000,
                    "created_at": "2026-09-14T15:20:00.000Z",
                    "updated_at": "2026-10-01T09:00:00.000Z",
                    "features": {
                      "crm_integration_enabled": true,
                      "ai_agents_enabled": true,
                      "workflows_enabled": true,
                      "invoicing_enabled": false,
                      "email_integration_enabled": true,
                      "phone_integration_enabled": true,
                      "llm_integration_enabled": false
                    },
                    "settings": {
                      "rebilling_enabled": true,
                      "monthly_credit_amount": 0,
                      "timezone": "America/New_York"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      },
      "patch": {
        "tags": [
          "Agency: Sub-Accounts"
        ],
        "summary": "Update sub-account settings",
        "description": "Merge keys into the sub-account's settings object. Keys you send overwrite existing ones; keys you don't send are kept.\n\nOnly settings can be changed; name, limits and features are not writable here. 400 if settings is missing or contains nothing but wallet_balance. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-update-sub-account",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "sub_account": {
                    "id": "5a1f3c2e-8b4d-4e6f-9a7b-1c2d3e4f5a01",
                    "name": "Acme Roofing",
                    "slug": "acme-roofing",
                    "parent_sub_account_id": null,
                    "is_active": true,
                    "archived_at": null,
                    "paused_reason": null,
                    "paused_at": null,
                    "paused_note": null,
                    "subscription_plan": "starter",
                    "subscription_status": "active",
                    "max_users": 10,
                    "max_contacts": 1000,
                    "created_at": "2026-09-14T15:20:00.000Z",
                    "updated_at": "2026-10-06T12:00:00.000Z",
                    "features": {
                      "crm_integration_enabled": true,
                      "ai_agents_enabled": true,
                      "workflows_enabled": true,
                      "invoicing_enabled": false,
                      "email_integration_enabled": true,
                      "phone_integration_enabled": true,
                      "llm_integration_enabled": false
                    },
                    "settings": {
                      "rebilling_enabled": true,
                      "monthly_credit_amount": 25,
                      "timezone": "America/Chicago"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "settings": {
                    "type": "object",
                    "description": "Free-form keys to merge into settings. wallet_balance is always dropped, so this endpoint can never move money."
                  }
                },
                "required": [
                  "settings"
                ]
              },
              "example": {
                "settings": {
                  "timezone": "America/Chicago",
                  "monthly_credit_amount": 25
                }
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agency/sub-accounts/{id}/archive": {
      "post": {
        "tags": [
          "Agency: Sub-Accounts"
        ],
        "summary": "Archive a sub-account",
        "description": "DESTRUCTIVE. Archive a sub-account and its direct child sub-accounts (sets archived_at, is_active = false), then release and delete their platform phone numbers and stop knowledge-base rebilling — the same flow as the agency Sub-Accounts page.\n\nReleased phone numbers are gone for good — unarchiving does not bring them back. Archived accounts are no longer billed for numbers; any number whose release failed (listed in release_failed) stays on file and is released later by the renewal job without billing. If the release step could not run at all, phone_numbers is { error, details } instead and the archive still stands. Knowledge bases are disabled and their billing turned off. No request body. 409 if the sub-account is already archived. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-archive-sub-account",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "archived": true,
                  "sub_account_ids": [
                    "5a1f3c2e-8b4d-4e6f-9a7b-1c2d3e4f5a01",
                    "6b2a4d3f-9c5e-4f70-8b1c-2d3e4f5a6b02"
                  ],
                  "phone_numbers": {
                    "platform_numbers": 2,
                    "released": 2,
                    "release_failed": []
                  },
                  "knowledge_base_billing_stopped": true
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      }
    },
    "/webhooks/inbound/agency/sub-accounts/{id}/unarchive": {
      "post": {
        "tags": [
          "Agency: Sub-Accounts"
        ],
        "summary": "Unarchive a sub-account",
        "description": "Restore an archived sub-account and its archived direct children (archived_at cleared, is_active = true).\n\nPhone numbers released at archive time are not restored, and knowledge-base billing is not re-enabled. No request body. 409 if the sub-account is not archived. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-unarchive-sub-account",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "unarchived": true,
                  "sub_account_ids": [
                    "5a1f3c2e-8b4d-4e6f-9a7b-1c2d3e4f5a01",
                    "6b2a4d3f-9c5e-4f70-8b1c-2d3e4f5a6b02"
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      }
    },
    "/webhooks/inbound/agency/sub-accounts/{id}/pause": {
      "post": {
        "tags": [
          "Agency: Sub-Accounts"
        ],
        "summary": "Pause a sub-account",
        "description": "Pause a sub-account on the agency's behalf (paused_reason = manual_agency). Its users see the paused screen in the app.\n\nThe body is optional. Pausing only gates the app UI; it does not stop agents or calls server-side. paused_by is set to the user who created the agency key. 409 if the sub-account is already paused for any reason. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-pause-sub-account",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "sub_account": {
                    "id": "5a1f3c2e-8b4d-4e6f-9a7b-1c2d3e4f5a01",
                    "name": "Acme Roofing",
                    "slug": "acme-roofing",
                    "parent_sub_account_id": null,
                    "is_active": true,
                    "archived_at": null,
                    "paused_reason": "manual_agency",
                    "paused_at": "2026-10-06T12:00:00.000Z",
                    "paused_note": "Invoice overdue",
                    "subscription_plan": "starter",
                    "subscription_status": "active",
                    "max_users": 10,
                    "max_contacts": 1000,
                    "created_at": "2026-09-14T15:20:00.000Z",
                    "updated_at": "2026-10-06T12:00:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency",
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "note": {
                    "type": "string",
                    "description": "Internal note stored as paused_note (max 1000 characters)."
                  }
                }
              },
              "example": {
                "note": "Invoice overdue"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agency/sub-accounts/{id}/unpause": {
      "post": {
        "tags": [
          "Agency: Sub-Accounts"
        ],
        "summary": "Unpause a sub-account",
        "description": "Lift an agency pause (paused_reason = manual_agency) and clear the pause fields.\n\nNo request body. 409 if the sub-account is not paused, or is paused for another reason (for example billing_failed or manual_admin) — those can't be lifted through the agency API. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-unpause-sub-account",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "sub_account": {
                    "id": "5a1f3c2e-8b4d-4e6f-9a7b-1c2d3e4f5a01",
                    "name": "Acme Roofing",
                    "slug": "acme-roofing",
                    "parent_sub_account_id": null,
                    "is_active": true,
                    "archived_at": null,
                    "paused_reason": null,
                    "paused_at": null,
                    "paused_note": null,
                    "subscription_plan": "starter",
                    "subscription_status": "active",
                    "max_users": 10,
                    "max_contacts": 1000,
                    "created_at": "2026-09-14T15:20:00.000Z",
                    "updated_at": "2026-10-06T12:30:00.000Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      }
    },
    "/webhooks/inbound/agency/sub-accounts/{id}/usage": {
      "get": {
        "tags": [
          "Agency: Sub-Accounts"
        ],
        "summary": "Get sub-account usage",
        "description": "Usage counts, limits and current wallet balance of a sub-account.\n\nwallet_balance is the newest ledger balance (rounded to 2 decimals), falling back to the stored settings value. Counts are the same ones the agency Sub-Accounts page shows. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-sub-account-usage",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "sub_account_id": "5a1f3c2e-8b4d-4e6f-9a7b-1c2d3e4f5a01",
                  "usage": {
                    "contacts": 412,
                    "users": 3,
                    "phone_numbers": 2,
                    "agents": 4
                  },
                  "limits": {
                    "max_users": 10,
                    "max_contacts": 1000
                  },
                  "wallet_balance": 57.25
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      }
    },
    "/webhooks/inbound/agency/sub-accounts/{id}/users": {
      "get": {
        "tags": [
          "Agency: Sub-Account Users"
        ],
        "summary": "List members and invitations",
        "description": "List a sub-account's active members (oldest first) plus its pending invitations (unaccepted and unexpired).\n\nlimit/offset page the members only; pending_invitations is always returned in full (newest first). email and names are null if the user has no profile. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-list-sub-account-users",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size; minimum 1, maximum 100 (larger values are rejected with 400).",
            "schema": {
              "type": "integer",
              "default": 50
            }
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Number of records to skip for paging; minimum 0.",
            "schema": {
              "type": "integer",
              "default": 0
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "total": 2,
                  "limit": 50,
                  "offset": 0,
                  "has_more": false,
                  "users": [
                    {
                      "user_id": "8d4c6f51-be70-4192-8d3e-4f5a6b7c8d04",
                      "email": "owner@example.com",
                      "first_name": "Jordan",
                      "last_name": "Lee",
                      "role": "sub_account_owner",
                      "created_at": "2026-09-14T15:30:00.000Z"
                    },
                    {
                      "user_id": "9e5d7062-cf81-42a3-9e4f-5a6b7c8d9e05",
                      "email": "sam@example.com",
                      "first_name": "Sam",
                      "last_name": "Rivera",
                      "role": "sub_account_user",
                      "created_at": "2026-09-20T10:00:00.000Z"
                    }
                  ],
                  "pending_invitations": [
                    {
                      "id": "af6e8173-d092-43b4-af50-6b7c8d9eaf06",
                      "email": "alex@example.com",
                      "first_name": "Alex",
                      "last_name": "Kim",
                      "role": "sub_account_admin",
                      "created_at": "2026-10-04T09:00:00.000Z",
                      "expires_at": "2026-10-11T09:00:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      },
      "post": {
        "tags": [
          "Agency: Sub-Account Users"
        ],
        "summary": "Invite a user",
        "description": "Invite a user to a sub-account and send the invitation email (valid 7 days). Inviting an email that already has a pending invitation refreshes it (new token and expiry) and re-sends it instead of creating a duplicate.\n\n201 on success; resent is true when an existing pending invitation was refreshed. 400 if email is invalid or role is not allowed. 403 when the sub-account is at its seat limit (max_users counts active members; a negative max_users means unlimited) — the body adds current_users and max_users. 502 if the invitation was saved but the email could not be sent; the body carries the same invitation fields plus details, and calling again re-sends it. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-invite-sub-account-user",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "message": "Invitation is sent to alex@example.com",
                  "invitation_id": "af6e8173-d092-43b4-af50-6b7c8d9eaf06",
                  "email": "alex@example.com",
                  "role": "sub_account_admin",
                  "resent": false,
                  "expires_at": "2026-10-13T12:00:00.000Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Email to invite (max 320 characters). Lower-cased."
                  },
                  "role": {
                    "type": "string",
                    "description": "One of sub_account_owner, sub_account_admin, sub_account_user."
                  },
                  "first_name": {
                    "type": "string",
                    "description": "First name (max 200 characters)."
                  },
                  "last_name": {
                    "type": "string",
                    "description": "Last name (max 200 characters)."
                  },
                  "phone": {
                    "type": "string",
                    "description": "Phone number (max 40 characters)."
                  }
                },
                "required": [
                  "email",
                  "role"
                ]
              },
              "example": {
                "email": "alex@example.com",
                "role": "sub_account_admin",
                "first_name": "Alex",
                "last_name": "Kim",
                "phone": "+14155550123"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agency/sub-accounts/{id}/users/{userId}": {
      "delete": {
        "tags": [
          "Agency: Sub-Account Users"
        ],
        "summary": "Remove a member",
        "description": "DESTRUCTIVE. Remove a member from a sub-account (deletes their membership; the user account itself is kept).\n\nPending invitations can't be removed here. 404 if userId is not a member of this sub-account. 409 when removing the last active owner. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-remove-sub-account-user",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "userId",
            "in": "path",
            "required": true,
            "description": "The member's user id (user_id from List members).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "removed": true,
                  "user_id": "9e5d7062-cf81-42a3-9e4f-5a6b7c8d9e05"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "409": {
            "$ref": "#/components/responses/Conflict"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      }
    },
    "/webhooks/inbound/agency/sub-accounts/{id}/api-keys": {
      "get": {
        "tags": [
          "Agency: Sub-Account API Keys"
        ],
        "summary": "List API keys",
        "description": "List a sub-account's API keys, newest first. Keys are masked — the full key is never returned here.\n\nkey_prefix is the first 8 characters of the key and last_four the last 4. Not paginated. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-list-sub-account-api-keys",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "api_keys": [
                    {
                      "id": "b07f9284-e1a3-44c5-b061-7c8d9eafb007",
                      "name": "Zapier",
                      "key_prefix": "cfy_Q3vX",
                      "last_four": "k9Tz",
                      "created_at": "2026-10-02T11:00:00.000Z",
                      "last_used_at": "2026-10-05T18:42:00.000Z"
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      },
      "post": {
        "tags": [
          "Agency: Sub-Account API Keys"
        ],
        "summary": "Create an API key",
        "description": "Create a sub-account API key (cfy_ followed by 32 random bytes, base64url). The full key is returned in this response only — it is not stored (only a SHA-256 hash, prefix and last four are) and can't be retrieved again.\n\n201 on success. Currently returns 503 'API key creation through the agency API is not available yet (api_keys.plaintext_key must become nullable)': the endpoint never writes the plaintext, and the database still requires api_keys.plaintext_key (NOT NULL), so the insert is refused rather than storing the key. It starts working once that column is made nullable. 400 if name is blank. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-create-sub-account-api-key",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "201": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "api_key": {
                    "id": "b07f9284-e1a3-44c5-b061-7c8d9eafb007",
                    "name": "Zapier",
                    "key_prefix": "cfy_Q3vX",
                    "last_four": "k9Tz",
                    "created_at": "2026-10-02T11:00:00.000Z",
                    "last_used_at": null
                  },
                  "key": "cfy_Q3vXexampleexampleexampleexampleexamplek9Tz",
                  "warning": "Store this key now — it cannot be retrieved again."
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "name": {
                    "type": "string",
                    "description": "Label for the key, 1–100 characters."
                  }
                },
                "required": [
                  "name"
                ]
              },
              "example": {
                "name": "Zapier"
              }
            }
          }
        }
      }
    },
    "/webhooks/inbound/agency/sub-accounts/{id}/api-keys/{keyId}": {
      "delete": {
        "tags": [
          "Agency: Sub-Account API Keys"
        ],
        "summary": "Revoke an API key",
        "description": "DESTRUCTIVE. Revoke a sub-account API key by deleting it. Integrations using it stop working immediately.\n\nThere is no revoked state — the key row is deleted and can't be restored. 404 if keyId is not a key of this sub-account. 404 when the sub-account id is not a uuid, does not exist, is deleted, or belongs to another agency. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-revoke-sub-account-api-key",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The sub-account id. Must belong to the key's agency and not be deleted.",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          },
          {
            "name": "keyId",
            "in": "path",
            "required": true,
            "description": "The API key id (id from List API keys).",
            "schema": {
              "type": "string",
              "format": "uuid"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "revoked": true,
                  "api_key_id": "b07f9284-e1a3-44c5-b061-7c8d9eafb007"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      }
    },
    "/webhooks/inbound/agency/wallet": {
      "get": {
        "tags": [
          "Agency: Agency Wallet"
        ],
        "summary": "Get the agency wallet balance",
        "description": "The agency's own (organization-level) wallet balance — not a sub-account wallet.\n\nbalance is rounded to 2 decimals. wallet_exists is false when the agency has no wallet row yet; balance then comes from the newest agency-level ledger entry, or 0. Read-only — there is no top-up endpoint. 401 when the key is missing, not cfy_-prefixed or unknown (including sub-account keys); 403 when the agency is not on a White Label or SaaS Mode plan; 429 above 50 requests per minute per agency (Retry-After header set); 500 'database error' on an internal failure.",
        "operationId": "agency-get-wallet",
        "parameters": [],
        "responses": {
          "200": {
            "description": "Success",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object"
                },
                "example": {
                  "status": "success",
                  "wallet": {
                    "balance": 1250.5,
                    "wallet_exists": true
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "429": {
            "$ref": "#/components/responses/TooManyRequests"
          }
        },
        "x-centerfy-key-type": "agency"
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyHeader": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Sub-account or agency API key (cfy_…)."
      },
      "BearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "The same API key sent as a Bearer token."
      }
    },
    "parameters": {
      "IdempotencyKey": {
        "name": "Idempotency-Key",
        "in": "header",
        "required": true,
        "description": "Unique value per purchase (e.g. a UUID). Replaying the same key + body returns the stored response with Idempotent-Replayed: true.",
        "schema": {
          "type": "string",
          "minLength": 1,
          "maxLength": 255
        }
      },
      "XWebhookSource": {
        "name": "X-Webhook-Source",
        "in": "header",
        "required": false,
        "description": "Label for your system. Changes made with this header are not echoed back to outbound webhooks whose Source matches.",
        "schema": {
          "type": "string"
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "status",
          "error"
        ],
        "properties": {
          "status": {
            "type": "string",
            "const": "error"
          },
          "error": {
            "type": "string"
          }
        },
        "additionalProperties": true
      }
    },
    "responses": {
      "BadRequest": {
        "description": "Validation failed, malformed UUID or missing required field.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing or invalid API key.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "PaymentRequired": {
        "description": "Insufficient wallet balance.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Forbidden": {
        "description": "Plan without API access, plan limit reached, or wrong key type for this endpoint.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "NotFound": {
        "description": "Not found in this sub-account (or agency).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Conflict": {
        "description": "Duplicate, wrong state, or an Idempotency-Key reuse with a different body.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "TooManyRequests": {
        "description": "Rate limit exceeded (50 requests per minute).",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        },
        "headers": {
          "Retry-After": {
            "schema": {
              "type": "integer"
            },
            "description": "Seconds to wait."
          },
          "X-RateLimit-Limit": {
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "schema": {
              "type": "integer"
            }
          }
        }
      },
      "ServerError": {
        "description": "Server or upstream provider error.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    }
  }
}
