{
  "openapi": "3.1.0",
  "info": {
    "title": "Dialerr API",
    "version": "1.0.0",
    "summary": "Push leads, read and send SMS, and embed the Dialerr softphone on your own site.",
    "description": "Dialerr is a WebRTC softphone a business embeds into its own website or app. This API has two halves, with two kinds of credential:\n\n**Data API** — push leads and read/send SMS for your organization. Authenticate with an org API token (`api_...`) created at https://dialerr.com → Settings → API Access, sent as `Authorization: Bearer api_...`.\n\n**Embed API** — provision keys and mint short-lived agent tokens so your backend can sign your agents into the embedded toolbar. Uses a Stripe-like key pair: a publishable key (`pk_...`, safe in client code) and a secret key (`sk_...`, server-side only).\n\nA human-readable guide for AI coding assistants lives at https://dialerr.com/llms.txt; developer docs at https://dialerr.com/developers.\n\nOutbound webhooks (configured in Dialerr on the Integrations page, one org URL + optional secret): `message.received` for every inbound text, `message.sent` for every outbound text (including sends through this API, carrying your `external_ref`), and `lead.disposition_set` when a rep dispositions a call (also available per disposition). Envelope `{ event, timestamp, organization_id, data }`; headers `X-Dialr-Event`, `X-Dialr-Timestamp`, `X-Dialr-Organization`, and `X-Dialr-Signature` (HMAC-SHA256 of the body) when a secret is set; three retries with exponential backoff, `data.message.id` stable across retries. Not sent: carrier delivery receipts. There is no email-sending endpoint.",
    "termsOfService": "https://dialerr.com/terms",
    "contact": {
      "name": "Dialerr",
      "url": "https://dialerr.com",
      "email": "contact@dialerr.com"
    },
    "license": {
      "name": "Proprietary",
      "url": "https://dialerr.com/terms"
    }
  },
  "servers": [
    {
      "url": "https://api.dialerr.com",
      "description": "Production"
    },
    {
      "url": "https://dialerr.com",
      "description": "Production (same API, proxied from the app domain)"
    }
  ],
  "tags": [
    {
      "name": "Leads",
      "description": "Create and import leads. Duplicates are merged by normalized phone (or by email for email-only leads), never created twice."
    },
    {
      "name": "Messaging",
      "description": "Read an SMS thread and send SMS from one of your organization's numbers. Sends are billed as normal SMS usage."
    },
    {
      "name": "Embed",
      "description": "Sign your agents into the embedded toolbar and provision keys without the dashboard."
    },
    {
      "name": "Calls",
      "description": "Start calls for your reps and read what happened on them. Built for assistants and CRMs acting on a rep's behalf."
    },
    {
      "name": "Fax",
      "description": "Send PDFs as faxes from the organization's fax line and read what happened to them. 10 cents a page; the line itself is bought by an admin in the app."
    },
    {
      "name": "AI Answering",
      "description": "Control when the AI agent answers your phones: its Voice channel, status and pickup rules, and which numbers it covers. Read with any token; changes need a token granted settings:write."
    }
  ],
  "security": [
    {
      "apiToken": []
    }
  ],
  "paths": {
    "/api/v1/leads": {
      "post": {
        "tags": [
          "Leads"
        ],
        "summary": "Create a lead",
        "operationId": "createLead",
        "description": "Creates a lead in your organization, or returns the existing one if the phone (or, for email-only leads, the email) already exists. A name is required — either `first_name`/`last_name` or a single `name` that will be split. Phone OR email is enough; a lead with neither is rejected. Unknown top-level keys are ignored — put anything unusual in `custom_fields`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/LeadInput"
              },
              "example": {
                "first_name": "John",
                "last_name": "Doe",
                "phone_number": "+13055550123",
                "email": "john@example.com",
                "company": "Acme Roofing",
                "source": "my-crm",
                "list_name": "Miami prospects",
                "custom_fields": {
                  "budget": "$25k",
                  "listing_url": "https://example.com/l/123"
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A lead with this phone (or email) already exists; it was not duplicated. If `list_name` was given, the existing lead was added to that list.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeadExists"
                }
              }
            }
          },
          "201": {
            "description": "Lead created.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LeadCreated"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      },
      "get": {
        "tags": [
          "Leads"
        ],
        "summary": "Find leads",
        "operationId": "searchLeads",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "Search your leads by name, phone or email. Use the id with the timeline, call and callback endpoints.",
        "parameters": [
          {
            "name": "search",
            "in": "query",
            "schema": {
              "type": "string"
            },
            "description": "Name, phone number or email; partial matches."
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          },
          {
            "name": "page",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 1
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Matching leads.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "leads": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Lead"
                      }
                    },
                    "pagination": {
                      "type": "object",
                      "properties": {
                        "currentPage": {
                          "type": "integer"
                        },
                        "totalPages": {
                          "type": "integer"
                        },
                        "totalCount": {
                          "type": "integer"
                        },
                        "pageSize": {
                          "type": "integer"
                        },
                        "hasNextPage": {
                          "type": "boolean"
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/leads/bulk-import": {
      "post": {
        "tags": [
          "Leads"
        ],
        "summary": "Import many leads",
        "operationId": "bulkImportLeads",
        "description": "Imports an array of leads in batches. Each item accepts the same fields as **Create a lead**. Re-importing existing leads fills in fields they were missing without overwriting hand-corrected data (`enrichedExisting`). Pass `listId` to assign every imported and already-known lead to a list.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "leads"
                ],
                "properties": {
                  "leads": {
                    "type": "array",
                    "minItems": 1,
                    "items": {
                      "$ref": "#/components/schemas/LeadInput"
                    }
                  },
                  "listId": {
                    "type": "integer",
                    "description": "Existing list id to assign every lead to. 404 if it is not in your organization."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Import finished. Counts cover every item in the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/BulkImportResult"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "`listId` does not belong to your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/conversations": {
      "get": {
        "tags": [
          "Messaging"
        ],
        "summary": "Read an SMS thread",
        "operationId": "getConversation",
        "description": "Returns the SMS messages exchanged with one phone number, oldest first. Both directions are included.",
        "parameters": [
          {
            "name": "phone",
            "in": "query",
            "required": true,
            "description": "The other party's number, E.164 preferred. Matched on the last ten digits.",
            "schema": {
              "type": "string",
              "example": "+13055550123"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Most recent N messages to return.",
            "schema": {
              "type": "integer",
              "minimum": 1,
              "maximum": 200,
              "default": 50
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The thread.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "messages"
                  ],
                  "properties": {
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/messages": {
      "post": {
        "tags": [
          "Messaging"
        ],
        "summary": "Send an SMS",
        "operationId": "sendMessage",
        "description": "Sends an SMS from one of your organization's numbers. Billed as normal SMS usage and requires wallet funds. A recipient who replied STOP is refused with 403 RECIPIENT_OPTED_OUT.\n\nA 200 means the gateway ACCEPTED the message, not that it was delivered. The carrier's verdict arrives seconds later — poll GET /api/v1/messages/{id} before telling anyone it arrived. A common failure is the sending number not being registered for the destination carrier (10DLC), which no amount of retrying fixes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to",
                  "body"
                ],
                "properties": {
                  "to": {
                    "type": "string",
                    "description": "Recipient, E.164.",
                    "example": "+13055550123"
                  },
                  "body": {
                    "type": "string",
                    "description": "Message text."
                  },
                  "from": {
                    "type": "string",
                    "description": "One of your organization's SMS-capable numbers, E.164. Defaults to the organization's primary number."
                  },
                  "external_ref": {
                    "type": "string",
                    "description": "Your own id for this message; echoed back and stored so you can correlate replies."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted for delivery.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "status"
                  ],
                  "properties": {
                    "id": {
                      "type": "string",
                      "description": "Provider message id."
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "accepted"
                      ],
                      "description": "The gateway accepted it. Delivery is a separate, later fact."
                    },
                    "external_ref": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "check": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Path to poll for the delivery outcome."
                    },
                    "from": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The number it was actually sent from."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`to` or `body` missing, `to` not a valid number (`INVALID_TO_NUMBER`), or `from` is not one of your numbers (`FROM_NOT_YOURS`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Wallet has no funds for this send.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The recipient opted out (replied STOP). `code` is `RECIPIENT_OPTED_OUT`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "`FROM_HAS_NO_SMS`: the number you named can make calls but is not set up for texting. `SMS_NOT_CONFIGURED`: the organization has no SMS-capable number at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/embed/token": {
      "post": {
        "tags": [
          "Embed"
        ],
        "summary": "Mint an agent token",
        "operationId": "mintEmbedToken",
        "description": "Called by **your backend** to sign one of your agents into the embedded toolbar. Returns a short-lived token (about 30 minutes; the widget re-mints automatically). Origin-bound: browser calls from origins not on the key's allowlist are rejected.",
        "security": [
          {
            "secretKey": []
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "description": "Identify which agent to sign in. Both are optional; omit both to use the key's default agent. The agent must belong to the key's organization.",
                "properties": {
                  "agentId": {
                    "type": "integer"
                  },
                  "agentEmail": {
                    "type": "string",
                    "format": "email"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Token minted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "token",
                    "expiresIn"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "token": {
                      "type": "string",
                      "description": "Pass to the widget as its session token."
                    },
                    "expiresIn": {
                      "type": "integer",
                      "description": "Seconds until the token expires."
                    },
                    "userData": {
                      "type": "object",
                      "description": "The signed-in agent's display details.",
                      "additionalProperties": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "description": "Missing, invalid or revoked secret key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "The calling origin is not on the key's allowlist, or the agent's account is inactive.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "More than 30 token requests per minute for this key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/embed/config": {
      "get": {
        "tags": [
          "Embed"
        ],
        "summary": "Validate a publishable key",
        "operationId": "getEmbedConfig",
        "description": "Public. Confirms a publishable key is valid and returns what the widget needs to render.",
        "security": [],
        "parameters": [
          {
            "name": "key",
            "in": "query",
            "required": true,
            "description": "Publishable key.",
            "schema": {
              "type": "string",
              "pattern": "^pk_",
              "example": "pk_live_xxx"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Key is valid.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "const": true
                    },
                    "name": {
                      "type": "string",
                      "description": "The key's label."
                    },
                    "orgName": {
                      "type": "string"
                    },
                    "allowedOrigins": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Origins the key may be used from. Supports wildcards such as `https://*.example.com`."
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Unknown or revoked key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/embed/device/code": {
      "post": {
        "tags": [
          "Embed"
        ],
        "summary": "Start key provisioning (device flow)",
        "operationId": "startDeviceFlow",
        "description": "Step 1 of provisioning a key pair without the dashboard — what the Dialerr MCP server and CLI use. Returns a code and a URL the **user** opens in a browser to approve; they sign in or create an account there. Codes expire after 10 minutes. No authentication.",
        "security": [],
        "responses": {
          "200": {
            "description": "Flow started.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "deviceCode",
                    "userCode",
                    "verificationUri",
                    "verificationUriComplete",
                    "interval",
                    "expiresIn"
                  ],
                  "properties": {
                    "deviceCode": {
                      "type": "string",
                      "description": "Keep this; poll **Poll key provisioning** with it.",
                      "pattern": "^dc_"
                    },
                    "userCode": {
                      "type": "string",
                      "description": "Short code shown to the user.",
                      "example": "XXXX-XXXX"
                    },
                    "verificationUri": {
                      "type": "string",
                      "format": "uri",
                      "example": "https://dialerr.com/device"
                    },
                    "verificationUriComplete": {
                      "type": "string",
                      "format": "uri",
                      "description": "Send the user here.",
                      "example": "https://dialerr.com/device?code=XXXX-XXXX"
                    },
                    "interval": {
                      "type": "integer",
                      "description": "Seconds to wait between polls."
                    },
                    "expiresIn": {
                      "type": "integer",
                      "description": "Seconds until the code expires."
                    }
                  }
                }
              }
            }
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/api/v1/embed/device/token": {
      "post": {
        "tags": [
          "Embed"
        ],
        "summary": "Poll key provisioning (device flow)",
        "operationId": "pollDeviceFlow",
        "description": "Step 2. Poll with the `deviceCode` at the returned `interval`. Answers `pending` until the user approves, then **once** returns the key pair. After that the secret is cleared server-side; further calls, and calls after expiry, return 410.",
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "deviceCode"
                ],
                "properties": {
                  "deviceCode": {
                    "type": "string",
                    "pattern": "^dc_"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Either still waiting, or — exactly once — the provisioned keys.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "required": [
                        "status"
                      ],
                      "properties": {
                        "status": {
                          "type": "string",
                          "const": "pending"
                        }
                      }
                    },
                    {
                      "type": "object",
                      "required": [
                        "status",
                        "publicKey",
                        "secretKey",
                        "orgId"
                      ],
                      "properties": {
                        "status": {
                          "type": "string",
                          "const": "approved"
                        },
                        "publicKey": {
                          "type": "string",
                          "pattern": "^pk_",
                          "description": "Safe in client code."
                        },
                        "secretKey": {
                          "type": "string",
                          "pattern": "^sk_",
                          "description": "Shown once. Store in a server-side environment variable only."
                        },
                        "orgId": {
                          "type": "integer"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Malformed device code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Unknown device code.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "410": {
            "description": "The code expired, or the keys were already collected.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/calls": {
      "post": {
        "tags": [
          "Calls"
        ],
        "summary": "Start a call for a rep",
        "operationId": "startCall",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "Rings the rep first — their Dialerr softphone if it's connected, otherwise their cell (Agents page → Cell / forwarding number) — then dials the lead and connects the two. Returns as soon as the rep's phone starts ringing; poll `GET /api/v1/calls/{id}` for the result. The call is recorded and dispositioned like any other and appears on the lead's timeline. Billed as normal call usage.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to",
                  "agent"
                ],
                "properties": {
                  "to": {
                    "type": "string",
                    "description": "The lead's number, E.164.",
                    "example": "+13055550123"
                  },
                  "agent": {
                    "type": "string",
                    "description": "The rep who takes the call: their email or user id.",
                    "example": "diana@example.com"
                  },
                  "from": {
                    "type": "string",
                    "description": "One of your organization's numbers, E.164. Defaults to your primary voice number."
                  },
                  "lead_id": {
                    "type": "integer",
                    "description": "Attach to this lead; otherwise matched by phone."
                  },
                  "note": {
                    "type": "string",
                    "description": "Why the call is being placed; shown on the call.",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The rep's phone is ringing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/CallStarted"
                }
              }
            }
          },
          "400": {
            "description": "Bad number, or `agent` missing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Wallet is empty.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "The rep isn't in your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Refused: the number is blocked (`NUMBER_BLOCKED`), the rep is already on a call (`AGENT_BUSY`), the rep has no connected phone and no cell on file (`AGENT_NO_PHONE`), or you have no number to call from (`NO_FROM_NUMBER`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/calls/{id}": {
      "get": {
        "tags": [
          "Calls"
        ],
        "summary": "Read a call",
        "operationId": "getCall",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "Where a call stands and what happened on it: state, duration, recording, disposition, and the AI's summary when the AI handled it. Poll this after `startCall` or `startAiCall`; on an AI task call, `ai_result` holds what the AI achieved once `state` is ended / no_answer / failed.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The call.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Call"
                }
              }
            }
          },
          "404": {
            "description": "No such call.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/leads/{id}/timeline": {
      "get": {
        "tags": [
          "Leads"
        ],
        "summary": "Read a lead's timeline",
        "operationId": "getLeadTimeline",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "Calls and texts with this lead, newest first, including the AI's summary on calls it handled. What an assistant should read before calling or texting someone.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "The lead and its timeline.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "lead": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string",
                          "nullable": true
                        },
                        "phone": {
                          "type": "string"
                        },
                        "email": {
                          "type": "string",
                          "nullable": true
                        },
                        "company": {
                          "type": "string",
                          "nullable": true
                        },
                        "status": {
                          "type": "string",
                          "nullable": true
                        },
                        "callback_at": {
                          "type": "string",
                          "nullable": true
                        }
                      }
                    },
                    "items": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/TimelineItem"
                      }
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No such lead.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/leads/{id}/callback": {
      "post": {
        "tags": [
          "Leads"
        ],
        "summary": "Book a callback",
        "operationId": "bookCallback",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "Promises the lead a callback at a time. The rep's callback runner dials it when due; it shows on the lead's timeline.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            }
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "at"
                ],
                "properties": {
                  "at": {
                    "type": "string",
                    "format": "date-time",
                    "description": "When, with timezone offset.",
                    "example": "2026-09-22T09:00:00-04:00"
                  },
                  "agent": {
                    "type": "string",
                    "description": "Who calls back: email or user id. Defaults to the lead's owner."
                  },
                  "note": {
                    "type": "string",
                    "maxLength": 500
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Booked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "lead_id": {
                      "type": "integer"
                    },
                    "callback_at": {
                      "type": "string",
                      "format": "date-time"
                    },
                    "agent_id": {
                      "type": "integer",
                      "nullable": true
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Bad or past time.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such lead or rep.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ai-calls": {
      "post": {
        "tags": [
          "Calls"
        ],
        "summary": "Have the AI make a call",
        "operationId": "startAiCall",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "Dialerr's AI dials a number and does an errand for a named person: books an appointment, asks a question, confirms hours, requests a call back. It opens as an AI assistant calling on behalf of that person, says the call is recorded for its notes, works phone menus, shares only the `facts` given, leaves a voicemail if it must, and writes down the result. Returns as soon as it is dialing; poll `GET /api/v1/calls/{id}` for `ai_result`. Not for sales or cold outreach. Billed as AI voice minutes.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to",
                  "goal",
                  "on_behalf_of"
                ],
                "properties": {
                  "to": {
                    "type": "string",
                    "description": "The number to call, E.164.",
                    "example": "+15612919839"
                  },
                  "goal": {
                    "type": "string",
                    "description": "What the AI must accomplish, one or two sentences.",
                    "example": "Book a pediatric ENT consultation for Lennox Rozenfeld — earliest available, mornings preferred."
                  },
                  "on_behalf_of": {
                    "type": "string",
                    "description": "Who the AI is calling for — the person's name.",
                    "example": "Steven Rozenfeld"
                  },
                  "facts": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "The only facts the AI may share. Anything not listed, it says the person will follow up with.",
                    "example": [
                      "Patient: Lennox Rozenfeld, DOB 10/23/2022",
                      "Insurance: Medicaid"
                    ]
                  },
                  "callback_number": {
                    "type": "string",
                    "description": "The person's own number, given to the business if they need to reach them."
                  },
                  "report_sms_to": {
                    "type": "string",
                    "description": "Text the result to this number when the call ends."
                  },
                  "report_to": {
                    "type": "string",
                    "format": "uri",
                    "description": "https URL that receives a POST with the full result (same shape as `GET /api/v1/calls/{id}` plus `transcript`) when the call ends."
                  },
                  "voicemail_message": {
                    "type": "string",
                    "description": "What to say on voicemail. Default: who is calling, for whom, why, and the callback number."
                  },
                  "agent_id": {
                    "type": "integer",
                    "description": "Which AI agent (name and voice) speaks. Defaults to the org's live voice agent."
                  },
                  "from": {
                    "type": "string",
                    "description": "Which of the organization's numbers to call from."
                  },
                  "reference": {
                    "type": "string",
                    "description": "Your own reference for this task, echoed back on the call."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The AI is dialing.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "status",
                    "to",
                    "from",
                    "poll"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "ringing"
                      ]
                    },
                    "to": {
                      "type": "string"
                    },
                    "from": {
                      "type": "string"
                    },
                    "agent": {
                      "type": "object",
                      "properties": {
                        "id": {
                          "type": "integer"
                        },
                        "name": {
                          "type": "string"
                        }
                      }
                    },
                    "lead_id": {
                      "type": "integer",
                      "nullable": true
                    },
                    "poll": {
                      "type": "string",
                      "description": "Path to poll for the result."
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Missing `goal` / `on_behalf_of`, bad number, bad `report_to`. Codes: GOAL_REQUIRED, ON_BEHALF_OF_REQUIRED, BAD_NUMBER, BAD_REPORT_TO, NO_FROM_NUMBER.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "402": {
            "description": "Wallet out of funds (INSUFFICIENT_FUNDS).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No AI agent in the organization (NO_AI_AGENT).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The AI is already on a call with that number (AI_BUSY), or the number is on the do-not-call list (NUMBER_BLOCKED).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Voice AI or the phone system is unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/messages/{id}": {
      "get": {
        "tags": [
          "Messaging"
        ],
        "summary": "Did the message arrive?",
        "operationId": "getMessage",
        "description": "The carrier's verdict on a message you sent. Poll this after POST /api/v1/messages rather than assuming a 200 means delivery.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `id` returned when the message was sent."
          }
        ],
        "responses": {
          "200": {
            "description": "The message and its delivery state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "accepted",
                        "sent",
                        "delivered",
                        "failed"
                      ]
                    },
                    "delivered": {
                      "type": "boolean"
                    },
                    "from": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "to": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "sent_at": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "failure_reason": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The carrier's own words when it refused, e.g. that the sending number is not registered."
                    },
                    "failure_code": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "retryable": {
                      "type": "boolean",
                      "description": "False when the carrier refused the sender rather than the message — re-sending will fail identically."
                    }
                  }
                }
              }
            }
          },
          "404": {
            "description": "No message with that id.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "security": [
          {
            "apiToken": []
          }
        ]
      }
    },
    "/api/v1/fax/upload": {
      "post": {
        "tags": [
          "Fax"
        ],
        "summary": "Upload the PDF to fax",
        "operationId": "uploadFaxDocument",
        "description": "The document, before anyone commits to sending it. The body is the raw PDF bytes — not JSON, not multipart. PDF only, checked by its bytes; up to 25 MB. Keep the returned key: POST /api/v1/fax/send takes it as media_key.",
        "requestBody": {
          "required": true,
          "content": {
            "application/pdf": {
              "schema": {
                "type": "string",
                "format": "binary"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Stored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "key"
                  ],
                  "properties": {
                    "key": {
                      "type": "string",
                      "example": "fax/31/out_2f338bc3-7b20-4812-944b-e1209ec24133.pdf"
                    },
                    "bytes": {
                      "type": "integer"
                    },
                    "pages": {
                      "type": [
                        "integer",
                        "null"
                      ],
                      "description": "Read off the PDF; null for PDFs that compress their page tree (counted when it sends)."
                    },
                    "page_cents": {
                      "type": "integer",
                      "description": "Price per page.",
                      "example": 10
                    },
                    "previewUrl": {
                      "type": "string",
                      "description": "A link to the uploaded PDF, good for 15 minutes."
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "The body was empty or not a PDF.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/fax/send": {
      "post": {
        "tags": [
          "Fax"
        ],
        "summary": "Send a fax",
        "operationId": "sendFax",
        "description": "Faxes a PDF from one of the organization's fax lines. 10 cents a page from the wallet. There is no cover page: the PDF goes exactly as uploaded.\n\nA 200 means QUEUED with the carrier, not delivered — a fax takes 30 seconds to a few minutes. Poll GET /api/v1/fax/{id}. A failed send retries by itself up to 3 attempts (2 then 8 minutes apart) unless the failure is permanent, such as a number that is not a fax line.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "to"
                ],
                "properties": {
                  "to": {
                    "type": "string",
                    "description": "Destination fax number, E.164.",
                    "example": "+18774604752"
                  },
                  "media_key": {
                    "type": "string",
                    "description": "The key from POST /api/v1/fax/upload. One of media_key or media_url is required."
                  },
                  "media_url": {
                    "type": "string",
                    "description": "Instead of uploading: a publicly reachable URL of a PDF."
                  },
                  "from": {
                    "type": "string",
                    "description": "One of the organization's fax lines, E.164. Defaults to the first."
                  },
                  "document_name": {
                    "type": "string",
                    "description": "The name shown on the timeline."
                  },
                  "lead_id": {
                    "type": "integer",
                    "description": "File the fax on this lead's timeline."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Queued with the carrier.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "id",
                    "status"
                  ],
                  "properties": {
                    "id": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "queued"
                      ]
                    },
                    "from": {
                      "type": "string"
                    },
                    "to": {
                      "type": "string"
                    },
                    "check": {
                      "type": "string",
                      "description": "Path to poll for the outcome.",
                      "example": "/api/v1/fax/fax-…"
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "`to` or the document missing; `NO_FAX_NUMBER` (the organization has no fax line yet — an admin buys one in the app); `FROM_NOT_YOURS`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "402": {
            "description": "Wallet has no funds for this send.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/fax/{id}": {
      "get": {
        "tags": [
          "Fax"
        ],
        "summary": "Did the fax arrive?",
        "operationId": "getFax",
        "description": "Where a fax stands. Poll this after POST /api/v1/fax/send rather than assuming a 200 means delivery.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The `id` returned when the fax was sent."
          }
        ],
        "responses": {
          "200": {
            "description": "The fax and its state.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Fax"
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "404": {
            "description": "No fax with that id in your organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/fax": {
      "get": {
        "tags": [
          "Fax"
        ],
        "summary": "Fax history",
        "operationId": "listFaxes",
        "description": "The organization's faxes, newest first, sent and received.",
        "parameters": [
          {
            "name": "direction",
            "in": "query",
            "schema": {
              "type": "string",
              "enum": [
                "outbound",
                "inbound"
              ]
            }
          },
          {
            "name": "lead_id",
            "in": "query",
            "schema": {
              "type": "integer"
            }
          },
          {
            "name": "limit",
            "in": "query",
            "schema": {
              "type": "integer",
              "default": 50,
              "maximum": 200
            }
          }
        ],
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "faxes": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Fax"
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/fax/numbers": {
      "get": {
        "tags": [
          "Fax"
        ],
        "summary": "The organization's fax lines",
        "operationId": "listFaxNumbers",
        "description": "Which numbers faxes can be sent from. Empty means no fax line yet: an admin buys one in the app at Settings → Phone Numbers → Fax.",
        "responses": {
          "200": {
            "description": "OK",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "numbers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "phone_number": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "label": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "monthly_cents": {
                      "type": "integer",
                      "example": 999
                    }
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          }
        }
      }
    },
    "/api/v1/ai-routing": {
      "get": {
        "tags": [
          "AI Answering"
        ],
        "summary": "Who answers the phones",
        "operationId": "getAiRouting",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "Every AI agent's status, Voice channel and pickup rules, and the AI agent each number hands calls to.",
        "responses": {
          "200": {
            "description": "Current setup.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agents": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "name": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "live",
                              "paused",
                              "draft"
                            ]
                          },
                          "channels": {
                            "type": "array",
                            "items": {
                              "type": "string"
                            }
                          },
                          "voice_enabled": {
                            "type": "boolean"
                          },
                          "voice_rules": {
                            "type": "object",
                            "properties": {
                              "pickup": {
                                "type": "object",
                                "properties": {
                                  "no_rep_online": {
                                    "type": "boolean",
                                    "description": "Answer when nobody in the organization is online, or when the call has nobody it could ring."
                                  },
                                  "no_answer_seconds": {
                                    "type": [
                                      "integer",
                                      "null"
                                    ],
                                    "minimum": 5,
                                    "maximum": 120,
                                    "description": "Ring reps this long, then the AI answers. null turns it off."
                                  },
                                  "outside_hours": {
                                    "type": "boolean",
                                    "description": "Answer outside the number's business hours."
                                  },
                                  "every_call": {
                                    "type": "boolean",
                                    "description": "Answer every call."
                                  }
                                }
                              },
                              "except": {
                                "type": "object",
                                "properties": {
                                  "assigned_rep_online": {
                                    "type": "boolean",
                                    "description": "Don't answer when the caller's own rep is online."
                                  },
                                  "on_list_id": {
                                    "type": [
                                      "integer",
                                      "null"
                                    ],
                                    "description": "Don't answer callers on this list."
                                  },
                                  "unknown_caller": {
                                    "type": "boolean",
                                    "description": "Don't answer callers who aren't a known lead."
                                  }
                                }
                              }
                            }
                          },
                          "summary": {
                            "type": "string",
                            "example": "Answers when no rep is online"
                          }
                        }
                      }
                    },
                    "numbers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "phone_number": {
                            "type": "string"
                          },
                          "friendly_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "status": {
                            "type": "string"
                          },
                          "ai_agent_id": {
                            "type": [
                              "integer",
                              "null"
                            ]
                          },
                          "ai_agent_mode": {
                            "type": "string",
                            "enum": [
                              "rules",
                              "always"
                            ]
                          }
                        }
                      }
                    }
                  }
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ai-agents/{id}/voice-routing": {
      "put": {
        "tags": [
          "AI Answering"
        ],
        "summary": "Set when the AI answers",
        "operationId": "setAiVoiceRouting",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "Turns the Voice channel on or off and sets the pickup rules. Fields left out keep their value. Instructions, tools and knowledge are not changeable here. Needs a token created with \"Can change AI answering settings\" ticked (scope `settings:write`); other tokens get 403 `missing_scope`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "AI agent id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "voice_enabled": {
                    "type": "boolean"
                  },
                  "pickup": {
                    "type": "object",
                    "properties": {
                      "no_rep_online": {
                        "type": "boolean",
                        "description": "Answer when nobody in the organization is online, or when the call has nobody it could ring."
                      },
                      "no_answer_seconds": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "minimum": 5,
                        "maximum": 120,
                        "description": "Ring reps this long, then the AI answers. null turns it off."
                      },
                      "outside_hours": {
                        "type": "boolean",
                        "description": "Answer outside the number's business hours."
                      },
                      "every_call": {
                        "type": "boolean",
                        "description": "Answer every call."
                      }
                    }
                  },
                  "except": {
                    "type": "object",
                    "properties": {
                      "assigned_rep_online": {
                        "type": "boolean",
                        "description": "Don't answer when the caller's own rep is online."
                      },
                      "on_list_id": {
                        "type": [
                          "integer",
                          "null"
                        ],
                        "description": "Don't answer callers on this list."
                      },
                      "unknown_caller": {
                        "type": "boolean",
                        "description": "Don't answer callers who aren't a known lead."
                      }
                    }
                  }
                }
              },
              "example": {
                "voice_enabled": true,
                "pickup": {
                  "no_rep_online": true,
                  "no_answer_seconds": null,
                  "outside_hours": false,
                  "every_call": false
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved; returns the rules now in force."
          },
          "400": {
            "description": "Invalid rule value.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks settings:write, or the user is not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such agent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/ai-agents/{id}/status": {
      "put": {
        "tags": [
          "AI Answering"
        ],
        "summary": "Turn an AI agent live or paused",
        "operationId": "setAiAgentStatus",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "A paused agent answers nothing on any channel. Needs a token created with \"Can change AI answering settings\" ticked (scope `settings:write`); other tokens get 403 `missing_scope`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "AI agent id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "required": [
                  "status"
                ],
                "properties": {
                  "status": {
                    "type": "string",
                    "enum": [
                      "live",
                      "paused",
                      "draft"
                    ]
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved."
          },
          "400": {
            "description": "Invalid status.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks settings:write, or the user is not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such agent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/phone-numbers/{id}/ai-agent": {
      "put": {
        "tags": [
          "AI Answering"
        ],
        "summary": "Set the AI agent on one number",
        "operationId": "setNumberAiAgent",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "Number ids come from GET /api/v1/ai-routing. Needs a token created with \"Can change AI answering settings\" ticked (scope `settings:write`); other tokens get 403 `missing_scope`.",
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "integer"
            },
            "description": "Phone number id"
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ai_agent_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "The AI agent to hand calls to (it needs Voice on). null removes the AI from these numbers."
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "rules",
                      "always"
                    ],
                    "default": "rules",
                    "description": "`rules`: the agent's pickup rules decide. `always`: the AI answers every call to these numbers."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved."
          },
          "400": {
            "description": "The agent isn't in this organization or has no Voice channel.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks settings:write, or the user is not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/api/v1/phone-numbers/ai-agent": {
      "put": {
        "tags": [
          "AI Answering"
        ],
        "summary": "Set the AI agent on many numbers",
        "operationId": "setNumbersAiAgent",
        "security": [
          {
            "apiToken": []
          }
        ],
        "description": "Pick the numbers by id, by phone number, or all active numbers. Numbers not found are listed in `not_found`. Needs a token created with \"Can change AI answering settings\" ticked (scope `settings:write`); other tokens get 403 `missing_scope`.",
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "ai_agent_id": {
                    "type": [
                      "integer",
                      "null"
                    ],
                    "description": "The AI agent to hand calls to (it needs Voice on). null removes the AI from these numbers."
                  },
                  "mode": {
                    "type": "string",
                    "enum": [
                      "rules",
                      "always"
                    ],
                    "default": "rules",
                    "description": "`rules`: the agent's pickup rules decide. `always`: the AI answers every call to these numbers."
                  },
                  "phone_number_ids": {
                    "type": "array",
                    "items": {
                      "type": "integer"
                    }
                  },
                  "phone_numbers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "example": [
                      "+13058425999"
                    ]
                  },
                  "all": {
                    "type": "boolean"
                  }
                }
              },
              "example": {
                "ai_agent_id": 3,
                "mode": "rules",
                "all": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Saved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "success": {
                      "type": "boolean"
                    },
                    "ai_agent_id": {
                      "type": [
                        "integer",
                        "null"
                      ]
                    },
                    "ai_agent_mode": {
                      "type": "string"
                    },
                    "updated": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "integer"
                          },
                          "phone_number": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "not_found": {
                      "type": "array",
                      "items": {}
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "No numbers named, or the agent is not usable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "403": {
            "description": "Token lacks settings:write, or the user is not an admin.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "None of those numbers are in this organization.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "securitySchemes": {
      "apiToken": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "api_...",
        "description": "Organization API token for the Data API. Created by an administrator at https://dialerr.com → Settings → API Access. Limited to 1,000 requests per hour per token."
      },
      "secretKey": {
        "type": "http",
        "scheme": "bearer",
        "bearerFormat": "sk_...",
        "description": "Embed secret key. Server-side only — never in client code, commands or commits. Provisioned in Settings → API Access or via the device flow."
      }
    },
    "responses": {
      "BadRequest": {
        "description": "The request was missing something or malformed. The `error` field says what.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "Unauthorized": {
        "description": "Missing, invalid or revoked API token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "RateLimited": {
        "description": "More than 1,000 requests in the last hour for this token.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      },
      "ServerError": {
        "description": "Something failed on our side. Safe to retry.",
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            }
          }
        }
      }
    },
    "schemas": {
      "Error": {
        "type": "object",
        "required": [
          "error"
        ],
        "properties": {
          "error": {
            "type": "string",
            "description": "Human-readable reason."
          },
          "details": {
            "type": "string"
          },
          "code": {
            "type": "string",
            "description": "Machine-readable reason when one exists, e.g. RECIPIENT_OPTED_OUT, INVALID_TO_NUMBER, SMS_NOT_CONFIGURED, AGENT_BUSY."
          }
        }
      },
      "LeadInput": {
        "type": "object",
        "description": "A lead. Provide a name (either `first_name`/`last_name` or `name`) and at least one of `phone_number` or `email`.",
        "properties": {
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "name": {
            "type": "string",
            "description": "Full name; split into first and last if the separate fields are absent."
          },
          "phone_number": {
            "type": "string",
            "description": "E.164 preferred; normalized on arrival. Also accepted as `phone`.",
            "example": "+13055550123"
          },
          "email": {
            "type": "string",
            "format": "email"
          },
          "company": {
            "type": "string"
          },
          "title": {
            "type": "string"
          },
          "website": {
            "type": "string"
          },
          "address": {
            "type": "string"
          },
          "city": {
            "type": "string"
          },
          "state": {
            "type": "string"
          },
          "postal_code": {
            "type": "string",
            "description": "Also accepted as `zip`."
          },
          "country": {
            "type": "string"
          },
          "source": {
            "type": "string",
            "description": "Where this lead came from, e.g. your system's name. Defaults to `manual`."
          },
          "status": {
            "type": "string"
          },
          "notes": {
            "type": "string",
            "description": "Free text, stored on the lead's custom fields."
          },
          "custom_fields": {
            "type": "object",
            "additionalProperties": true,
            "description": "Arbitrary extra key/values kept on the lead."
          },
          "list_name": {
            "type": "string",
            "description": "Create-only. Auto-creates the list if needed and assigns the lead to it — including when the lead already existed."
          }
        }
      },
      "Lead": {
        "type": "object",
        "properties": {
          "id": {
            "type": "integer"
          },
          "first_name": {
            "type": "string"
          },
          "last_name": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "phone": {
            "type": "string",
            "description": "Normalized E.164, or empty for an email-only lead."
          },
          "company": {
            "type": "string"
          },
          "status": {
            "type": "string"
          },
          "source": {
            "type": "string"
          },
          "organization_id": {
            "type": "integer"
          }
        }
      },
      "LeadCreated": {
        "type": "object",
        "required": [
          "success",
          "message",
          "lead"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Lead created successfully"
          },
          "lead": {
            "$ref": "#/components/schemas/Lead"
          }
        }
      },
      "LeadExists": {
        "type": "object",
        "required": [
          "success",
          "message",
          "lead"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "message": {
            "type": "string",
            "const": "Lead already exists"
          },
          "lead": {
            "type": "object",
            "required": [
              "id"
            ],
            "properties": {
              "id": {
                "type": "integer",
                "description": "The existing lead's id."
              },
              "phone": {
                "type": "string"
              }
            }
          }
        }
      },
      "BulkImportResult": {
        "type": "object",
        "required": [
          "success",
          "imported",
          "skipped",
          "duplicatesAddedToList",
          "enrichedExisting",
          "total"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "const": true
          },
          "imported": {
            "type": "integer",
            "description": "New leads created."
          },
          "skipped": {
            "type": "integer",
            "description": "Items with no usable name, phone or email."
          },
          "duplicatesAddedToList": {
            "type": "integer",
            "description": "Already-known leads that were assigned to `listId`."
          },
          "enrichedExisting": {
            "type": "integer",
            "description": "Already-known leads that gained a field they were missing."
          },
          "total": {
            "type": "integer",
            "description": "Items in the request."
          },
          "errors": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Present only when some items failed."
          }
        }
      },
      "Message": {
        "type": "object",
        "required": [
          "id",
          "direction",
          "body",
          "conversation_id",
          "sender"
        ],
        "properties": {
          "id": {
            "type": "string"
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "body": {
            "type": "string"
          },
          "from": {
            "type": [
              "string",
              "null"
            ]
          },
          "to": {
            "type": [
              "string",
              "null"
            ]
          },
          "sent_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "conversation_id": {
            "type": "string",
            "description": "`sms:` followed by the other party's last ten digits.",
            "example": "sms:3055550123"
          },
          "sender": {
            "type": "object",
            "required": [
              "type"
            ],
            "properties": {
              "type": {
                "type": "string",
                "enum": [
                  "customer",
                  "agent",
                  "api"
                ],
                "description": "`customer` for inbound; `agent` when a person sent it from Dialerr; `api` when it was sent through this API."
              },
              "name": {
                "type": [
                  "string",
                  "null"
                ],
                "description": "The sending agent's email, when `type` is `agent`."
              }
            }
          },
          "external_ref": {
            "type": [
              "string",
              "null"
            ],
            "description": "The `external_ref` supplied when the message was sent through this API."
          }
        }
      },
      "CallStarted": {
        "type": "object",
        "required": [
          "id",
          "status",
          "to",
          "from",
          "rings",
          "agent",
          "poll"
        ],
        "properties": {
          "id": {
            "type": "string",
            "description": "Call id. Poll GET /api/v1/calls/{id} for the result."
          },
          "status": {
            "type": "string",
            "enum": [
              "ringing"
            ]
          },
          "to": {
            "type": "string",
            "example": "+13055550123"
          },
          "from": {
            "type": "string",
            "description": "The number the lead sees."
          },
          "rings": {
            "type": "string",
            "enum": [
              "toolbar",
              "cell"
            ],
            "description": "Where the rep is rung first: their Dialerr softphone, or their cell when the softphone isn't connected."
          },
          "agent": {
            "type": "object",
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              }
            }
          },
          "lead_id": {
            "type": "integer",
            "nullable": true
          },
          "poll": {
            "type": "string"
          }
        }
      },
      "Call": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "enum": [
              "ringing",
              "connected",
              "ended",
              "no_answer",
              "failed"
            ],
            "description": "Where the call stands. `ended` means people talked."
          },
          "status": {
            "type": "string",
            "description": "Raw status."
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "to": {
            "type": "string"
          },
          "from": {
            "type": "string"
          },
          "agent": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string"
              },
              "email": {
                "type": "string"
              }
            }
          },
          "lead": {
            "type": "object",
            "nullable": true,
            "properties": {
              "id": {
                "type": "integer"
              },
              "name": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "started_at": {
            "type": "string",
            "format": "date-time"
          },
          "answered_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "ended_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "duration_seconds": {
            "type": "integer"
          },
          "recording_url": {
            "type": "string",
            "nullable": true,
            "description": "Fetch with the same bearer token."
          },
          "disposition": {
            "type": "string",
            "nullable": true
          },
          "disposition_notes": {
            "type": "string",
            "nullable": true
          },
          "outcome": {
            "type": "string",
            "nullable": true
          },
          "handled_by_ai": {
            "type": "boolean"
          },
          "ai_summary": {
            "type": "string",
            "nullable": true,
            "description": "What the AI did, when the AI answered."
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "ai_task": {
            "type": "object",
            "nullable": true,
            "description": "On an AI task call: what it was asked to do.",
            "properties": {
              "goal": {
                "type": "string"
              },
              "on_behalf_of": {
                "type": "string"
              },
              "reference": {
                "type": "string",
                "nullable": true
              }
            }
          },
          "ai_result": {
            "allOf": [
              {
                "$ref": "#/components/schemas/AiResult"
              }
            ],
            "nullable": true
          }
        }
      },
      "TimelineItem": {
        "type": "object",
        "required": [
          "type",
          "id",
          "at"
        ],
        "properties": {
          "type": {
            "type": "string",
            "enum": [
              "call",
              "text"
            ]
          },
          "id": {
            "type": "string"
          },
          "at": {
            "type": "string",
            "format": "date-time"
          },
          "direction": {
            "type": "string",
            "enum": [
              "inbound",
              "outbound"
            ]
          },
          "result": {
            "type": "string",
            "enum": [
              "answered",
              "missed",
              "no_answer"
            ],
            "description": "Calls only."
          },
          "duration_seconds": {
            "type": "integer"
          },
          "agent": {
            "type": "string",
            "nullable": true
          },
          "handled_by_ai": {
            "type": "boolean"
          },
          "disposition": {
            "type": "string",
            "nullable": true
          },
          "notes": {
            "type": "string",
            "nullable": true
          },
          "ai_summary": {
            "type": "string",
            "nullable": true
          },
          "recording_url": {
            "type": "string",
            "nullable": true
          },
          "body": {
            "type": "string",
            "description": "Texts only."
          },
          "sent_by": {
            "type": "string",
            "nullable": true
          }
        }
      },
      "AiResult": {
        "type": "object",
        "description": "What the AI achieved on a task call. Present only on calls started with `POST /api/v1/ai-calls`.",
        "properties": {
          "outcome": {
            "type": "string",
            "enum": [
              "in_progress",
              "booked",
              "info_gathered",
              "callback_needed",
              "refused",
              "voicemail",
              "no_answer",
              "wrong_number",
              "other"
            ]
          },
          "outcome_label": {
            "type": "string",
            "nullable": true
          },
          "details": {
            "type": "string",
            "description": "What they said and what was agreed, in plain sentences."
          },
          "appointment_at": {
            "type": "string",
            "format": "date-time",
            "nullable": true
          },
          "confirmation": {
            "type": "string",
            "nullable": true,
            "description": "Confirmation number, who they spoke with, or a reference."
          },
          "next_step": {
            "type": "string",
            "nullable": true,
            "description": "What the person has to do next, or \"nothing\"."
          },
          "recorded_at": {
            "type": "string",
            "format": "date-time"
          },
          "transcript": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "speaker": {
                  "type": "string",
                  "enum": [
                    "agent",
                    "lead",
                    "unknown"
                  ]
                },
                "text": {
                  "type": "string"
                }
              }
            }
          }
        }
      },
      "Fax": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "example": "fax-f49ea64b-f4da-45b4-a16c-c613799cd833"
          },
          "direction": {
            "type": "string",
            "enum": [
              "outbound",
              "inbound"
            ]
          },
          "from_number": {
            "type": "string"
          },
          "to_number": {
            "type": "string"
          },
          "lead_id": {
            "type": [
              "integer",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "queued",
              "sending",
              "retrying",
              "delivered",
              "failed",
              "received"
            ],
            "description": "queued → sending → delivered or failed. \"retrying\" sits between a failed attempt and the next one."
          },
          "delivered": {
            "type": "boolean"
          },
          "page_count": {
            "type": [
              "integer",
              "null"
            ],
            "description": "The carrier's count, known once it sends."
          },
          "document_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "failure_reason": {
            "type": [
              "string",
              "null"
            ],
            "description": "The carrier's own words when it failed."
          },
          "attempt": {
            "type": "integer"
          },
          "max_attempts": {
            "type": "integer",
            "example": 3
          },
          "retry_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "When the next attempt is due, while retrying."
          },
          "retryable": {
            "type": "boolean",
            "description": "False when the failure is permanent (not a fax line, number rejected): sending again fails identically."
          },
          "created_at": {
            "type": "string"
          },
          "updated_at": {
            "type": "string"
          }
        }
      }
    }
  }
}