{
  "openapi": "3.1.0",
  "info": {
    "title": "Cadence API",
    "version": "2026-09-24",
    "summary": "Agent-native RCS messaging with automatic SMS fallback.",
    "description": "Every path here is generated from the live route table, and request and response schemas are generated from the validators that actually run — so this file cannot describe a route that does not exist, or a body the server would reject.\n\n### Three things that catch people\n\n1. **Casing is not uniform.** Brand routes are snake_case; conversation routes are camelCase. `PATCH /v1/brands/{id}` rejects camelCase rather than silently ignoring it.\n2. **Two different things are called a webhook.** `POST /v1/webhooks` registers an endpoint *we* POST your inbound activity to — that is the one you want. `PUT /v1/agents/{id}/webhook` points *Google* somewhere other than Cadence and switches off flow handling. See the `Delivery webhooks` and `Google inbound routing` tags.\n3. **Three operations are irreversible** and marked `x-irreversible`. They refuse with 409 `CONFIRMATION_REQUIRED` and change nothing until re-sent with `{\"confirm\": \"irreversible\"}`.\n\nSandbox keys simulate delivery, so nothing reaches a phone while you build. `POST /v1/conversations/{id}/simulate-inbound` delivers a reply as though the recipient sent it, which is how a flow is exercised end to end without touching Google.",
    "contact": {
      "email": "support@cadencercs.com",
      "url": "https://cadencercs.com/docs.html"
    },
    "license": {
      "name": "Proprietary — Cadence Platform Terms",
      "url": "https://cadencercs.com/customer-terms"
    }
  },
  "servers": [
    {
      "url": "https://api.cadencercs.com",
      "description": "Production."
    },
    {
      "url": "https://cadence-api-tlx9.onrender.com",
      "description": "The same service under its hosting provider's hostname. Still works, and will keep working — anything already configured against it does not need to change today. Use api.cadencercs.com for anything new: a vendor hostname in your config is a migration you inherit later."
    }
  ],
  "tags": [
    {
      "name": "Onboarding",
      "description": "Get a key and find out what to do next."
    },
    {
      "name": "Conversations",
      "description": "Open, send, read. camelCase in and out."
    },
    {
      "name": "Flows",
      "description": "Hand the conversation to the server so replies are answered without a process of yours running."
    },
    {
      "name": "Delivery webhooks",
      "description": "We POST inbound activity to your endpoint. For apps with no process to keep running."
    },
    {
      "name": "Brands",
      "description": "The customer identity carriers review. **snake_case in and out.**"
    },
    {
      "name": "Agents",
      "description": "The sender. Local records, launch questionnaire and supporting documents."
    },
    {
      "name": "Google & carriers",
      "description": "Everything that touches Google RBM. Three operations here are irreversible."
    },
    {
      "name": "Google inbound routing",
      "description": "Where GOOGLE pushes an agent's traffic. Not how you receive messages from Cadence — see Flows."
    },
    {
      "name": "Consent",
      "description": "Who may be messaged, and the one audited way an opt-out is reversed."
    },
    {
      "name": "Readiness",
      "description": "Public pre-submission validator."
    },
    {
      "name": "Accounts",
      "description": "Dashboard accounts, sessions and usage."
    },
    {
      "name": "API keys",
      "description": "Issue and revoke."
    }
  ],
  "components": {
    "schemas": {
      "Error": {
        "type": "object",
        "description": "Every non-2xx response in this API. `error` is a stable machine code — switch on it, never on `message`. `remediation` says what to do next and is present wherever there is a next thing to do; `problems` itemises a multi-part validation failure.",
        "properties": {
          "error": {
            "type": "string",
            "description": "Stable machine-readable code, e.g. `FLOW_INVALID`, `CONFIRMATION_REQUIRED`."
          },
          "message": {
            "type": "string",
            "description": "Human-readable explanation. Wording is not stable; do not match on it."
          },
          "remediation": {
            "type": "string",
            "description": "The concrete next action, when one exists."
          },
          "problems": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "One entry per distinct problem, for multi-part failures."
          }
        },
        "required": [
          "error"
        ],
        "additionalProperties": true,
        "x-common-extras": "Several errors carry route-specific fields alongside the envelope: `reasons` (readiness gates), `missing` (an incomplete contact or config), `problems`, `editable`, `available`, `freezes`, `likelyCause`. They are additive — the four documented fields are always the ones to switch on."
      },
      "FlowInvalid": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Error"
          }
        ],
        "type": "object",
        "description": "Returned with HTTP 422 by `POST /v1/conversations` when the flow can't be run. **Nothing was sent and no conversation was opened** — `opened` is always false and `conversationId` always null, so there is no half-started conversation to clean up. Validation runs before anything is delivered, deliberately: a rejection that has already texted a real person is worse than no validation.",
        "properties": {
          "opened": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "conversationId": {
            "type": "null"
          },
          "problems": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "One entry per reason the flow was rejected."
          }
        },
        "required": [
          "opened",
          "conversationId",
          "problems"
        ]
      },
      "ConfirmationRequired": {
        "type": "object",
        "description": "Returned with HTTP 409 by the three irreversible operations. **Nothing has happened** — the request was refused, not partially applied. Re-send the identical request with `{\"confirm\": \"irreversible\"}` in the body once a human has agreed. A retry loop cannot produce that field by accident, which is the point.",
        "properties": {
          "error": {
            "type": "string",
            "enum": [
              "CONFIRMATION_REQUIRED"
            ]
          },
          "message": {
            "type": "string",
            "description": "What is created, and why it is permanent."
          },
          "beforeYouDo": {
            "type": "string",
            "description": "What to check before confirming."
          },
          "remediation": {
            "type": "string"
          },
          "irreversible": {
            "type": "boolean",
            "enum": [
              true
            ]
          }
        },
        "required": [
          "error",
          "message",
          "irreversible"
        ]
      },
      "Account": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "email": {
            "type": "string"
          },
          "name": {
            "type": [
              "string",
              "null"
            ]
          },
          "company": {
            "type": [
              "string",
              "null"
            ]
          },
          "email_verified_at": {
            "type": [
              "string",
              "null"
            ],
            "description": "ISO 8601, or null while unverified."
          },
          "suspended_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string",
            "description": "ISO 8601."
          }
        },
        "required": [
          "id",
          "email"
        ]
      },
      "ApiKey": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "key_prefix": {
            "type": "string",
            "description": "The visible prefix, e.g. `sk_sandbox_a1b2c3`. The key itself is shown once."
          },
          "kind": {
            "type": "string",
            "enum": [
              "sandbox",
              "live"
            ]
          },
          "label": {
            "type": [
              "string",
              "null"
            ]
          },
          "brand_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "Set on a brand-scoped key; null on an account-wide one."
          },
          "revoked_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "key_prefix",
          "kind"
        ]
      },
      "Brand": {
        "type": "object",
        "description": "**Brand fields are snake_case, in and out.** Conversation routes are camelCase. This is the single most common mistake against this API — `PATCH /v1/brands/{id}` rejects camelCase with `UNKNOWN_FIELDS` rather than ignoring it.",
        "properties": {
          "id": {
            "type": "string"
          },
          "account_id": {
            "type": "string"
          },
          "display_name": {
            "type": "string",
            "description": "Consumer-facing name."
          },
          "legal_entity": {
            "type": [
              "string",
              "null"
            ],
            "description": "Registered entity, e.g. `Salus, Inc.` — carriers check this against consent surfaces."
          },
          "website": {
            "type": [
              "string",
              "null"
            ]
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "use_case": {
            "type": [
              "string",
              "null"
            ],
            "enum": [
              "transactional",
              "promo",
              "leadgen",
              "multi_use",
              "otp",
              "conversational",
              null
            ],
            "description": "**Permanent at Google once the agent is synced**, and an agent's `useCase` defaults to this. `transactional` means the agent may never send anything promotional — no upgrade prompts, no win-back, ever. `multi_use` mixes both and is the value to pick if that is ever in scope. `conversational` is rejected at sync time: it describes turn structure rather than content, and Google's vocabulary is about content."
          },
          "logo_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "banner_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "color": {
            "type": [
              "string",
              "null"
            ],
            "description": "Hex, e.g. `#5B37DE`. Needs 4.5:1 contrast against white for launch review — the example is 6.9:1. `#7C5CFF` is 4.35:1 and Google refuses it, which is the kind of near-miss that only shows up at launch."
          },
          "contact_email": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "privacy_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "terms_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "ready",
              "submitted",
              "verified",
              "rejected"
            ]
          },
          "rbm_brand_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The brand's id at Google, once synced or linked."
          },
          "last_check_id": {
            "type": [
              "string",
              "null"
            ]
          },
          "submitted_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "verified_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "reject_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "account_id",
          "display_name",
          "status"
        ]
      },
      "Agent": {
        "type": "object",
        "description": "An agent as the API describes it. The webhook plumbing (`webhook_integration_id`, `webhook_uri`, token version) is deliberately not on the wire; `hasOwnWebhook` is the part worth knowing.",
        "properties": {
          "id": {
            "type": "string"
          },
          "brand_id": {
            "type": "string"
          },
          "display_name": {
            "type": "string"
          },
          "rbm_agent_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The agent's id at Google. Null until sync-google or link-google."
          },
          "use_case": {
            "type": [
              "string",
              "null"
            ]
          },
          "hosting_region": {
            "type": [
              "string",
              "null"
            ],
            "description": "`NORTH_AMERICA` | `EUROPE` | `ASIA_PACIFIC`. **Permanent** once the agent exists at Google."
          },
          "billing_category": {
            "type": [
              "string",
              "null"
            ],
            "description": "`CONVERSATIONAL` | `NON_CONVERSATIONAL`. Fixed at launch; only CONVERSATIONAL gets 24-hour session billing."
          },
          "region": {
            "type": "string",
            "description": "Derived API endpoint prefix, not Google's hosting region."
          },
          "status": {
            "type": "string",
            "enum": [
              "draft",
              "created",
              "submitted",
              "verified",
              "launching",
              "launched",
              "rejected"
            ],
            "description": "`created` reaches accepted test devices only. `launched` reaches real recipients, per carrier."
          },
          "hasOwnWebhook": {
            "type": "boolean",
            "description": "Whether this agent overrides the partner-level inbound webhook at Google."
          },
          "rbm_service_id": {
            "type": [
              "string",
              "null"
            ],
            "description": "The agent's messaging address (`bot@…`). Not readable from Google's API — copied by hand from the Console."
          },
          "deeplink_phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "launch_video_url": {
            "type": [
              "string",
              "null"
            ]
          },
          "reviewer_instructions": {
            "type": [
              "string",
              "null"
            ]
          },
          "trigger_description": {
            "type": [
              "string",
              "null"
            ]
          },
          "interaction_types": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_name": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_email": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_phone": {
            "type": [
              "string",
              "null"
            ]
          },
          "contact_title": {
            "type": [
              "string",
              "null"
            ]
          },
          "optin_description": {
            "type": [
              "string",
              "null"
            ]
          },
          "second_use_case": {
            "type": [
              "string",
              "null"
            ]
          },
          "submitted_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "verified_at": {
            "type": [
              "string",
              "null"
            ]
          },
          "reject_reason": {
            "type": [
              "string",
              "null"
            ]
          },
          "created_at": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "brand_id",
          "display_name",
          "status"
        ]
      },
      "Conversation": {
        "type": "object",
        "description": "Conversation fields are camelCase, in and out — unlike brand fields.",
        "properties": {
          "id": {
            "type": "string"
          },
          "agentId": {
            "type": "string"
          },
          "brandId": {
            "type": "string"
          },
          "to": {
            "type": "string",
            "description": "Recipient, E.164."
          },
          "channel": {
            "type": "string",
            "enum": [
              "rcs",
              "sms",
              "queued"
            ]
          },
          "channelConfidence": {
            "type": "string",
            "enum": [
              "provisional",
              "committed"
            ],
            "description": "`provisional` means we chose a channel and have no confirmation yet. It becomes `committed` when the channel is proven: on the first delivery receipt from the handset on RCS, or on an SMS fallback **caused by the recipient** — no RCS on their device, or your agent not launched on their carrier.\n\nA fallback caused by *us* — a rejected payload, a bad credential, a timeout — leaves this `provisional`, because it says nothing about the recipient. Committing there would mark a perfectly reachable handset SMS-only for the rest of the conversation on the strength of our own bug, and report it as a fact about their phone. See `fellBack.ourFault` on the send response."
          },
          "trustLevel": {
            "type": "string",
            "enum": [
              "verified_rcs",
              "branded_sms",
              "unbranded_sms"
            ]
          },
          "state": {
            "type": "string",
            "enum": [
              "OPENING",
              "OPEN",
              "USER_REPLIED",
              "CHANNEL_FLIP_TO_SMS",
              "CLOSING",
              "CLOSED",
              "CLOSED_OPTOUT"
            ]
          },
          "useCase": {
            "type": "string",
            "enum": [
              "transactional",
              "promo",
              "conversational",
              "leadgen"
            ]
          },
          "turnCount": {
            "type": "number"
          },
          "turnCeiling": {
            "type": "number"
          },
          "rcsOnly": {
            "type": "boolean",
            "description": "SMS is disabled for this conversation."
          },
          "createdAt": {
            "type": "string"
          },
          "closedAt": {
            "type": [
              "string",
              "null"
            ]
          }
        },
        "required": [
          "id",
          "agentId",
          "brandId",
          "to",
          "state"
        ]
      },
      "FellBack": {
        "type": "object",
        "properties": {
          "cause": {
            "type": "string",
            "enum": [
              "recipient",
              "sender"
            ],
            "description": "`recipient`: their handset has no RCS, or your agent is not launched on their carrier. `sender`: we were rejected — a bad payload, a media URL Google could not fetch, a timeout. Nothing was learned about the recipient."
          },
          "reason": {
            "type": "string",
            "description": "Google's or the carrier's own words, trimmed."
          },
          "ourFault": {
            "type": "boolean",
            "description": "True when the fallback says nothing about the recipient. Retrying rich is reasonable once the reason is fixed."
          },
          "remediation": {
            "type": "string"
          },
          "mediaRejected": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "description": "The file Google refused."
              },
              "stage": {
                "type": "string",
                "enum": [
                  "google_fetch"
                ],
                "description": "Google fetches `media.url` itself; we never see the HTTP exchange."
              },
              "status": {
                "type": "number",
                "description": "The HTTP status Google's error names, when it names one."
              },
              "reason": {
                "type": "string",
                "description": "Google's sentence about the file."
              }
            },
            "required": [
              "url",
              "stage",
              "reason"
            ],
            "description": "Present when Google refused the message because of the file behind `media.url` or a card's media. The URL must answer GET and HEAD with 200, a real Content-Type and Content-Length, over public https with no login. The recipient is reachable; the SMS carried the fallback text."
          }
        },
        "required": [
          "cause",
          "reason",
          "ourFault"
        ]
      },
      "Message": {
        "type": "object",
        "properties": {
          "seq": {
            "type": "number",
            "description": "Monotonic within the conversation."
          },
          "direction": {
            "type": "string",
            "enum": [
              "in",
              "out"
            ]
          },
          "channel": {
            "type": "string",
            "enum": [
              "rcs",
              "sms"
            ],
            "description": "Per message, so an RCS→SMS fallback is visible in the transcript."
          },
          "media": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string"
              },
              "mimeType": {
                "type": "string"
              },
              "sizeBytes": {
                "type": "number"
              },
              "fileName": {
                "type": "string"
              },
              "thumbnailUrl": {
                "type": "string"
              },
              "altText": {
                "type": "string"
              }
            },
            "description": "Present on a row that carried a file: inbound (Google-hosted URL, may be time-limited; `deliveredText` is a placeholder such as `[photo]`) or outbound (the file you sent, on the RCS row and on the SMS row where it went as a link)."
          },
          "body": {
            "type": [
              "string",
              "null"
            ],
            "description": "The text as composed. On a message delivered over SMS this is the RCS text, which is **not** what the recipient read."
          },
          "smsFallbackText": {
            "type": [
              "string",
              "null"
            ]
          },
          "deliveredText": {
            "type": [
              "string",
              "null"
            ],
            "description": "What the recipient actually received on this message's channel. **Build transcripts from this, not from `body`** — the two differ on the SMS leg, and a transcript built from `body` shows a human operator messages that were never sent."
          },
          "optionLabel": {
            "type": "string",
            "description": "On an inbound chip tap: the chip's label, resolved server-side. Branch on this, never on `body`. Absent when the reply was free text or a suggestion we did not send."
          },
          "optionIndex": {
            "type": "number",
            "description": "1-based among the REPLY chips of the message this answers. Absent on an action chip, which has no number a recipient could have typed."
          },
          "chipAction": {
            "type": "string",
            "enum": [
              "open_url",
              "dial",
              "calendar",
              "view_location",
              "share_location"
            ],
            "description": "Present when an action chip was tapped rather than a reply chip."
          },
          "answersSeq": {
            "type": "number",
            "description": "The `seq` of the outbound message whose chips this reply answers — **not necessarily the message before it**. Someone can read the morning message and tap its chip after the next one has landed, and the reply text alone cannot tell you that. Assuming the previous message is how a late tap gets filed against the wrong question."
          },
          "providerMsgId": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "seq",
          "direction"
        ]
      },
      "FlowCard": {
        "type": "object",
        "description": "Branding for the first message, rendered as an RCS standalone card. Sent as its own leading message, not wrapped around the question: a card description truncates after ~120 visible characters behind a chevron, so text inside a card is *less* visible than text outside it. SMS recipients never see it.",
        "properties": {
          "title": {
            "type": "string",
            "description": "Card heading, ≤200 characters. Defaults to the flow name."
          },
          "imageUrl": {
            "type": "string",
            "description": "Publicly reachable image URL. Google fetches it directly and reads the MIME type from the response's `content-type`, so that header must be present and correct. **Redirects are not followed** — which rules out most CDN vanity URLs and every link shortener."
          },
          "thumbnailUrl": {
            "type": "string",
            "description": "Optional, ≤100 kB. Without one the card shows a blank placeholder while loading."
          },
          "height": {
            "type": "string",
            "enum": [
              "SHORT",
              "MEDIUM",
              "TALL"
            ],
            "description": "112dp · 168dp · 264dp."
          }
        },
        "required": [
          "imageUrl"
        ]
      },
      "FlowStep": {
        "type": "object",
        "description": "One step of a flow. `steps[0]` **is** the opening message — see `POST /v1/conversations`.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Your identifier for this step. Answers are keyed by it in `GET /v1/conversations/{id}/answers`."
          },
          "text": {
            "type": "string",
            "description": "What is asked. Required."
          },
          "suggestions": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 25,
                  "description": "A reply chip."
                },
                {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "text",
                    "action"
                  ],
                  "description": "An action chip: opens a URL, dials, adds a calendar event, or shows a location.",
                  "properties": {
                    "text": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 25
                    },
                    "action": {
                      "oneOf": [
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "openUrl"
                          ],
                          "properties": {
                            "openUrl": {
                              "type": "string",
                              "format": "uri"
                            }
                          }
                        },
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "dial"
                          ],
                          "properties": {
                            "dial": {
                              "type": "string",
                              "pattern": "^\\+[1-9]\\d{6,14}$"
                            }
                          }
                        },
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "calendar"
                          ],
                          "properties": {
                            "calendar": {
                              "type": "object",
                              "additionalProperties": false,
                              "required": [
                                "title",
                                "startTime",
                                "endTime"
                              ],
                              "properties": {
                                "title": {
                                  "type": "string",
                                  "maxLength": 100
                                },
                                "startTime": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "endTime": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "description": {
                                  "type": "string",
                                  "maxLength": 500
                                }
                              }
                            }
                          }
                        },
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "viewLocation"
                          ],
                          "properties": {
                            "viewLocation": {
                              "oneOf": [
                                {
                                  "type": "object",
                                  "additionalProperties": false,
                                  "required": [
                                    "query"
                                  ],
                                  "properties": {
                                    "query": {
                                      "type": "string",
                                      "maxLength": 200
                                    }
                                  }
                                },
                                {
                                  "type": "object",
                                  "additionalProperties": false,
                                  "required": [
                                    "lat",
                                    "long"
                                  ],
                                  "properties": {
                                    "lat": {
                                      "type": "number"
                                    },
                                    "long": {
                                      "type": "number"
                                    },
                                    "label": {
                                      "type": "string",
                                      "maxLength": 100
                                    }
                                  }
                                }
                              ]
                            }
                          }
                        },
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "shareLocation"
                          ],
                          "properties": {
                            "shareLocation": {
                              "const": true
                            }
                          }
                        }
                      ]
                    },
                    "smsFallback": {
                      "type": "string",
                      "maxLength": 320,
                      "description": "What the SMS leg says instead. Defaulted per action type; \"\" says nothing."
                    }
                  }
                }
              ]
            },
            "description": "Chip labels, ≤11. RCS renders tap targets; SMS gets a numbered fallback automatically."
          },
          "smsFallbackText": {
            "type": "string",
            "description": "Overrides the generated SMS text for this step."
          },
          "followUps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FlowFollowUp"
            },
            "description": "Asked only when the answer matches. Depth is capped."
          }
        },
        "required": [
          "id",
          "text"
        ]
      },
      "FlowFollowUp": {
        "type": "object",
        "properties": {
          "when": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "Chip labels or free-text keywords, matched case-insensitively as substrings. A `when` that can match none of its step's `suggestions` is a validation error, not a step that never fires."
          },
          "ask": {
            "$ref": "#/components/schemas/FlowStep"
          }
        },
        "required": [
          "when",
          "ask"
        ]
      },
      "Flow": {
        "type": "object",
        "description": "**This is how you get two-way messaging without keeping a process alive.** Attach a flow when you open a conversation and Cadence answers every reply itself, on its own inbound webhook from Google — a recipient who replies an hour later, after your deploy or your restart, still gets the next step. Read the results from `GET /v1/conversations/{id}/answers`.\n\nIt is the whole definition rather than an id on purpose: editing a flow mid-wave must not change the steps already in flight for someone halfway through it.\n\n**A flow is a fixed script with one level of branching** (`followUps`), not a place to put a model. If your own model writes each reply, register a delivery webhook instead and answer with `POST /v1/conversations/{id}/messages`.\n\nCalled `flow`, not `survey` — appointment confirmations, onboarding sequences and lead qualification are the same machine.",
        "properties": {
          "id": {
            "type": "string"
          },
          "name": {
            "type": "string"
          },
          "steps": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/FlowStep"
            },
            "description": "In order. `steps[0]` is the opening message."
          },
          "closing": {
            "type": "string",
            "description": "Sent after the last answer. **Optional** — a flow whose purpose is to compute something and tell them has no useful closing to write in advance, and ends up sending filler followed by a second message carrying the actual answer. Omit it and the flow finishes silently: the `completed` webhook fires with the answers and your own reply is the last thing the recipient sees. If you omit it, the opt-out instruction must be in `disclaimer`, which is where carriers expect it anyway. An empty string is rejected — that would send a blank message."
          },
          "intro": {
            "type": "string",
            "description": "Prepended to the first message, before step one."
          },
          "disclaimer": {
            "type": "string",
            "description": "Appended to the **first outbound message only**: frequency, rates, HELP and STOP. This is where the opt-out instruction belongs — carriers expect it at the start of a programme."
          },
          "keyword": {
            "type": "string",
            "description": "Inbound keyword that starts this flow. Recorded for reference; routing on it is yours."
          },
          "holdOpen": {
            "type": "boolean",
            "description": "Leave the conversation OPEN after the last answer instead of closing it. A flow normally closes on completion — right for a survey, wrong when `completed` is where your work *starts*. With this set, the `completed` webhook fires, the flow stops asking, and the reply you compute is a normal turn rather than a message sent after the end. Close it yourself with `POST /v1/conversations/{id}/close` when you are done.\n\n**Goes inside `flow`, not beside it.** It was missing from this schema while the prose recommended it, so the first person to follow that advice put it at the top level, got a 200, and watched the conversation close anyway."
          },
          "card": {
            "$ref": "#/components/schemas/FlowCard"
          }
        },
        "required": [
          "id",
          "name",
          "steps"
        ]
      },
      "FlowAnswer": {
        "type": "object",
        "properties": {
          "stepId": {
            "type": "string"
          },
          "question": {
            "type": [
              "string",
              "null"
            ],
            "description": "The step's text, so results read without holding the flow definition alongside."
          },
          "text": {
            "type": "string",
            "description": "What they actually sent."
          },
          "optionIndex": {
            "type": "number",
            "description": "**1-based position among the REPLY chips** — the same number the SMS fallback tells the recipient to reply with, so it means the same thing on both legs. Action chips are not counted and never carry one: there is no number you could reply with to open a URL. Absent on an action tap, which arrives with `optionLabel` and `chipAction` instead. `optionLabel` is provided so you never have to index at all."
          },
          "optionLabel": {
            "type": "string",
            "description": "The matched chip's label."
          },
          "freeText": {
            "type": "boolean",
            "description": "True when they typed something rather than choosing an offered option."
          }
        },
        "required": [
          "stepId",
          "text"
        ]
      },
      "FlowAnswers": {
        "type": "object",
        "description": "Structured results of a server-driven flow — the reason to run one.\n\n`outcome` and `final` exist so one request answers \"is anything more coming\". Without them `done:false, completion:0` meant three different things — still waiting, closed early, and opted out — and telling them apart cost a second call per poll.",
        "properties": {
          "conversationId": {
            "type": "string"
          },
          "state": {
            "type": "string",
            "description": "The conversation's own state, e.g. `OPEN`, `CLOSED`, `CLOSED_OPTOUT`."
          },
          "outcome": {
            "type": "string",
            "enum": [
              "awaiting_reply",
              "completed",
              "closed_early",
              "opted_out"
            ]
          },
          "final": {
            "type": "boolean",
            "description": "True when nothing further will arrive on this conversation."
          },
          "flowId": {
            "type": "string"
          },
          "flowName": {
            "type": "string"
          },
          "done": {
            "type": "boolean",
            "description": "True once the closing has been sent."
          },
          "completion": {
            "type": "number",
            "description": "Fraction of the flow's base steps answered, 0–1. Follow-ups are bonus and do not inflate it."
          },
          "currentStepId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null when the flow is done."
          },
          "answers": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "stepId": {
                  "type": "string"
                },
                "question": {
                  "type": [
                    "string",
                    "null"
                  ],
                  "description": "The step's text, so results read without holding the flow definition alongside."
                },
                "text": {
                  "type": "string",
                  "description": "What they actually sent."
                },
                "optionIndex": {
                  "type": "number",
                  "description": "**1-based position among the REPLY chips** — the same number the SMS fallback tells the recipient to reply with, so it means the same thing on both legs. Action chips are not counted and never carry one: there is no number you could reply with to open a URL. Absent on an action tap, which arrives with `optionLabel` and `chipAction` instead. `optionLabel` is provided so you never have to index at all."
                },
                "optionLabel": {
                  "type": "string",
                  "description": "The matched chip's label."
                },
                "freeText": {
                  "type": "boolean",
                  "description": "True when they typed something rather than choosing an offered option."
                }
              },
              "required": [
                "stepId",
                "text"
              ]
            }
          }
        },
        "required": [
          "flowId",
          "done",
          "completion",
          "answers"
        ]
      },
      "DeliveryWebhook": {
        "type": "object",
        "description": "An endpoint **we POST to** when something happens on your conversations. The opposite direction from `PUT /v1/agents/{id}/webhook`, which points *Google* somewhere other than Cadence.",
        "properties": {
          "id": {
            "type": "string"
          },
          "url": {
            "type": "string",
            "description": "Your https:// endpoint."
          },
          "events": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "reply",
                "fellback",
                "help",
                "optout",
                "optin",
                "completed",
                "ping"
              ]
            }
          },
          "brandId": {
            "type": [
              "string",
              "null"
            ],
            "description": "Null means every brand on the account."
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "active": {
            "type": "boolean",
            "description": "False once disabled, whether by you or by repeated failure."
          },
          "health": {
            "type": "object",
            "properties": {
              "lastSuccessAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "lastFailureAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "lastError": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "consecutiveFailures": {
                "type": "number",
                "description": "Consecutive *events* that failed every retry — not attempts. One slow afternoon does not count."
              },
              "disabledAt": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "disabledReason": {
                "type": [
                  "string",
                  "null"
                ]
              }
            }
          },
          "createdAt": {
            "type": "string"
          }
        },
        "required": [
          "id",
          "url",
          "events",
          "active"
        ]
      },
      "DeliveryEvent": {
        "type": "object",
        "description": "The body we POST. Signed with `Cadence-Signature: t=<unix>,v1=<hex>` — recompute HMAC-SHA256 over the exact string `${t}.${rawBody}` and compare in constant time. **Sign the raw bytes**; re-serialising the parsed JSON often produces identical bytes today, which is the trap — it passes your tests and fails later when a proxy reformats the body or a number normalises. Reject a `t` outside your tolerance (ours is 300s).\n\nAlso sent as headers: `Cadence-Event`, `Cadence-Event-Id`, `Cadence-Delivery-Id`, `Cadence-Attempt` — enough to route and dedupe before parsing the body. Deliveries are at-least-once: dedupe on `Cadence-Event-Id`.",
        "properties": {
          "id": {
            "type": "string",
            "description": "Stable per event. Retries of the same event reuse it — dedupe on this."
          },
          "type": {
            "type": "string",
            "enum": [
              "reply",
              "fellback",
              "help",
              "optout",
              "optin",
              "completed",
              "ping"
            ]
          },
          "createdAt": {
            "type": "string",
            "description": "ISO 8601."
          },
          "data": {
            "type": "object",
            "properties": {
              "conversationId": {
                "type": "string"
              },
              "agentId": {
                "type": "string"
              },
              "brandId": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "to": {
                "type": "string",
                "description": "The recipient, E.164."
              },
              "type": {
                "type": "string",
                "description": "Same as the envelope's, kept for callers that only read `data`."
              },
              "intent": {
                "type": "string",
                "enum": [
                  "free_text",
                  "postback",
                  "optin",
                  "optout",
                  "help",
                  "media"
                ],
                "description": "How the reply was classified. `optout` and `help` arrive as their own event types rather than as a `reply`, so a `reply` carries `free_text` or `postback` in practice; `optin` (START) reverses an opt-out and is reported on the reply that carried it."
              },
              "body": {
                "type": "string",
                "description": "What they sent."
              },
              "value": {
                "type": "string",
                "description": "A chip's postback payload, when the reply was a tap."
              },
              "optionIndex": {
                "type": "number",
                "description": "**1-based** position in the chips offered — the same number the SMS fallback names. Prefer `optionLabel` over indexing."
              },
              "optionLabel": {
                "type": "string"
              },
              "trustLevel": {
                "type": "string"
              },
              "channel": {
                "type": "string",
                "enum": [
                  "rcs",
                  "sms"
                ],
                "description": "On `fellback`: the conversation's channel **after** the fallback. `sms` means the recipient cannot receive RCS and the conversation flipped for good. `rcs` means one message degraded — our payload or your media URL was rejected — and rich sends still work."
              },
              "fallback": {
                "$ref": "#/components/schemas/FellBack",
                "description": "On `fellback`: why, plus `at` (`open` or `send`) and the `providerMsgId` of the SMS that went instead."
              },
              "completion": {
                "type": "number",
                "description": "On `completed`: fraction of base steps answered."
              },
              "resubscribed": {
                "type": "boolean",
                "description": "On `optin`: whether this reversed an opt-out, or was a stray START."
              },
              "answers": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/FlowAnswer"
                },
                "description": "On `completed`: the answers themselves, so a handler woken by this event does not have to call back for the thing the event is about. This **is** the array — it briefly carried the whole `/answers` object with the array nested inside, which is a reasonable way to earn a TypeError from a field called `answers`."
              },
              "flow": {
                "type": "object",
                "properties": {
                  "flowId": {
                    "type": "string"
                  },
                  "flowName": {
                    "type": "string"
                  },
                  "done": {
                    "type": "boolean"
                  },
                  "completion": {
                    "type": "number"
                  },
                  "currentStepId": {
                    "type": [
                      "string",
                      "null"
                    ]
                  }
                },
                "required": [
                  "flowId",
                  "done"
                ]
              }
            },
            "required": [
              "conversationId",
              "agentId"
            ]
          }
        },
        "required": [
          "id",
          "type",
          "createdAt",
          "data"
        ]
      },
      "WebhookDelivery": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "eventId": {
            "type": "string"
          },
          "eventType": {
            "type": "string"
          },
          "conversationId": {
            "type": [
              "string",
              "null"
            ]
          },
          "attempts": {
            "type": "number"
          },
          "status": {
            "type": "string",
            "enum": [
              "delivered",
              "pending",
              "abandoned"
            ]
          },
          "responseStatus": {
            "type": [
              "number",
              "null"
            ],
            "description": "What your endpoint returned."
          },
          "responseBody": {
            "type": [
              "string",
              "null"
            ],
            "description": "First 2 kB of your endpoint's response — kept so \"my handler 500s on your payload\" is answerable."
          },
          "lastError": {
            "type": [
              "string",
              "null"
            ]
          },
          "nextAttemptAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "deliveredAt": {
            "type": [
              "string",
              "null"
            ]
          },
          "createdAt": {
            "type": "string"
          },
          "payload": {
            "$ref": "#/components/schemas/DeliveryEvent"
          }
        },
        "required": [
          "id",
          "eventId",
          "eventType",
          "attempts",
          "status"
        ]
      },
      "ReadinessCheck": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string"
          },
          "label": {
            "type": "string"
          },
          "status": {
            "type": "string",
            "enum": [
              "pass",
              "fail",
              "warn",
              "unknown"
            ]
          },
          "detail": {
            "type": "string"
          },
          "fix": {
            "type": "string",
            "description": "How to resolve it. Present when not passing."
          },
          "blocking": {
            "type": "boolean",
            "description": "A failing blocking check means the submission will very likely be rejected."
          },
          "permanent": {
            "type": "boolean",
            "description": "**This one cannot be undone later.** A separate axis from `blocking`, deliberately: agent artwork is accepted by Google as a non-blocking warning and then freezes at `request-verification`, so filtering on `blocking === true` walks past it into a one-way door. If you automate a decision from this report, gate on `permanent` as well."
          }
        },
        "required": [
          "id",
          "label",
          "status",
          "detail",
          "blocking"
        ]
      },
      "ReadinessReport": {
        "type": "object",
        "properties": {
          "brand": {
            "type": "string"
          },
          "entity": {
            "type": "string"
          },
          "domain": {
            "type": "string"
          },
          "verdict": {
            "type": "string",
            "enum": [
              "ready",
              "needs_work",
              "blocked"
            ]
          },
          "summary": {
            "type": "object",
            "properties": {
              "passed": {
                "type": "number"
              },
              "failed": {
                "type": "number"
              },
              "warnings": {
                "type": "number"
              },
              "total": {
                "type": "number"
              }
            }
          },
          "checks": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ReadinessCheck"
            }
          },
          "artifacts": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "name": {
                  "type": "string"
                },
                "filename": {
                  "type": "string"
                },
                "description": {
                  "type": "string"
                },
                "content": {
                  "type": "string",
                  "description": "The generated document, ready to publish."
                }
              },
              "required": [
                "name",
                "filename",
                "content"
              ]
            },
            "description": "Policy text we generate for what the brand is missing."
          },
          "discovered": {
            "type": "object",
            "properties": {
              "privacyUrl": {
                "type": "string"
              },
              "termsUrl": {
                "type": "string"
              },
              "optinUrl": {
                "type": "string"
              },
              "auto": {
                "type": "array",
                "items": {
                  "type": "string"
                },
                "description": "URLs found by crawling rather than supplied."
              }
            }
          }
        },
        "required": [
          "brand",
          "domain",
          "verdict",
          "summary",
          "checks"
        ]
      },
      "BrandGate": {
        "type": "object",
        "properties": {
          "ready": {
            "type": "boolean",
            "description": "Whether this brand can be submitted for carrier review."
          },
          "reasons": {
            "type": "array",
            "items": {
              "type": "string"
            },
            "description": "What is blocking, when `ready` is false."
          }
        },
        "required": [
          "ready"
        ]
      },
      "LaunchReadiness": {
        "type": "object",
        "properties": {
          "verdict": {
            "type": "string",
            "enum": [
              "ready",
              "needs_work"
            ]
          },
          "checks": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "label": {
                  "type": "string"
                },
                "status": {
                  "type": "string",
                  "enum": [
                    "pass",
                    "fail",
                    "warn",
                    "unknown"
                  ]
                },
                "blocking": {
                  "type": "boolean",
                  "description": "`false` means worth fixing but not submission-stopping. Absent means blocking."
                },
                "detail": {
                  "type": "string"
                },
                "fix": {
                  "type": "string"
                }
              },
              "required": [
                "id",
                "label",
                "status",
                "detail"
              ]
            }
          },
          "prefilled": {
            "type": "object",
            "properties": {
              "optOutMessage": {
                "type": "string",
                "description": "The opt-out confirmation, verbatim — Google's launch review asks for exactly this."
              }
            }
          },
          "blockingCount": {
            "type": "number"
          },
          "agentId": {
            "type": "string"
          },
          "note": {
            "type": "string"
          }
        },
        "required": [
          "verdict",
          "checks",
          "blockingCount"
        ]
      },
      "Usage": {
        "type": "object",
        "properties": {
          "periodStart": {
            "type": "string"
          },
          "periodEnd": {
            "type": "string"
          },
          "lines": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "channel": {
                  "type": "string",
                  "enum": [
                    "rcs",
                    "sms"
                  ]
                },
                "market": {
                  "type": "string",
                  "description": "How **carriers** bill in the destination geography — `perMessage` or `session`. A property of the country, not of this line: a US RCS line reads `perMessage` while it is charged a flat rate per conversation, because the US is a per-message market and RCS is priced per conversation regardless. Reconcile against `billedPer`, not this."
                },
                "billedPer": {
                  "type": "string",
                  "enum": [
                    "conversation",
                    "message"
                  ],
                  "description": "How this line was actually charged. RCS is per conversation everywhere; SMS is per message everywhere, because no carrier sells an SMS session."
                },
                "conversations": {
                  "type": "number"
                },
                "unitPriceCents": {
                  "type": [
                    "number",
                    "null"
                  ],
                  "description": "Per-conversation list price, or null if unpriceable."
                },
                "priceCents": {
                  "type": "number"
                }
              },
              "required": [
                "channel",
                "market",
                "billedPer",
                "conversations",
                "priceCents"
              ]
            }
          },
          "sandboxConversations": {
            "type": "number",
            "description": "Never billed."
          },
          "totalConversations": {
            "type": "number"
          },
          "totalPriceCents": {
            "type": "number"
          },
          "unpricedConversations": {
            "type": "number",
            "description": "Live conversations with no list price — billed as zero, and worth knowing about."
          },
          "note": {
            "type": "string"
          }
        },
        "required": [
          "periodStart",
          "periodEnd",
          "lines",
          "totalPriceCents"
        ]
      },
      "Onboarding": {
        "type": "object",
        "properties": {
          "testDevicePath": {
            "type": "object",
            "properties": {
              "required": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "step": {
                      "type": "string"
                    },
                    "api": {
                      "type": "string"
                    }
                  }
                }
              },
              "notRequired": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "step": {
                      "type": "string"
                    },
                    "why": {
                      "type": "string"
                    }
                  }
                }
              },
              "note": {
                "type": "string"
              }
            },
            "required": [
              "required",
              "notRequired",
              "note"
            ],
            "description": "The six calls that reach your own accepted test devices over real RCS, in order, and the checklist steps that do not gate that (email verification, the readiness check, brand submission, carrier launch)."
          },
          "complete": {
            "type": "boolean"
          },
          "doneCount": {
            "type": "number"
          },
          "total": {
            "type": "number"
          },
          "steps": {
            "type": "array",
            "items": {
              "type": "object",
              "properties": {
                "id": {
                  "type": "string"
                },
                "title": {
                  "type": "string"
                },
                "detail": {
                  "type": "string"
                },
                "done": {
                  "type": "boolean"
                },
                "next": {
                  "type": "boolean",
                  "description": "Present only on the first incomplete step — the single next action."
                },
                "api": {
                  "type": "string",
                  "description": "How an agent does this step, so the checklist is self-describing over the API."
                }
              },
              "required": [
                "id",
                "title",
                "detail",
                "done"
              ]
            }
          }
        },
        "required": [
          "complete",
          "doneCount",
          "total",
          "steps"
        ]
      },
      "Carrier": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "displayName": {
            "type": "string"
          },
          "managementType": {
            "type": "string",
            "enum": [
              "GOOGLE_MANAGED",
              "CARRIER_MANAGED"
            ],
            "description": "`GOOGLE_MANAGED` clears through Google in days. `CARRIER_MANAGED` runs the carrier's own approval and requires a commercial agreement with them before they deliver."
          }
        },
        "required": [
          "name"
        ]
      },
      "OpenConversationRequest": {
        "type": "object",
        "properties": {
          "agentId": {
            "type": "string"
          },
          "brandId": {
            "type": "string"
          },
          "to": {
            "type": "string",
            "pattern": "^\\+[1-9]\\d{6,14}$"
          },
          "useCase": {
            "type": "string",
            "enum": [
              "transactional",
              "promo",
              "conversational",
              "leadgen"
            ]
          },
          "opening": {
            "$ref": "#/components/schemas/Opening"
          },
          "rcsOnly": {
            "type": "boolean",
            "description": "Disable SMS delivery for the entire conversation. Requires a backend that guarantees RCS-only sends. Defaults to false."
          },
          "optin": {
            "type": "object",
            "properties": {
              "method": {
                "type": "string"
              },
              "evidenceRef": {
                "type": "string"
              },
              "capturedAt": {
                "type": "string"
              }
            },
            "required": [
              "method",
              "capturedAt"
            ],
            "additionalProperties": false
          },
          "phiSafe": {
            "type": "boolean",
            "default": false
          },
          "turnCeiling": {
            "type": "integer",
            "exclusiveMinimum": 0,
            "default": 30
          },
          "idempotencyKey": {
            "type": "string"
          },
          "flow": {
            "$ref": "#/components/schemas/Flow"
          }
        },
        "required": [
          "agentId",
          "brandId",
          "to",
          "useCase",
          "optin"
        ],
        "additionalProperties": false,
        "description": "Open a conversation and send the first message.\n\n`opening` is required **unless** you attach a `flow`, in which case the flow's first step is the opening and the server sends it — passing a different opening returns 422 `OPENING_IS_STEP_ONE` rather than asking the recipient nothing and then filing their reply against a step they never saw."
      },
      "SendMessageRequest": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "maxLength": 3072
          },
          "richCard": {
            "type": "object",
            "additionalProperties": {}
          },
          "media": {
            "type": "object",
            "properties": {
              "url": {
                "type": "string",
                "format": "uri"
              },
              "altText": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200
              },
              "thumbnailUrl": {
                "type": "string",
                "format": "uri"
              }
            },
            "required": [
              "url",
              "altText"
            ],
            "additionalProperties": false
          },
          "card": {
            "type": "object",
            "properties": {
              "title": {
                "type": "string",
                "maxLength": 200
              },
              "description": {
                "type": "string",
                "maxLength": 2000
              },
              "media": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri"
                  },
                  "altText": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200
                  },
                  "thumbnailUrl": {
                    "type": "string",
                    "format": "uri"
                  },
                  "height": {
                    "type": "string",
                    "enum": [
                      "SHORT",
                      "MEDIUM",
                      "TALL"
                    ]
                  }
                },
                "required": [
                  "url",
                  "altText"
                ],
                "additionalProperties": false
              },
              "suggestions": {
                "type": "array",
                "items": {
                  "anyOf": [
                    {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 25
                    },
                    {
                      "type": "object",
                      "properties": {
                        "text": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 25
                        },
                        "action": {
                          "anyOf": [
                            {
                              "type": "object",
                              "properties": {
                                "openUrl": {
                                  "type": "string",
                                  "format": "uri"
                                }
                              },
                              "required": [
                                "openUrl"
                              ],
                              "additionalProperties": false
                            },
                            {
                              "type": "object",
                              "properties": {
                                "dial": {
                                  "type": "string",
                                  "pattern": "^\\+[1-9]\\d{6,14}$"
                                }
                              },
                              "required": [
                                "dial"
                              ],
                              "additionalProperties": false
                            },
                            {
                              "type": "object",
                              "properties": {
                                "calendar": {
                                  "type": "object",
                                  "properties": {
                                    "title": {
                                      "type": "string",
                                      "minLength": 1,
                                      "maxLength": 100
                                    },
                                    "startTime": {
                                      "type": "string",
                                      "format": "date-time"
                                    },
                                    "endTime": {
                                      "type": "string",
                                      "format": "date-time"
                                    },
                                    "description": {
                                      "type": "string",
                                      "maxLength": 500
                                    }
                                  },
                                  "required": [
                                    "title",
                                    "startTime",
                                    "endTime"
                                  ],
                                  "additionalProperties": false
                                }
                              },
                              "required": [
                                "calendar"
                              ],
                              "additionalProperties": false
                            },
                            {
                              "type": "object",
                              "properties": {
                                "viewLocation": {
                                  "anyOf": [
                                    {
                                      "type": "object",
                                      "properties": {
                                        "query": {
                                          "type": "string",
                                          "minLength": 1,
                                          "maxLength": 200
                                        }
                                      },
                                      "required": [
                                        "query"
                                      ],
                                      "additionalProperties": false
                                    },
                                    {
                                      "type": "object",
                                      "properties": {
                                        "lat": {
                                          "type": "number"
                                        },
                                        "long": {
                                          "type": "number"
                                        },
                                        "label": {
                                          "type": "string",
                                          "maxLength": 100
                                        }
                                      },
                                      "required": [
                                        "lat",
                                        "long"
                                      ],
                                      "additionalProperties": false
                                    }
                                  ]
                                }
                              },
                              "required": [
                                "viewLocation"
                              ],
                              "additionalProperties": false
                            },
                            {
                              "type": "object",
                              "properties": {
                                "shareLocation": {
                                  "type": "boolean",
                                  "const": true
                                }
                              },
                              "required": [
                                "shareLocation"
                              ],
                              "additionalProperties": false
                            }
                          ]
                        },
                        "smsFallback": {
                          "type": "string",
                          "maxLength": 320
                        }
                      },
                      "required": [
                        "text",
                        "action"
                      ],
                      "additionalProperties": false
                    }
                  ]
                },
                "maxItems": 4
              },
              "orientation": {
                "type": "string",
                "enum": [
                  "VERTICAL",
                  "HORIZONTAL"
                ]
              },
              "thumbnailAlignment": {
                "type": "string",
                "enum": [
                  "LEFT",
                  "RIGHT"
                ]
              }
            },
            "additionalProperties": false
          },
          "carousel": {
            "type": "object",
            "properties": {
              "width": {
                "type": "string",
                "enum": [
                  "SMALL",
                  "MEDIUM"
                ]
              },
              "cards": {
                "type": "array",
                "items": {
                  "type": "object",
                  "properties": {
                    "title": {
                      "type": "string",
                      "maxLength": 200
                    },
                    "description": {
                      "type": "string",
                      "maxLength": 2000
                    },
                    "media": {
                      "type": "object",
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri"
                        },
                        "altText": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 200
                        },
                        "thumbnailUrl": {
                          "type": "string",
                          "format": "uri"
                        },
                        "height": {
                          "type": "string",
                          "enum": [
                            "SHORT",
                            "MEDIUM",
                            "TALL"
                          ]
                        }
                      },
                      "required": [
                        "url",
                        "altText"
                      ],
                      "additionalProperties": false
                    },
                    "suggestions": {
                      "type": "array",
                      "items": {
                        "anyOf": [
                          {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 25
                          },
                          {
                            "type": "object",
                            "properties": {
                              "text": {
                                "type": "string",
                                "minLength": 1,
                                "maxLength": 25
                              },
                              "action": {
                                "anyOf": [
                                  {
                                    "type": "object",
                                    "properties": {
                                      "openUrl": {
                                        "type": "string",
                                        "format": "uri"
                                      }
                                    },
                                    "required": [
                                      "openUrl"
                                    ],
                                    "additionalProperties": false
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "dial": {
                                        "type": "string",
                                        "pattern": "^\\+[1-9]\\d{6,14}$"
                                      }
                                    },
                                    "required": [
                                      "dial"
                                    ],
                                    "additionalProperties": false
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "calendar": {
                                        "type": "object",
                                        "properties": {
                                          "title": {
                                            "type": "string",
                                            "minLength": 1,
                                            "maxLength": 100
                                          },
                                          "startTime": {
                                            "type": "string",
                                            "format": "date-time"
                                          },
                                          "endTime": {
                                            "type": "string",
                                            "format": "date-time"
                                          },
                                          "description": {
                                            "type": "string",
                                            "maxLength": 500
                                          }
                                        },
                                        "required": [
                                          "title",
                                          "startTime",
                                          "endTime"
                                        ],
                                        "additionalProperties": false
                                      }
                                    },
                                    "required": [
                                      "calendar"
                                    ],
                                    "additionalProperties": false
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "viewLocation": {
                                        "anyOf": [
                                          {
                                            "type": "object",
                                            "properties": {
                                              "query": {
                                                "type": "string",
                                                "minLength": 1,
                                                "maxLength": 200
                                              }
                                            },
                                            "required": [
                                              "query"
                                            ],
                                            "additionalProperties": false
                                          },
                                          {
                                            "type": "object",
                                            "properties": {
                                              "lat": {
                                                "type": "number"
                                              },
                                              "long": {
                                                "type": "number"
                                              },
                                              "label": {
                                                "type": "string",
                                                "maxLength": 100
                                              }
                                            },
                                            "required": [
                                              "lat",
                                              "long"
                                            ],
                                            "additionalProperties": false
                                          }
                                        ]
                                      }
                                    },
                                    "required": [
                                      "viewLocation"
                                    ],
                                    "additionalProperties": false
                                  },
                                  {
                                    "type": "object",
                                    "properties": {
                                      "shareLocation": {
                                        "type": "boolean",
                                        "const": true
                                      }
                                    },
                                    "required": [
                                      "shareLocation"
                                    ],
                                    "additionalProperties": false
                                  }
                                ]
                              },
                              "smsFallback": {
                                "type": "string",
                                "maxLength": 320
                              }
                            },
                            "required": [
                              "text",
                              "action"
                            ],
                            "additionalProperties": false
                          }
                        ]
                      },
                      "maxItems": 4
                    }
                  },
                  "additionalProperties": false
                },
                "minItems": 2,
                "maxItems": 10
              }
            },
            "required": [
              "cards"
            ],
            "additionalProperties": false
          },
          "suggestions": {
            "type": "array",
            "items": {
              "anyOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 25
                },
                {
                  "type": "object",
                  "properties": {
                    "text": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 25
                    },
                    "action": {
                      "anyOf": [
                        {
                          "type": "object",
                          "properties": {
                            "openUrl": {
                              "type": "string",
                              "format": "uri"
                            }
                          },
                          "required": [
                            "openUrl"
                          ],
                          "additionalProperties": false
                        },
                        {
                          "type": "object",
                          "properties": {
                            "dial": {
                              "type": "string",
                              "pattern": "^\\+[1-9]\\d{6,14}$"
                            }
                          },
                          "required": [
                            "dial"
                          ],
                          "additionalProperties": false
                        },
                        {
                          "type": "object",
                          "properties": {
                            "calendar": {
                              "type": "object",
                              "properties": {
                                "title": {
                                  "type": "string",
                                  "minLength": 1,
                                  "maxLength": 100
                                },
                                "startTime": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "endTime": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "description": {
                                  "type": "string",
                                  "maxLength": 500
                                }
                              },
                              "required": [
                                "title",
                                "startTime",
                                "endTime"
                              ],
                              "additionalProperties": false
                            }
                          },
                          "required": [
                            "calendar"
                          ],
                          "additionalProperties": false
                        },
                        {
                          "type": "object",
                          "properties": {
                            "viewLocation": {
                              "anyOf": [
                                {
                                  "type": "object",
                                  "properties": {
                                    "query": {
                                      "type": "string",
                                      "minLength": 1,
                                      "maxLength": 200
                                    }
                                  },
                                  "required": [
                                    "query"
                                  ],
                                  "additionalProperties": false
                                },
                                {
                                  "type": "object",
                                  "properties": {
                                    "lat": {
                                      "type": "number"
                                    },
                                    "long": {
                                      "type": "number"
                                    },
                                    "label": {
                                      "type": "string",
                                      "maxLength": 100
                                    }
                                  },
                                  "required": [
                                    "lat",
                                    "long"
                                  ],
                                  "additionalProperties": false
                                }
                              ]
                            }
                          },
                          "required": [
                            "viewLocation"
                          ],
                          "additionalProperties": false
                        },
                        {
                          "type": "object",
                          "properties": {
                            "shareLocation": {
                              "type": "boolean",
                              "const": true
                            }
                          },
                          "required": [
                            "shareLocation"
                          ],
                          "additionalProperties": false
                        }
                      ]
                    },
                    "smsFallback": {
                      "type": "string",
                      "maxLength": 320
                    }
                  },
                  "required": [
                    "text",
                    "action"
                  ],
                  "additionalProperties": false
                }
              ]
            },
            "maxItems": 11
          },
          "smsFallbackText": {
            "type": "string",
            "maxLength": 1600
          },
          "idempotencyKey": {
            "type": "string"
          },
          "expiresIn": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2592000
          },
          "expiresAt": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": false,
        "description": "Unknown fields are **rejected**, not ignored: someone posted `{\"direction\":\"in\",\"text\":\"…\"}` and got a 200 while the text went out to the recipient as the brand. A typo in a field name must never become a message to a real person."
      },
      "ReadinessCheckRequest": {
        "type": "object",
        "properties": {
          "brand": {
            "type": "string",
            "minLength": 1,
            "maxLength": 120
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 200
          },
          "entity": {
            "type": "string",
            "maxLength": 160
          },
          "website": {
            "type": "string",
            "minLength": 3,
            "maxLength": 300
          },
          "privacyUrl": {
            "type": "string",
            "maxLength": 300
          },
          "termsUrl": {
            "type": "string",
            "maxLength": 300
          },
          "optinUrl": {
            "type": "string",
            "maxLength": 300
          },
          "logoUrl": {
            "type": "string",
            "maxLength": 500
          },
          "bannerUrl": {
            "type": "string",
            "maxLength": 500
          },
          "useCase": {
            "type": "string",
            "maxLength": 400
          },
          "supportEmail": {
            "type": "string",
            "maxLength": 160
          }
        },
        "required": [
          "brand",
          "email",
          "website"
        ],
        "additionalProperties": false
      },
      "AccountSignupRequest": {
        "type": "object",
        "properties": {
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 200
          },
          "password": {
            "type": "string",
            "maxLength": 200
          },
          "name": {
            "type": "string",
            "maxLength": 120
          },
          "company": {
            "type": "string",
            "maxLength": 160
          },
          "acceptTerms": {
            "type": "boolean",
            "const": true
          }
        },
        "required": [
          "email",
          "password",
          "acceptTerms"
        ],
        "additionalProperties": false
      },
      "Opening": {
        "type": "object",
        "properties": {
          "text": {
            "type": "string",
            "description": "≤3072 characters."
          },
          "disclaimer": {
            "type": "string",
            "description": "Frequency, rates, HELP and STOP — appended to this message and this message only, on both the RCS text and the SMS fallback so the two cannot drift apart. `flow` has always had this; without it, a webhook-driven conversation had to paste the carrier disclosure into two fields by hand. Carriers expect it at the start of a programme."
          },
          "richCard": {
            "type": "object",
            "description": "A Google RBM rich card, passed through untouched. Prefer `card` or `carousel`: chips inside a hand-rolled `richCard` are not numbered by us, so taps on them arrive with no `optionLabel` and cannot be attributed to a question. A message carries either `richCard` or `text`, never both.",
            "additionalProperties": true
          },
          "card": {
            "type": "object",
            "additionalProperties": false,
            "properties": {
              "title": {
                "type": "string",
                "maxLength": 200,
                "description": "≤200 characters, but a MEDIUM carousel card is 232 DP wide, so anything past a short phrase truncates rather than wraps."
              },
              "description": {
                "type": "string",
                "maxLength": 2000,
                "description": "≤2000 characters."
              },
              "media": {
                "type": "object",
                "additionalProperties": false,
                "required": [
                  "url",
                  "altText"
                ],
                "properties": {
                  "url": {
                    "type": "string",
                    "format": "uri",
                    "description": "https only — Google fetches it themselves."
                  },
                  "altText": {
                    "type": "string",
                    "minLength": 1,
                    "maxLength": 200,
                    "description": "What the file says, for anyone who cannot see it. RBM has nowhere to put this, so it does not travel to Google — it is what the SMS leg carries instead of the image."
                  },
                  "thumbnailUrl": {
                    "type": "string",
                    "format": "uri",
                    "description": "Max 100 kB; 50 kB or less is better."
                  },
                  "height": {
                    "type": "string",
                    "enum": [
                      "SHORT",
                      "MEDIUM",
                      "TALL"
                    ],
                    "description": "112, 168 or 264 DP. Google recommends a different aspect ratio for each (7:2, 21:9, 3:2), so an image cropped for one height is cropped wrong for another. Defaults to MEDIUM: TALL pushes the text below the fold on smaller handsets."
                  }
                }
              },
              "suggestions": {
                "type": "array",
                "items": {
                  "oneOf": [
                    {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 25,
                      "description": "A reply chip."
                    },
                    {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "text",
                        "action"
                      ],
                      "description": "An action chip: opens a URL, dials, adds a calendar event, or shows a location.",
                      "properties": {
                        "text": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 25
                        },
                        "action": {
                          "oneOf": [
                            {
                              "type": "object",
                              "additionalProperties": false,
                              "required": [
                                "openUrl"
                              ],
                              "properties": {
                                "openUrl": {
                                  "type": "string",
                                  "format": "uri"
                                }
                              }
                            },
                            {
                              "type": "object",
                              "additionalProperties": false,
                              "required": [
                                "dial"
                              ],
                              "properties": {
                                "dial": {
                                  "type": "string",
                                  "pattern": "^\\+[1-9]\\d{6,14}$"
                                }
                              }
                            },
                            {
                              "type": "object",
                              "additionalProperties": false,
                              "required": [
                                "calendar"
                              ],
                              "properties": {
                                "calendar": {
                                  "type": "object",
                                  "additionalProperties": false,
                                  "required": [
                                    "title",
                                    "startTime",
                                    "endTime"
                                  ],
                                  "properties": {
                                    "title": {
                                      "type": "string",
                                      "maxLength": 100
                                    },
                                    "startTime": {
                                      "type": "string",
                                      "format": "date-time"
                                    },
                                    "endTime": {
                                      "type": "string",
                                      "format": "date-time"
                                    },
                                    "description": {
                                      "type": "string",
                                      "maxLength": 500
                                    }
                                  }
                                }
                              }
                            },
                            {
                              "type": "object",
                              "additionalProperties": false,
                              "required": [
                                "viewLocation"
                              ],
                              "properties": {
                                "viewLocation": {
                                  "oneOf": [
                                    {
                                      "type": "object",
                                      "additionalProperties": false,
                                      "required": [
                                        "query"
                                      ],
                                      "properties": {
                                        "query": {
                                          "type": "string",
                                          "maxLength": 200
                                        }
                                      }
                                    },
                                    {
                                      "type": "object",
                                      "additionalProperties": false,
                                      "required": [
                                        "lat",
                                        "long"
                                      ],
                                      "properties": {
                                        "lat": {
                                          "type": "number"
                                        },
                                        "long": {
                                          "type": "number"
                                        },
                                        "label": {
                                          "type": "string",
                                          "maxLength": 100
                                        }
                                      }
                                    }
                                  ]
                                }
                              }
                            },
                            {
                              "type": "object",
                              "additionalProperties": false,
                              "required": [
                                "shareLocation"
                              ],
                              "properties": {
                                "shareLocation": {
                                  "const": true
                                }
                              }
                            }
                          ]
                        },
                        "smsFallback": {
                          "type": "string",
                          "maxLength": 320,
                          "description": "What the SMS leg says instead. Defaulted per action type; \"\" says nothing."
                        }
                      }
                    }
                  ]
                },
                "description": "At most 4 — a per-card limit, separate from and much smaller than the 11-chip list below the message. Taps on these resolve to `optionLabel` exactly like the message-level chips, because their postback data is numbered in the same sequence."
              },
              "orientation": {
                "type": "string",
                "enum": [
                  "VERTICAL",
                  "HORIZONTAL"
                ],
                "description": "HORIZONTAL puts the image beside the text instead of above it. A horizontal card carrying media must also carry a title, description or suggestions (`HORIZONTAL_CARD_NEEDS_CONTENT`) — an image alone has nothing to sit beside."
              },
              "thumbnailImageAlignment": {
                "type": "string",
                "enum": [
                  "LEFT",
                  "RIGHT"
                ],
                "description": "Which side the image sits on. Only meaningful with HORIZONTAL."
              }
            },
            "description": "A single rich card, built for you. Cannot be combined with `text`, `media`, `carousel` or `richCard` — RBM's contentMessage holds exactly one of them (`MULTIPLE_MESSAGE_BODIES`); the card's own `title` and `description` carry the words."
          },
          "carousel": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "cards"
            ],
            "properties": {
              "width": {
                "type": "string",
                "enum": [
                  "SMALL",
                  "MEDIUM"
                ],
                "description": "120 or 232 DP. Defaults to MEDIUM."
              },
              "cards": {
                "type": "array",
                "minItems": 2,
                "maxItems": 10,
                "items": {
                  "type": "object",
                  "additionalProperties": false,
                  "properties": {
                    "title": {
                      "type": "string",
                      "maxLength": 200,
                      "description": "≤200 characters, but a MEDIUM carousel card is 232 DP wide, so anything past a short phrase truncates rather than wraps."
                    },
                    "description": {
                      "type": "string",
                      "maxLength": 2000,
                      "description": "≤2000 characters."
                    },
                    "media": {
                      "type": "object",
                      "additionalProperties": false,
                      "required": [
                        "url",
                        "altText"
                      ],
                      "properties": {
                        "url": {
                          "type": "string",
                          "format": "uri",
                          "description": "https only — Google fetches it themselves."
                        },
                        "altText": {
                          "type": "string",
                          "minLength": 1,
                          "maxLength": 200,
                          "description": "What the file says, for anyone who cannot see it. RBM has nowhere to put this, so it does not travel to Google — it is what the SMS leg carries instead of the image."
                        },
                        "thumbnailUrl": {
                          "type": "string",
                          "format": "uri",
                          "description": "Max 100 kB; 50 kB or less is better."
                        },
                        "height": {
                          "type": "string",
                          "enum": [
                            "SHORT",
                            "MEDIUM",
                            "TALL"
                          ],
                          "description": "112, 168 or 264 DP. Google recommends a different aspect ratio for each (7:2, 21:9, 3:2), so an image cropped for one height is cropped wrong for another. Defaults to MEDIUM: TALL pushes the text below the fold on smaller handsets."
                        }
                      }
                    },
                    "suggestions": {
                      "type": "array",
                      "items": {
                        "oneOf": [
                          {
                            "type": "string",
                            "minLength": 1,
                            "maxLength": 25,
                            "description": "A reply chip."
                          },
                          {
                            "type": "object",
                            "additionalProperties": false,
                            "required": [
                              "text",
                              "action"
                            ],
                            "description": "An action chip: opens a URL, dials, adds a calendar event, or shows a location.",
                            "properties": {
                              "text": {
                                "type": "string",
                                "minLength": 1,
                                "maxLength": 25
                              },
                              "action": {
                                "oneOf": [
                                  {
                                    "type": "object",
                                    "additionalProperties": false,
                                    "required": [
                                      "openUrl"
                                    ],
                                    "properties": {
                                      "openUrl": {
                                        "type": "string",
                                        "format": "uri"
                                      }
                                    }
                                  },
                                  {
                                    "type": "object",
                                    "additionalProperties": false,
                                    "required": [
                                      "dial"
                                    ],
                                    "properties": {
                                      "dial": {
                                        "type": "string",
                                        "pattern": "^\\+[1-9]\\d{6,14}$"
                                      }
                                    }
                                  },
                                  {
                                    "type": "object",
                                    "additionalProperties": false,
                                    "required": [
                                      "calendar"
                                    ],
                                    "properties": {
                                      "calendar": {
                                        "type": "object",
                                        "additionalProperties": false,
                                        "required": [
                                          "title",
                                          "startTime",
                                          "endTime"
                                        ],
                                        "properties": {
                                          "title": {
                                            "type": "string",
                                            "maxLength": 100
                                          },
                                          "startTime": {
                                            "type": "string",
                                            "format": "date-time"
                                          },
                                          "endTime": {
                                            "type": "string",
                                            "format": "date-time"
                                          },
                                          "description": {
                                            "type": "string",
                                            "maxLength": 500
                                          }
                                        }
                                      }
                                    }
                                  },
                                  {
                                    "type": "object",
                                    "additionalProperties": false,
                                    "required": [
                                      "viewLocation"
                                    ],
                                    "properties": {
                                      "viewLocation": {
                                        "oneOf": [
                                          {
                                            "type": "object",
                                            "additionalProperties": false,
                                            "required": [
                                              "query"
                                            ],
                                            "properties": {
                                              "query": {
                                                "type": "string",
                                                "maxLength": 200
                                              }
                                            }
                                          },
                                          {
                                            "type": "object",
                                            "additionalProperties": false,
                                            "required": [
                                              "lat",
                                              "long"
                                            ],
                                            "properties": {
                                              "lat": {
                                                "type": "number"
                                              },
                                              "long": {
                                                "type": "number"
                                              },
                                              "label": {
                                                "type": "string",
                                                "maxLength": 100
                                              }
                                            }
                                          }
                                        ]
                                      }
                                    }
                                  },
                                  {
                                    "type": "object",
                                    "additionalProperties": false,
                                    "required": [
                                      "shareLocation"
                                    ],
                                    "properties": {
                                      "shareLocation": {
                                        "const": true
                                      }
                                    }
                                  }
                                ]
                              },
                              "smsFallback": {
                                "type": "string",
                                "maxLength": 320,
                                "description": "What the SMS leg says instead. Defaulted per action type; \"\" says nothing."
                              }
                            }
                          }
                        ]
                      },
                      "description": "At most 4 — a per-card limit, separate from and much smaller than the 11-chip list below the message. Taps on these resolve to `optionLabel` exactly like the message-level chips, because their postback data is numbered in the same sequence."
                    }
                  }
                },
                "description": "2 to 10. One card in a carousel is a standalone card drawn worse, and Google refuses it."
              }
            },
            "description": "A swipeable row of cards. Always vertical — there is no orientation to choose. The whole payload must serialise under 250 KB (`RICHCARD_TOO_LARGE`), which ten 2000-character descriptions can approach; image URLs cost only their length."
          },
          "suggestions": {
            "type": "array",
            "items": {
              "oneOf": [
                {
                  "type": "string",
                  "minLength": 1,
                  "maxLength": 25,
                  "description": "A reply chip."
                },
                {
                  "type": "object",
                  "additionalProperties": false,
                  "required": [
                    "text",
                    "action"
                  ],
                  "description": "An action chip: opens a URL, dials, adds a calendar event, or shows a location.",
                  "properties": {
                    "text": {
                      "type": "string",
                      "minLength": 1,
                      "maxLength": 25
                    },
                    "action": {
                      "oneOf": [
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "openUrl"
                          ],
                          "properties": {
                            "openUrl": {
                              "type": "string",
                              "format": "uri"
                            }
                          }
                        },
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "dial"
                          ],
                          "properties": {
                            "dial": {
                              "type": "string",
                              "pattern": "^\\+[1-9]\\d{6,14}$"
                            }
                          }
                        },
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "calendar"
                          ],
                          "properties": {
                            "calendar": {
                              "type": "object",
                              "additionalProperties": false,
                              "required": [
                                "title",
                                "startTime",
                                "endTime"
                              ],
                              "properties": {
                                "title": {
                                  "type": "string",
                                  "maxLength": 100
                                },
                                "startTime": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "endTime": {
                                  "type": "string",
                                  "format": "date-time"
                                },
                                "description": {
                                  "type": "string",
                                  "maxLength": 500
                                }
                              }
                            }
                          }
                        },
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "viewLocation"
                          ],
                          "properties": {
                            "viewLocation": {
                              "oneOf": [
                                {
                                  "type": "object",
                                  "additionalProperties": false,
                                  "required": [
                                    "query"
                                  ],
                                  "properties": {
                                    "query": {
                                      "type": "string",
                                      "maxLength": 200
                                    }
                                  }
                                },
                                {
                                  "type": "object",
                                  "additionalProperties": false,
                                  "required": [
                                    "lat",
                                    "long"
                                  ],
                                  "properties": {
                                    "lat": {
                                      "type": "number"
                                    },
                                    "long": {
                                      "type": "number"
                                    },
                                    "label": {
                                      "type": "string",
                                      "maxLength": 100
                                    }
                                  }
                                }
                              ]
                            }
                          }
                        },
                        {
                          "type": "object",
                          "additionalProperties": false,
                          "required": [
                            "shareLocation"
                          ],
                          "properties": {
                            "shareLocation": {
                              "const": true
                            }
                          }
                        }
                      ]
                    },
                    "smsFallback": {
                      "type": "string",
                      "maxLength": 320,
                      "description": "What the SMS leg says instead. Defaulted per action type; \"\" says nothing."
                    }
                  }
                }
              ]
            },
            "description": "Chips, at most 11. A string is a reply chip; an object is an action chip (open a URL, dial, calendar, location). Action chips are excluded from SMS numbering and carry no `optionIndex`."
          },
          "media": {
            "type": "object",
            "additionalProperties": false,
            "required": [
              "url",
              "altText"
            ],
            "description": "An image, chart, PDF or short video to send with this message. Google fetches the URL from their own infrastructure, so it must be publicly reachable over HTTPS — anything behind a session, a VPN or an expired signed URL fetches as a 403 and the message arrives empty. Cannot be combined with `richCard` (`MEDIA_AND_RICHCARD`); `media` builds the card for you when there is text to carry.",
            "properties": {
              "url": {
                "type": "string",
                "format": "uri",
                "description": "https only."
              },
              "altText": {
                "type": "string",
                "minLength": 1,
                "maxLength": 200,
                "description": "What the file says, for anyone who cannot see it. Required — RCS renders media inline, and on the SMS leg this text is all the recipient gets."
              },
              "thumbnailUrl": {
                "type": "string",
                "format": "uri",
                "description": "A still for video or PDF. Google generates one when omitted, where it can."
              }
            }
          },
          "expiresIn": {
            "type": "integer",
            "minimum": 1,
            "maximum": 2592000,
            "description": "Seconds this message stays worth delivering. RBM holds a message for an offline handset up to 30 days and delivers it whenever the device returns — for anything tied to a moment that is a data problem, not a cosmetic one: a daily check-in that lands three days late has its answer recorded against the wrong day, and nothing downstream can tell. On the SMS leg this becomes the carrier validity period, which our SMS provider caps at 10 hours, so a longer deadline is exact on RCS and clamped on SMS."
          },
          "expiresAt": {
            "type": "string",
            "description": "An absolute deadline instead of a duration. Send one or the other, never both (`409 CONFLICTING_EXPIRY`). Already in the past is refused (`EXPIRY_IN_PAST`) rather than sent — a message that succeeds and is never delivered is the loudest possible silence."
          },
          "smsFallbackText": {
            "type": "string",
            "description": "≤1600 characters. **Required whenever `suggestions` or `richCard` is present** — omitting it is `400 MISSING_SMS_FALLBACK`, not a generated default. This said \"generated from suggestions when omitted\", which was true of a branch the validator makes unreachable. Any action chips' content is appended to what you write here, unless your text already contains it."
          }
        }
      }
    },
    "securitySchemes": {
      "bearerAuth": {
        "type": "http",
        "scheme": "bearer",
        "description": "An `sk_sandbox_` or `sk_live_` API key from `POST /v1/signup`, or a dashboard session token from `POST /v1/accounts/login`. Account-management routes accept either; conversation routes want a key."
      }
    }
  },
  "security": [
    {
      "bearerAuth": []
    }
  ],
  "paths": {
    "/v1/signup": {
      "post": {
        "summary": "Get a sandbox key (start here)",
        "description": "The front door. Pass an `email` and you get a real account with a key **linked to it** — that key can then create brands, run readiness checks, sync an agent to Google and mint itself a live key, with no dashboard and no password. Pass no email and the key can only simulate sends: fine for kicking the tyres, a dead end for anything else.\n\nAn email that already has an account is refused rather than served — otherwise anyone who knows your address could mint credentials inside your account.",
        "operationId": "post_v1_signup",
        "tags": [
          "Onboarding"
        ],
        "security": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "Creates and links an account. Omit for an unlinked send-only key."
                  },
                  "name": {
                    "type": "string"
                  },
                  "company": {
                    "type": "string"
                  },
                  "label": {
                    "type": "string",
                    "description": "Shown in the key list."
                  },
                  "acceptTerms": {
                    "type": "boolean",
                    "description": "Records terms acceptance, which a live key later requires."
                  }
                }
              },
              "example": {
                "email": "you@example.com",
                "company": "Basal",
                "acceptTerms": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A sandbox key, shown once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_key": {
                      "type": "string",
                      "description": "Store it now — it is never shown again."
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "sandbox"
                      ]
                    },
                    "account_linked": {
                      "type": "boolean",
                      "description": "False means this key can only send; it cannot manage brands or be upgraded."
                    },
                    "verification_sent": {
                      "type": "boolean"
                    },
                    "terms_version": {
                      "type": "string"
                    },
                    "terms_accepted": {
                      "type": "boolean"
                    },
                    "terms_url": {
                      "type": "string"
                    },
                    "next": {
                      "type": "string",
                      "description": "What this key can do, precisely."
                    },
                    "docs": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "api_key",
                    "kind",
                    "account_linked"
                  ]
                },
                "example": {
                  "api_key": "sk_sandbox_…",
                  "kind": "sandbox",
                  "account_linked": true,
                  "verification_sent": true
                }
              }
            }
          },
          "409": {
            "description": "An account already exists for that email.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "ACCOUNT_EXISTS",
                  "message": "An account already exists for that email, so this route won't issue a key for it.",
                  "remediation": "Sign in and create a key there, or POST /v1/accounts/request-reset."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/onboarding": {
      "get": {
        "summary": "First-run checklist",
        "description": "Computed from real account state, and each step names the API call that completes it — so it is as useful to an agent as to a person.",
        "operationId": "get_v1_onboarding",
        "tags": [
          "Onboarding"
        ],
        "responses": {
          "200": {
            "description": "Checklist.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Onboarding"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/eta": {
      "get": {
        "summary": "Observed verification times",
        "description": "Median of our own observed submitted → verified durations. Google publishes no SLA and we quote no industry estimate; `null` means not enough completed verifications yet, which is shown rather than guessed.",
        "operationId": "get_v1_eta",
        "tags": [
          "Onboarding"
        ],
        "security": [],
        "responses": {
          "200": {
            "description": "Medians, or nulls with a note.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brandVerifyHours": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "brandSamples": {
                      "type": "number"
                    },
                    "agentVerifyHours": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "agentSamples": {
                      "type": "number"
                    },
                    "basis": {
                      "type": "string"
                    },
                    "note": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/signup": {
      "post": {
        "summary": "Create a dashboard account (human path)",
        "description": "**Two signup routes exist and this is the one for people.** It takes a password and returns a dashboard session. If you are a program, use `POST /v1/signup` instead: an email, no password, and a working API key in the response.",
        "operationId": "post_v1_accounts_signup",
        "tags": [
          "Accounts"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/AccountSignupRequest"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Account and session token.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/Account"
                    },
                    "token": {
                      "type": "string",
                      "description": "Session token. Send as `Authorization: Bearer <token>`."
                    },
                    "termsVersion": {
                      "type": "string"
                    },
                    "verificationSent": {
                      "type": "boolean"
                    },
                    "emailConfigured": {
                      "type": "boolean"
                    },
                    "verifyLink": {
                      "type": "string",
                      "description": "Only returned when no mailer is configured, so a configured deployment never puts a credential in a response body."
                    }
                  },
                  "required": [
                    "account",
                    "token"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Weak password or malformed body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "WEAK_PASSWORD",
                  "message": "Use at least 12 characters."
                }
              }
            }
          },
          "409": {
            "description": "Email already registered.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "EMAIL_TAKEN"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited per IP and per targeted account. Honour `Retry-After`.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/login": {
      "post": {
        "summary": "Sign in",
        "description": "Sets a session cookie and returns the same token for cross-origin callers.",
        "operationId": "post_v1_accounts_login",
        "tags": [
          "Accounts"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "email",
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Signed in.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/Account"
                    },
                    "token": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "account",
                    "token"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing email or password.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Incorrect email or password.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_CREDENTIALS"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited per IP and per targeted account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/logout": {
      "post": {
        "summary": "Sign out",
        "description": "Destroys the session and clears the cookie. API keys are unaffected.",
        "operationId": "post_v1_accounts_logout",
        "tags": [
          "Accounts"
        ],
        "responses": {
          "200": {
            "description": "Signed out.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No usable session cookie, session token or API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "summary": "The current account",
        "description": "Works with a session token or an account-linked API key. Carries any email change that has been started and not finished — without it, a two-stage flow had no observable second stage and the only way to know one was in flight was to have kept the response that began it.",
        "operationId": "get_v1_me",
        "tags": [
          "Accounts"
        ],
        "responses": {
          "200": {
            "description": "The account, and any change in flight.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "account": {
                      "$ref": "#/components/schemas/Account"
                    },
                    "pendingEmailChange": {
                      "oneOf": [
                        {
                          "type": "object",
                          "properties": {
                            "email": {
                              "type": "string",
                              "description": "The address it would move to."
                            },
                            "stage": {
                              "type": "string",
                              "enum": [
                                "awaiting_approval",
                                "awaiting_confirmation"
                              ]
                            },
                            "expiresAt": {
                              "type": "string",
                              "description": "Links last an hour."
                            },
                            "note": {
                              "type": "string"
                            },
                            "remediation": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "email",
                            "stage"
                          ]
                        },
                        {
                          "type": "null"
                        }
                      ],
                      "description": "Never includes the token — that is a credential, and this is a plain read."
                    }
                  },
                  "required": [
                    "account"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Change the account email",
        "description": "**Two steps if the current address is verified, one if it is not**, and that asymmetry is the design. Moving an account's email is how a stolen session becomes a stolen account, so a verified address has to approve before the new one is even offered a link. An address that was never verified has no established owner to ask — demanding proof from it would only block the person it is meant to protect, which is exactly the person who mistyped it or let an agent invent it.\n\nThis is the account email: who we bill and notify. It is **not** what carriers see — that is `brand.contact_email` and `agent.contact_email`, and Google emails the latter a mandatory authorisation request during launch review.",
        "operationId": "patch_v1_me",
        "tags": [
          "Accounts"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string",
                    "description": "The new address."
                  }
                },
                "required": [
                  "email"
                ]
              },
              "example": {
                "email": "you@yourcompany.com"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "A link is on its way. Which link depends on whether the current address is verified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "stage": {
                      "type": "string",
                      "enum": [
                        "awaiting_approval",
                        "awaiting_confirmation"
                      ]
                    },
                    "pendingEmail": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "note": {
                      "type": "string"
                    },
                    "emailConfigured": {
                      "type": "boolean"
                    },
                    "delivered": {
                      "type": "boolean"
                    },
                    "approveLink": {
                      "type": "string",
                      "description": "Only when no mailer is configured, so a deployment with email never returns a credential."
                    },
                    "confirmLink": {
                      "type": "string",
                      "description": "Only when no mailer is configured."
                    }
                  },
                  "required": [
                    "ok",
                    "stage",
                    "pendingEmail"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Malformed address, or a field this route does not edit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNKNOWN_FIELDS"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "409": {
            "description": "Already this account's address, or another account holds it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "EMAIL_TAKEN"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/approve-email-change": {
      "post": {
        "summary": "Approve a change from the current address",
        "description": "Consumes the link sent to the address on file and issues the confirmation link to the new one. Nothing has moved yet at this point.",
        "operationId": "post_v1_accounts_approve-email-change",
        "tags": [
          "Accounts"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "From the emailed link."
                  }
                },
                "required": [
                  "token"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Approved; the new address now has a link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "stage": {
                      "type": "string",
                      "enum": [
                        "awaiting_confirmation"
                      ]
                    },
                    "pendingEmail": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "emailConfigured": {
                      "type": "boolean"
                    },
                    "delivered": {
                      "type": "boolean"
                    },
                    "confirmLink": {
                      "type": "string",
                      "description": "Only when no mailer is configured."
                    }
                  },
                  "required": [
                    "ok",
                    "stage"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid, used or expired link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_TOKEN"
                }
              }
            }
          },
          "409": {
            "description": "The address was claimed by another account while this was pending.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "EMAIL_TAKEN"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/change-email": {
      "post": {
        "summary": "Confirm the new address",
        "description": "The step that actually moves the account. The new address becomes the account email and is marked verified — it just proved itself by receiving this. The old address is notified after the fact; it cannot undo the change, but silence is how someone finds out weeks later.",
        "operationId": "post_v1_accounts_change-email",
        "tags": [
          "Accounts"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "From the emailed link."
                  }
                },
                "required": [
                  "token"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Moved.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "email": {
                      "type": "string"
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok",
                    "email"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid, used or expired link.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_TOKEN"
                }
              }
            }
          },
          "409": {
            "description": "The address was claimed by another account while this was pending.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "EMAIL_TAKEN"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/send-verification": {
      "post": {
        "summary": "Resend the verification email",
        "description": "Signup already sends one; this is the resend.",
        "operationId": "post_v1_accounts_send-verification",
        "tags": [
          "Accounts"
        ],
        "responses": {
          "200": {
            "description": "Attempted. `delivered` is the honest signal — a configured-but-failing mail provider reports false.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "alreadyVerified": {
                      "type": "boolean"
                    },
                    "emailConfigured": {
                      "type": "boolean"
                    },
                    "delivered": {
                      "type": "boolean"
                    },
                    "link": {
                      "type": "string",
                      "description": "Only when no mailer is configured."
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/verify": {
      "post": {
        "summary": "Verify an email address",
        "description": "Token-only, and deliberately works while signed in as a different account — so the response names the address that was verified.",
        "operationId": "post_v1_accounts_verify",
        "tags": [
          "Accounts"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string",
                    "description": "From the emailed link."
                  }
                },
                "required": [
                  "token"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Verified.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "email": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing, invalid or expired token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_TOKEN",
                  "message": "That link is invalid or expired."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/request-reset": {
      "post": {
        "summary": "Request a password reset",
        "description": "Always answers the same way whether or not the account exists — it must not be an account-existence oracle.",
        "operationId": "post_v1_accounts_request-reset",
        "tags": [
          "Accounts"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "email": {
                    "type": "string"
                  }
                },
                "required": [
                  "email"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Generic acknowledgement, sent either way.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    },
                    "emailConfigured": {
                      "type": "boolean"
                    },
                    "link": {
                      "type": "string",
                      "description": "Only when no mailer is configured."
                    }
                  },
                  "required": [
                    "ok",
                    "message"
                  ]
                }
              }
            }
          },
          "429": {
            "description": "Rate limited, so reset links can't be used to spam an inbox.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/accounts/reset": {
      "post": {
        "summary": "Set a new password",
        "description": "Consumes the token and drops every existing session for that account.",
        "operationId": "post_v1_accounts_reset",
        "tags": [
          "Accounts"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "token": {
                    "type": "string"
                  },
                  "password": {
                    "type": "string"
                  }
                },
                "required": [
                  "token",
                  "password"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Password updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Weak password, or an invalid, used or expired token.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys": {
      "get": {
        "summary": "List API keys",
        "description": "Prefixes and metadata only. Key material is shown once, at creation.",
        "operationId": "get_v1_keys",
        "tags": [
          "API keys"
        ],
        "responses": {
          "200": {
            "description": "Keys.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "keys": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ApiKey"
                      }
                    }
                  },
                  "required": [
                    "keys"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Issue an API key",
        "description": "A **live** key must say what it is for: `brandId` scopes it to one brand, `scope: \"account\"` makes it account-wide. There is no default, because the previous behaviour silently bound the key to whichever brand happened to have an agent first.\n\nA live key needs a brand with an RBM agent behind it — not a carrier-verified one. An unlaunched agent sends real RCS to its accepted test devices, which is exactly what you need before launch.",
        "operationId": "post_v1_keys",
        "tags": [
          "API keys"
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "kind": {
                    "type": "string",
                    "enum": [
                      "sandbox",
                      "live"
                    ],
                    "default": "sandbox"
                  },
                  "brandId": {
                    "type": "string",
                    "description": "Scopes the key to one brand. Required for a live key unless `scope` is `account`."
                  },
                  "scope": {
                    "type": "string",
                    "enum": [
                      "account"
                    ],
                    "description": "Account-wide. A leak reaches every brand you own, so prefer `brandId`."
                  },
                  "label": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "kind": "live",
                "brandId": "brd_…",
                "label": "Basal production"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The key, shown once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "api_key": {
                      "type": "string"
                    },
                    "kind": {
                      "type": "string",
                      "enum": [
                        "sandbox",
                        "live"
                      ]
                    },
                    "key_prefix": {
                      "type": "string"
                    },
                    "brandId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "scope": {
                      "type": "string",
                      "enum": [
                        "brand",
                        "account"
                      ]
                    },
                    "reach": {
                      "type": "string",
                      "enum": [
                        "test_devices_only",
                        "all_recipients"
                      ],
                      "description": "Live keys only. How far this key actually reaches today."
                    },
                    "reach_note": {
                      "type": "string"
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "api_key",
                    "kind",
                    "key_prefix",
                    "scope"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "A live key was requested with no scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "SCOPE_REQUIRED"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "403": {
            "description": "A brand-scoped key tried to issue a key beyond its own scope.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "SCOPE_ESCALATION"
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "BRAND_NOT_FOUND"
                }
              }
            }
          },
          "409": {
            "description": "No brand on this account has an RBM agent yet, so a live key would have nothing to send through.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NO_SENDABLE_BRAND"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/keys/{prefix}/revoke": {
      "post": {
        "summary": "Revoke a key",
        "description": "Immediate. In-flight conversations opened by the key remain readable by the account.",
        "operationId": "post_v1_keys_prefix_revoke",
        "tags": [
          "API keys"
        ],
        "parameters": [
          {
            "name": "prefix",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "The key's visible prefix, from `GET /v1/keys`."
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such key on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brands": {
      "get": {
        "summary": "List brands",
        "description": "Every brand on the account.",
        "operationId": "get_v1_brands",
        "tags": [
          "Brands"
        ],
        "responses": {
          "200": {
            "description": "Brands.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brands": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Brand"
                      }
                    }
                  },
                  "required": [
                    "brands"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Create a brand",
        "description": "Only `display_name` is required to create one, but the readiness check will want the rest before submission. **snake_case.**",
        "operationId": "post_v1_brands",
        "tags": [
          "Brands"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "display_name": {
                    "type": "string",
                    "description": "Consumer-facing name."
                  },
                  "legal_entity": {
                    "type": "string"
                  },
                  "website": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "use_case": {
                    "type": "string"
                  },
                  "logo_url": {
                    "type": "string"
                  },
                  "banner_url": {
                    "type": "string"
                  },
                  "color": {
                    "type": "string"
                  },
                  "contact_email": {
                    "type": "string"
                  },
                  "contact_phone": {
                    "type": "string"
                  },
                  "privacy_url": {
                    "type": "string"
                  },
                  "terms_url": {
                    "type": "string"
                  }
                },
                "required": [
                  "display_name"
                ]
              },
              "example": {
                "display_name": "Basal",
                "website": "https://basal.ai",
                "use_case": "conversational"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The brand.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brand": {
                      "$ref": "#/components/schemas/Brand"
                    }
                  },
                  "required": [
                    "brand"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`display_name` is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brands/{id}": {
      "get": {
        "summary": "Brand detail",
        "description": "Includes the submission gate, so a UI or an agent can always say exactly what is blocking.",
        "operationId": "get_v1_brands_id",
        "tags": [
          "Brands"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "responses": {
          "200": {
            "description": "Brand, last readiness report, gate and agents.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brand": {
                      "$ref": "#/components/schemas/Brand"
                    },
                    "check": {
                      "oneOf": [
                        {
                          "$ref": "#/components/schemas/ReadinessReport"
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "gate": {
                      "$ref": "#/components/schemas/BrandGate"
                    },
                    "agents": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Agent"
                      }
                    }
                  },
                  "required": [
                    "brand",
                    "gate",
                    "agents"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update a brand",
        "description": "**snake_case only.** Unknown fields are rejected with `UNKNOWN_FIELDS` and nothing is changed — camelCase used to be dropped in silence, so `{\"legalEntity\": …}` returned 200 and changed nothing, and the brand looked complete until a carrier said otherwise. A submitted or verified brand is locked.",
        "operationId": "patch_v1_brands_id",
        "tags": [
          "Brands"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "display_name": {
                    "type": "string"
                  },
                  "legal_entity": {
                    "type": "string"
                  },
                  "website": {
                    "type": "string"
                  },
                  "description": {
                    "type": "string"
                  },
                  "use_case": {
                    "type": "string"
                  },
                  "logo_url": {
                    "type": "string"
                  },
                  "banner_url": {
                    "type": "string"
                  },
                  "color": {
                    "type": "string"
                  },
                  "contact_email": {
                    "type": "string"
                  },
                  "contact_phone": {
                    "type": "string"
                  },
                  "privacy_url": {
                    "type": "string"
                  },
                  "terms_url": {
                    "type": "string"
                  }
                }
              },
              "example": {
                "legal_entity": "Basal, Inc.",
                "privacy_url": "https://basal.ai/privacy"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The updated brand.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brand": {
                      "$ref": "#/components/schemas/Brand"
                    }
                  },
                  "required": [
                    "brand"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Unknown or camelCase fields. Nothing was changed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNKNOWN_FIELDS",
                  "message": "Not editable on a brand: legalEntity.",
                  "problems": [
                    "legalEntity: brand routes are snake_case — did you mean legal_entity?"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A submitted or verified brand can't be edited.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "LOCKED"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a draft brand",
        "description": "Only an untouched draft with no agents and no traffic. A brand whose agents reached Google is effectively permanent from our side — Google deprecated agent deletion for RBM and routes it through their support.",
        "operationId": "delete_v1_brands_id",
        "tags": [
          "Brands"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not an empty draft.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "BRAND_NOT_EMPTY"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brands/{id}/check": {
      "post": {
        "summary": "Run the readiness check",
        "description": "Runs against the brand's own details and attaches the report, promoting a passing draft to `ready`. This is the gate: submission is impossible until it passes. Carrier review rejects on a small, predictable set of issues, and one rejection costs weeks.",
        "operationId": "post_v1_brands_id_check",
        "tags": [
          "Brands"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "responses": {
          "200": {
            "description": "Report, gate and the updated brand.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "report": {
                      "$ref": "#/components/schemas/ReadinessReport"
                    },
                    "gate": {
                      "$ref": "#/components/schemas/BrandGate"
                    },
                    "brand": {
                      "$ref": "#/components/schemas/Brand"
                    }
                  },
                  "required": [
                    "report",
                    "gate",
                    "brand"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "The brand has no website to check.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brands/{id}/submit": {
      "post": {
        "summary": "Submit for carrier review",
        "description": "Refused until the readiness check passes.",
        "operationId": "post_v1_brands_id_submit",
        "tags": [
          "Brands"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "responses": {
          "200": {
            "description": "Submitted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brand": {
                      "$ref": "#/components/schemas/Brand"
                    }
                  },
                  "required": [
                    "brand"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Readiness check not passed, or an invalid status transition.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NOT_READY",
                  "message": "This brand would be rejected by carrier review. Fix these first.",
                  "problems": [
                    "No privacy policy found"
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brands/{id}/sync-google": {
      "post": {
        "summary": "Create this brand and its agents at Google",
        "description": "Deliberately **not** gated on the readiness check: an agent that merely exists reaches allowlisted test devices only, involves no carrier, and cannot be rejected by one. The readiness gate stays on `/submit`, where the risk actually is.\n\nWhat this does cost is permanence, so it refuses until confirmed.",
        "operationId": "post_v1_brands_id_sync-google",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "dryRun": {
                    "type": "boolean",
                    "description": "**Rehearse it.** Validates everything, contacts Google only to read, creates nothing, and returns the exact payload each agent would be created with plus the fields that can never change. Needs no `confirm`, and works without Google credentials — what it cannot check from a sandbox it says it cannot check rather than reporting a pass. Do this first: it is the same information as the refusal below without having to trust the refusal."
                  },
                  "confirm": {
                    "type": "string",
                    "enum": [
                      "irreversible"
                    ],
                    "description": "Required for the real call. Without it, and without `dryRun`, the request is refused with 409 and nothing happens."
                  }
                }
              },
              "example": {
                "dryRun": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "With `dryRun: true`, what would happen and nothing else. Otherwise, created at Google.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "type": "object",
                      "properties": {
                        "dryRun": {
                          "type": "boolean",
                          "enum": [
                            true
                          ]
                        },
                        "wouldSucceed": {
                          "type": "boolean",
                          "description": "False when anything below would be rejected."
                        },
                        "brand": {
                          "type": "object",
                          "properties": {
                            "action": {
                              "type": "string",
                              "enum": [
                                "create",
                                "adopt",
                                "reuse",
                                "unknown"
                              ],
                              "description": "`adopt` means a brand of this name already exists at Google and would be reused rather than duplicated."
                            },
                            "rbmBrandId": {
                              "type": [
                                "string",
                                "null"
                              ]
                            },
                            "note": {
                              "type": "string"
                            }
                          }
                        },
                        "agents": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "agentId": {
                                "type": "string"
                              },
                              "displayName": {
                                "type": "string"
                              },
                              "action": {
                                "type": "string",
                                "enum": [
                                  "create",
                                  "skip",
                                  "would_fail"
                                ]
                              },
                              "wouldSend": {
                                "type": "object",
                                "additionalProperties": true,
                                "description": "The exact payload, built by the same code path that would send it."
                              },
                              "problems": {
                                "type": "array",
                                "items": {
                                  "type": "string"
                                }
                              },
                              "permanentFields": {
                                "type": "object",
                                "properties": {
                                  "hostingRegion": {
                                    "type": "string"
                                  },
                                  "agentUseCase": {
                                    "type": "string"
                                  },
                                  "billingCategory": {
                                    "type": "string"
                                  }
                                }
                              }
                            }
                          }
                        },
                        "blockers": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          }
                        },
                        "permanentWarnings": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Permanent findings from the brand's last readiness check."
                        },
                        "permanent": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "What this call makes unchangeable, in plain language."
                        },
                        "limitation": {
                          "type": "string",
                          "description": "Present when Google could not be consulted — from a sandbox key, for instance."
                        },
                        "nextStep": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "dryRun",
                        "wouldSucceed",
                        "agents"
                      ]
                    },
                    {
                      "type": "object",
                      "properties": {
                        "rbmBrandId": {
                          "type": "string"
                        },
                        "agents": {
                          "type": "array",
                          "items": {
                            "type": "object",
                            "properties": {
                              "agentId": {
                                "type": "string"
                              },
                              "rbmAgentId": {
                                "type": "string"
                              }
                            },
                            "required": [
                              "agentId",
                              "rbmAgentId"
                            ]
                          }
                        },
                        "reach": {
                          "type": "string",
                          "description": "What this bought — test devices, or carrier review."
                        },
                        "nextStep": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "rbmBrandId",
                        "agents"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "An agent's spec can't be created at Google.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_AGENT"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not confirmed. Nothing happened. If the brand's last readiness check flagged anything `permanent` and still unresolved, the body repeats it here — this is the last refusal before the artwork reaches Google.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ConfirmationRequired"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "permanentWarnings": {
                          "type": "array",
                          "items": {
                            "type": "string"
                          },
                          "description": "Non-blocking findings that become permanent past this point."
                        },
                        "warningsNote": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "RBM_ERROR"
                }
              }
            }
          },
          "503": {
            "description": "This deployment isn't configured to talk to Google RBM.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "RBM_NOT_CONFIGURED"
                }
              }
            }
          }
        },
        "x-irreversible": "Creates an RCS agent at Google under our partner account. Google deprecated agent deletion for RBM, so it can never be removed — only abandoned. Hosting region and use case are fixed at creation and can never be changed.",
        "x-confirm-required": true
      }
    },
    "/v1/brands/{id}/link-google": {
      "post": {
        "summary": "Adopt a brand that already exists at Google",
        "description": "An agent created in Google's console leaves our brand row with no `rbm_brand_id`, and every Google-side operation needs it. The symptom is severe and silent: verification and carrier launch both fail for a brand that looks completely set up. Back-fills agent links too.",
        "operationId": "post_v1_brands_id_link-google",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "rbmBrandId": {
                    "type": "string",
                    "description": "Match explicitly. Omit to match on display name."
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Linked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "rbmBrandId": {
                      "type": "string"
                    },
                    "agentsLinked": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "googleAgents": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "rbmAgentId": {
                            "type": "string"
                          },
                          "displayName": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "rbmBrandId"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand here, or no matching brand at Google. The error lists what is available.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NO_MATCHING_GOOGLE_BRAND",
                  "remediation": "Pass `rbmBrandId` explicitly from the list above, or run sync-google to create one."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brands/{id}/agents": {
      "post": {
        "summary": "Create an agent",
        "description": "Local only — nothing reaches Google until `sync-google`. Hosting region and use case are stored explicitly here because Google fixes them **forever** at creation, and discovering at the last moment that we have to invent a value for a field that can never be corrected is how those get set wrong.\n\n`CONVERSATIONAL` is the only billing category eligible for 24-hour session billing, which the whole RCS price depends on.",
        "operationId": "post_v1_brands_id_agents",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "displayName": {
                    "type": "string",
                    "description": "Consumer-facing agent name."
                  },
                  "hostingRegion": {
                    "type": "string",
                    "enum": [
                      "NORTH_AMERICA",
                      "EUROPE",
                      "ASIA_PACIFIC"
                    ],
                    "default": "NORTH_AMERICA",
                    "description": "**Permanent** once synced to Google."
                  },
                  "billingCategory": {
                    "type": "string",
                    "enum": [
                      "CONVERSATIONAL",
                      "NON_CONVERSATIONAL"
                    ],
                    "default": "CONVERSATIONAL",
                    "description": "Fixed at launch."
                  },
                  "useCase": {
                    "type": "string",
                    "description": "Defaults to the brand's. **Permanent** once synced."
                  }
                },
                "required": [
                  "displayName"
                ]
              },
              "example": {
                "displayName": "Basal",
                "useCase": "conversational"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The agent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agent": {
                      "$ref": "#/components/schemas/Agent"
                    }
                  },
                  "required": [
                    "agent"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid spec.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_AGENT"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brands/{id}/optins/{e164}": {
      "get": {
        "summary": "Can this brand message this number?",
        "description": "Without this the only way to learn someone opted out is to attempt a send and read the 409 — which means finding out at the moment you were going to message them.",
        "operationId": "get_v1_brands_id_optins_e164",
        "tags": [
          "Consent"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          },
          {
            "name": "e164",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Recipient in E.164, URL-encoded (`%2B12025550123`)."
          }
        ],
        "responses": {
          "200": {
            "description": "Consent status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "e164": {
                      "type": "string"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "opted_in",
                        "opted_out",
                        "no_record"
                      ]
                    },
                    "sendable": {
                      "type": "boolean",
                      "description": "Whether a send would be accepted right now."
                    },
                    "method": {
                      "type": "string",
                      "description": "How consent was captured."
                    },
                    "evidenceRef": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "capturedAt": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "optedOutAt": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "reconsents": {
                      "type": "number",
                      "description": "How many times this opt-out has already been reversed."
                    },
                    "remediation": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "e164",
                    "status",
                    "sendable"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`e164` must be E.164.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brands/{id}/optins/reconsent": {
      "post": {
        "summary": "Record that someone who opted out consented again",
        "description": "**The one exception to \"only the recipient reverses their own STOP\".** It exists because refusing to build it does not prevent the reversal, it just moves it somewhere with no record — and the version with no record was a bug we shipped.\n\nNarrow on purpose:\n\n- `evidenceRef` is **required**, unlike an ordinary opt-in. An unevidenced claim about someone who has already said no is exactly the claim that must not be taken on trust.\n- `capturedAt` must be **after** the opt-out. Consent collected before it is the consent they revoked, and replaying the original signup record is the most likely misuse.\n- A number may be re-consented at most twice per brand. A third is refused outright: at that point consent and persistence are not distinguishable, and it goes to a human.\n- Every reversal is written to an audit trail with the account, the evidence and the opt-out it reverses, readable at `GET /v1/brands/{id}/optins/reconsents`.\n\nTexting `START` always works and is never rate-limited — it is the recipient's own channel.",
        "operationId": "post_v1_brands_id_optins_reconsent",
        "tags": [
          "Consent"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "to": {
                    "type": "string",
                    "description": "Recipient in E.164."
                  },
                  "method": {
                    "type": "string",
                    "description": "How consent was collected this time, e.g. `web_form`, `account_signup`."
                  },
                  "evidenceRef": {
                    "type": "string",
                    "description": "The URL, record id or ticket showing they consented again. Required — this is the record you would be asked to produce."
                  },
                  "capturedAt": {
                    "type": "string",
                    "description": "ISO 8601. Must be after the opt-out."
                  }
                },
                "required": [
                  "to",
                  "method",
                  "evidenceRef",
                  "capturedAt"
                ]
              },
              "example": {
                "to": "+12025550123",
                "method": "account_signup",
                "evidenceRef": "https://basal.ai/admin/consent/8812",
                "capturedAt": "2026-06-14T09:31:00Z"
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Recorded. The number is sendable again.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "reconsentId": {
                      "type": "string"
                    },
                    "e164": {
                      "type": "string"
                    },
                    "sendable": {
                      "type": "boolean"
                    },
                    "previousOptOutAt": {
                      "type": "string",
                      "description": "The opt-out this reversed."
                    },
                    "reconsents": {
                      "type": "number"
                    },
                    "remaining": {
                      "type": "number",
                      "description": "Reversals left before only START will work."
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok",
                    "reconsentId",
                    "sendable"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Missing or placeholder `evidenceRef`, or consent that predates the opt-out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "CONSENT_PREDATES_OPTOUT",
                  "message": "The consent you are citing was captured before this number opted out.",
                  "details": {
                    "capturedAt": "2026-03-01T00:00:00Z",
                    "optedOutAt": "2026-05-02T00:00:00Z"
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Nothing to reverse (`NOT_OPTED_OUT`, `NO_OPTIN_RECORD`), too many cycles for this number (`TOO_MANY_RECONSENTS`), or the account's daily ceiling (`ACCOUNT_DAILY_LIMIT`).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "TOO_MANY_RECONSENTS",
                  "message": "This number has already been re-consented 2 times for this brand and has opted out again since.",
                  "remediation": "Ask them to text START, which we always honour, or contact support@cadencercs.com."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brands/{id}/optins/reconsents": {
      "get": {
        "summary": "This brand's re-consent audit trail",
        "description": "Every time this brand reversed an opt-out, with the evidence cited at the time. This is the record a carrier or a complainant asks for.",
        "operationId": "get_v1_brands_id_optins_reconsents",
        "tags": [
          "Consent"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 100
            },
            "description": "1–500, default 100."
          }
        ],
        "responses": {
          "200": {
            "description": "Audit trail, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brandId": {
                      "type": "string"
                    },
                    "reconsents": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "e164": {
                            "type": "string"
                          },
                          "method": {
                            "type": "string"
                          },
                          "evidenceRef": {
                            "type": "string"
                          },
                          "capturedAt": {
                            "type": "string"
                          },
                          "reversedOptOutAt": {
                            "type": "string"
                          },
                          "recordedAt": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id",
                          "e164",
                          "evidenceRef"
                        ]
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "brandId",
                    "reconsents"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}": {
      "get": {
        "summary": "Agent detail",
        "description": "The agent, its brand, its per-carrier launch state and its lifecycle history.",
        "operationId": "get_v1_agents_id",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Agent detail.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agent": {
                      "$ref": "#/components/schemas/Agent"
                    },
                    "brand": {
                      "$ref": "#/components/schemas/Brand"
                    },
                    "carriers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "carrier": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          },
                          "requested_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "launched_at": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        }
                      }
                    },
                    "history": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "from_status": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "to_status": {
                            "type": "string"
                          },
                          "reason": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "created_at": {
                            "type": "string"
                          }
                        }
                      }
                    }
                  },
                  "required": [
                    "agent",
                    "brand"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete an agent that never reached Google",
        "description": "Narrow on purpose. Once the agent exists at Google it **cannot be deleted by anyone** — Google deprecated agent deletion for RBM and routes it through their support — so removing our record would orphan theirs invisibly, which is worse than the growing list this exists to trim. Drafts with no traffic only.",
        "operationId": "delete_v1_agents_id",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "deleted": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "It exists at Google (`AGENT_EXISTS_AT_GOOGLE`), isn't a draft (`AGENT_NOT_DRAFT`), or has conversations against it (`AGENT_HAS_TRAFFIC`) — those transcripts are someone's messages and the consent record points at this agent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "AGENT_EXISTS_AT_GOOGLE",
                  "remediation": "Contact RBM support if it genuinely has to go."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/launch-info": {
      "patch": {
        "summary": "Record launch-questionnaire answers",
        "description": "Held here because Google's launch questionnaire **cannot be edited after submission** — this is what lets you check the answers while they are still changeable. Accepts snake_case or camelCase for each field.",
        "operationId": "patch_v1_agents_id_launch-info",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "launch_video_url": {
                    "type": "string",
                    "description": "A recording of the full flow, including a STOP exchange."
                  },
                  "reviewer_instructions": {
                    "type": "string",
                    "description": "How a reviewer reaches the agent."
                  },
                  "trigger_description": {
                    "type": "string",
                    "description": "What causes a message to be sent."
                  },
                  "interaction_types": {
                    "type": "string",
                    "description": "What the conversation does."
                  },
                  "contact_name": {
                    "type": "string",
                    "description": "A named individual at the brand."
                  },
                  "contact_email": {
                    "type": "string",
                    "description": "That person's individual mailbox — **not** a shared alias. Google emails it a mandatory authorisation request, and a role mailbox is a rejection."
                  },
                  "contact_phone": {
                    "type": "string"
                  },
                  "contact_title": {
                    "type": "string",
                    "description": "Google's schema marks this optional and their launch endpoint refuses without it."
                  },
                  "optin_description": {
                    "type": "string",
                    "description": "How consent is captured."
                  },
                  "second_use_case": {
                    "type": "string"
                  },
                  "rbm_service_id": {
                    "type": "string",
                    "description": "The agent's `bot@…` address, from the Console overview page. Cannot be read back from Google's API; every deep link is built from it."
                  },
                  "deeplink_phone": {
                    "type": "string",
                    "description": "Default phone number for generated deep links."
                  }
                }
              },
              "example": {
                "contact_name": "Dakota Wixom",
                "contact_title": "Founder",
                "contact_email": "dakota@basal.ai",
                "trigger_description": "Daily weight check-in the user opted into at signup."
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "updated": {
                      "type": "number",
                      "description": "How many fields changed."
                    },
                    "agent": {
                      "$ref": "#/components/schemas/Agent"
                    }
                  },
                  "required": [
                    "ok",
                    "updated"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "No recognised fields in the body.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NOTHING_TO_UPDATE"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/launch-readiness": {
      "get": {
        "summary": "Check launch readiness",
        "description": "The more valuable of the two readiness checks. The brand check protects a submission that can be redone; this protects the launch questionnaire, which cannot be edited after submission. It also hands back the opt-out message verbatim, because Google asks for it exactly.\n\nReads the live profile from Google where it can; a Google outage degrades the check rather than failing it.",
        "operationId": "get_v1_agents_id_launch-readiness",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          },
          {
            "name": "carriers",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Comma-separated carrier names to check management type for, e.g. `T-Mobile,AT&T`."
          }
        ],
        "responses": {
          "200": {
            "description": "Readiness report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/LaunchReadiness"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/google": {
      "get": {
        "summary": "The agent's profile at Google",
        "description": "What the agent actually looks like to Google, and therefore to a consumer. `missing` is the point: it lists the fields launch review expects and you haven't set. An agent created in the console shows a bare info screen, and the only symptom is a message that arrives looking unbranded.",
        "operationId": "get_v1_agents_id_google",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Live profile.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agentId": {
                      "type": "string"
                    },
                    "rbmAgentId": {
                      "type": "string"
                    },
                    "profile": {
                      "type": "object",
                      "additionalProperties": true,
                      "description": "Google's `rcsBusinessMessagingAgent` verbatim."
                    },
                    "missing": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Fields empty at Google that launch review expects."
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "agentId",
                    "profile",
                    "missing"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This agent doesn't exist at Google yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NOT_SYNCED",
                  "remediation": "POST /v1/brands/{id}/sync-google first."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update the agent's profile at Google",
        "description": "Editable fields only — hosting region and use case are permanent and billing category is fixed at launch, so they are rejected here rather than sent and refused confusingly. Omitted fields fall back to the brand's own details, which is usually what you want.\n\nImages are fetched and **inspected byte-by-byte**, not just size-checked: a palette PNG is accepted by Google and then re-quantised, which is how one hero image was permanently destroyed. That returns a warning, not a rejection — but it becomes permanent the moment verification is requested.",
        "operationId": "patch_v1_agents_id_google",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "description": {
                    "type": "string"
                  },
                  "logoUri": {
                    "type": "string",
                    "description": "Square. Must be directly fetchable — no redirects."
                  },
                  "heroUri": {
                    "type": "string",
                    "description": "Banner. Same fetch rules."
                  },
                  "color": {
                    "type": "string",
                    "description": "Hex; needs 4.5:1 contrast against white. Check before you send — a colour that looks fine is often 4.3:1."
                  },
                  "privacyUrl": {
                    "type": "string"
                  },
                  "termsUrl": {
                    "type": "string"
                  },
                  "contactEmail": {
                    "type": "string"
                  },
                  "contactPhone": {
                    "type": "string"
                  },
                  "website": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "profile": {
                      "type": "object",
                      "additionalProperties": true
                    },
                    "warnings": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Accepted by Google, but worth fixing before verification freezes them."
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Invalid profile, or nothing to update.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_PROFILE",
                  "problems": [
                    "heroUri … is a palette PNG; Google will re-quantise it."
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not synced to Google yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NOT_SYNCED"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/brands/{id}/sms-number": {
      "get": {
        "summary": "The number this brand sends SMS from",
        "description": "An SMS carries no agent, so the number a reply comes **to** is the only thing that says which brand the person answered. A brand with its own number gets its replies routed to it; a brand on the shared number can only be attributed by the sender, which breaks the moment two of your brands text the same person.",
        "operationId": "get_v1_brands_id_sms-number",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "responses": {
          "200": {
            "description": "The brand's sending number, or null when it uses the shared one.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brandId": {
                      "type": "string"
                    },
                    "e164": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "shared": {
                      "type": "boolean",
                      "description": "True when the brand sends from the shared number."
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "brandId",
                    "shared"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Give this brand its own SMS sending number",
        "description": "Buys a local SMS-capable number, adds it to the registered messaging campaign, and stores it on the brand. From then on this brand's SMS (fallback included) goes out from that number and replies to it are routed to this brand. Idempotent: a brand that already has one keeps it and nothing is bought. Live keys only — sandbox brands never reach a carrier. Roughly $1.15/month per number, billed as part of your plan.",
        "operationId": "post_v1_brands_id_sms-number",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Brand id."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "areaCode": {
                    "type": "string",
                    "description": "Three digits, e.g. `206`. Optional; omit for any US number."
                  }
                }
              },
              "example": {
                "areaCode": "206"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "The brand's number.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "brandId": {
                      "type": "string"
                    },
                    "e164": {
                      "type": "string"
                    },
                    "provisioned": {
                      "type": "boolean",
                      "description": "True when a number was bought on this call."
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "brandId",
                    "e164",
                    "provisioned"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`areaCode` is not three digits.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_AREA_CODE"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "403": {
            "description": "Sandbox key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "SANDBOX_KEY"
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "No number available, in that area code or at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NO_NUMBERS_AVAILABLE"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "The SMS provider refused.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "SMS_PROVIDER_ERROR"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/testers": {
      "get": {
        "summary": "Read a test-device invitation status",
        "description": "Read Google's invitation status without sending or resending an invitation. Acceptance and RCS reachability are separate checks.",
        "operationId": "get_v1_agents_id_testers",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          },
          {
            "name": "e164",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            }
          }
        ],
        "responses": {
          "200": {
            "description": "Invitation status.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agentId": {
                      "type": "string"
                    },
                    "inviteStatus": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "400": {
            "description": "Invalid number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "Agent or tester not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Agent not mapped.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Status unavailable.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Invite a test device",
        "description": "Before carrier launch an agent reaches allowlisted testers only, so every brand needs this to see its own messages. **The device must accept the invitation** before anything can reach it — sends fail silently until they tap accept.\n\nGoogle's limits are 20/day and 200 lifetime per agent, and the lifetime cap cannot be undone.",
        "operationId": "post_v1_agents_id_testers",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "e164": {
                    "type": "string",
                    "description": "The device number in E.164, e.g. `+12025550123`."
                  },
                  "phoneNumber": {
                    "type": "string",
                    "description": "Accepted as an alias for `e164`."
                  }
                }
              },
              "example": {
                "e164": "+12025550123"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Invitation sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "agentId": {
                      "type": "string"
                    },
                    "rbmAgentId": {
                      "type": "string"
                    },
                    "inviteStatus": {
                      "type": "string"
                    },
                    "nextStep": {
                      "type": "string",
                      "description": "The device must accept before this agent can reach it."
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Not a valid E.164 number.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_E164"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This agent doesn't exist at Google yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "AGENT_NOT_MAPPED"
                }
              }
            }
          },
          "422": {
            "description": "Google's RCS platform has no registration it can address for this number: RCS is off on the handset, or its carrier runs RCS on a platform Google's business messaging cannot reach. Nothing is consumed against the invite cap.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NUMBER_NOT_ADDRESSABLE"
                }
              }
            }
          },
          "429": {
            "description": "Google's tester invite limit reached (20/day, 200 lifetime per agent).",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "TESTER_LIMIT_REACHED"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/reachability": {
      "get": {
        "summary": "Can this agent reach this number over RCS?",
        "description": "Folds in every reason a send might not land: the device has no RCS, the agent isn't launched on that carrier, or the number simply hasn't used RCS lately. **Point-in-time** — Google only answers for numbers seen on RCS in roughly the last 31 days, so never cache it as a permanent fact.\n\n`false` is not a dead end: the send falls back to SMS. The value is knowing before you send.\n\n**Not available on a sandbox key.** It asks Google about a real agent, and a sandbox account has none — it answers `409 AGENT_NOT_MAPPED` until `sync-google` has run, which is irreversible. Sending needs none of this: `POST /v1/conversations` resolves a channel and simulates delivery in sandbox, so treat this as a go-live check rather than a build-time one.",
        "operationId": "get_v1_agents_id_reachability",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          },
          {
            "name": "to",
            "in": "query",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "E.164 number to check."
          }
        ],
        "responses": {
          "200": {
            "description": "Point-in-time answer.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "to": {
                      "type": "string"
                    },
                    "reachable": {
                      "type": "boolean"
                    },
                    "channel": {
                      "type": "string",
                      "enum": [
                        "rcs",
                        "sms"
                      ],
                      "description": "What a send right now would use."
                    },
                    "launchedCarriers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "note": {
                      "type": "string"
                    },
                    "remediation": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "to",
                    "reachable",
                    "channel"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`to` must be E.164.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This agent doesn't exist at Google yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "AGENT_NOT_MAPPED"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "This deployment's backend can't check reachability.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/archive": {
      "post": {
        "summary": "Archive an agent",
        "description": "The honest answer to \"how do I get rid of this\". **Google deprecated agent deletion for RBM**, so an agent created by mistake — a typo, a test, a brand that changed its mind — is permanent. Archiving is what removal actually looks like here: it disappears from listings on both sides, and unlike deletion it can be undone.\n\nGoogle is updated first and our record only follows if that succeeded, because a local archive sitting over a live agent at Google looks tidy here and keeps receiving traffic there.\n\nConversations, transcripts and consent records are untouched. Archiving is not deletion, and there is no deletion.",
        "operationId": "post_v1_agents_id_archive",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Archived.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "agentId": {
                      "type": "string"
                    },
                    "archived": {
                      "type": "boolean"
                    },
                    "atGoogle": {
                      "type": "string",
                      "enum": [
                        "done",
                        "skipped",
                        "failed"
                      ]
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok",
                    "archived"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "409": {
            "description": "Conversations are still open on this agent — archiving mid-conversation leaves people who replied to a live message with nothing answering them.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google refused. Nothing changed here either, so the two cannot disagree silently.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/unarchive": {
      "post": {
        "summary": "Restore an archived agent",
        "description": "Reverses an archive. Being reversible is the point — nobody archives confidently if they cannot undo it.",
        "operationId": "post_v1_agents_id_unarchive",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Restored.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "agentId": {
                      "type": "string"
                    },
                    "archived": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok",
                    "archived"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google refused; nothing changed here either.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/analytics": {
      "get": {
        "summary": "Delivery health",
        "description": "Google **throttles on reputation**, so without this the first sign of a problem is a customer saying their messages stopped arriving. `LOW` is the documented default for a new agent, so the signal is the direction of travel and the unsubscribe reasons, not the letter itself — \"too many messages\" and \"spam\" are different problems with different fixes.\n\nGoogle returns no row at all for an agent-country pair without enough data, which is the normal state early on.",
        "operationId": "get_v1_agents_id_analytics",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Per-country metrics, or nulls with a note.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agentId": {
                      "type": "string"
                    },
                    "countries": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      }
                    },
                    "summary": {
                      "oneOf": [
                        {
                          "type": "object",
                          "properties": {
                            "reputation": {
                              "type": "string",
                              "enum": [
                                "HIGH",
                                "MEDIUM",
                                "LOW",
                                "REPUTATION_UNSPECIFIED"
                              ]
                            },
                            "trafficLimit": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "spamTrend28d": {
                              "type": [
                                "number",
                                "null"
                              ]
                            },
                            "asOf": {
                              "type": [
                                "string",
                                "null"
                              ]
                            }
                          }
                        },
                        {
                          "type": "null"
                        }
                      ]
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "agentId",
                    "countries"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/deep-link": {
      "get": {
        "summary": "One-tap opt-in link",
        "description": "\"Text SURVEY to +1…\" asks someone to open an app, type an exact word and address it correctly. This does all three in a tap — the missing half of a keyword opt-in flow.\n\nBuilt from the agent's `rbm_service_id`, which Google's API cannot return: copy it from the Console overview page into `PATCH /v1/agents/{id}/launch-info` first.\n\n**Not available on a sandbox key**, for the same reason — the service ID only exists after the irreversible `sync-google`. Since a deep link is the only way a *user* can start a conversation, that one journey cannot be rehearsed in sandbox. Agent-initiated conversations, which is everything this API opens, are fully testable.",
        "operationId": "get_v1_agents_id_deep-link",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          },
          {
            "name": "phone",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Override the agent's default deep-link phone number."
          },
          {
            "name": "body",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Pre-filled message body, e.g. the opt-in keyword."
          }
        ],
        "responses": {
          "200": {
            "description": "The link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "launched": {
                      "type": "boolean"
                    },
                    "caveat": {
                      "type": "string",
                      "description": "Present when the agent isn't launched — the link only works for allowlisted test devices."
                    }
                  },
                  "required": [
                    "url",
                    "launched"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Couldn't build the link from the values given.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The agent's service id isn't recorded and can't be read from Google.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "SERVICE_ID_UNKNOWN"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/request-verification": {
      "post": {
        "summary": "Ask Google to verify the brand behind this agent",
        "description": "Refuses if the profile is still incomplete at Google, because this is the last moment any of it can be changed. Google also requires all five verification-contact fields, and the brand contact must be a **named individual** — they email it a mandatory authorisation request and a shared alias is a rejection.",
        "operationId": "post_v1_agents_id_request-verification",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "confirm": {
                    "type": "string",
                    "enum": [
                      "irreversible"
                    ]
                  }
                }
              },
              "example": {
                "confirm": "irreversible"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Submitted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "submitted"
                      ]
                    },
                    "frozen": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The profile fields now permanently locked."
                    },
                    "note": {
                      "type": "string"
                    },
                    "nextStep": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok",
                    "status",
                    "frozen"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not confirmed (`CONFIRMATION_REQUIRED`), brand not ready (`NOT_READY`), verification contact incomplete (`VERIFICATION_CONTACT_INCOMPLETE`), profile still empty at Google (`PROFILE_NOT_FINAL`), or the agent isn't at Google yet (`AGENT_NOT_MAPPED`). Nothing happened.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ConfirmationRequired"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-irreversible": "Permanently freezes the agent's ENTIRE public profile — description, logo, hero image, colour, privacy and terms URLs, emails, phone numbers and websites. The lock fires on the request, not on completion, and changing any of it afterwards requires an RBM support ticket.",
        "x-confirm-required": true
      }
    },
    "/v1/agents/{id}/verification": {
      "get": {
        "summary": "Poll verification state",
        "description": "Records what Google says, but only persists a status the lifecycle actually allows — reading a status must never fail because writing it would be an illegal transition.",
        "operationId": "get_v1_agents_id_verification",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Current state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "status": {
                      "type": "string",
                      "enum": [
                        "unverified",
                        "submitted",
                        "verified",
                        "rejected"
                      ]
                    },
                    "persisted": {
                      "type": "boolean",
                      "description": "Whether this reading changed our stored status."
                    },
                    "agentStatus": {
                      "type": "string",
                      "description": "Our status before this reading."
                    },
                    "googleState": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Google's raw enum."
                    },
                    "note": {
                      "type": "string"
                    },
                    "remediation": {
                      "type": "string"
                    },
                    "raw": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true
                    }
                  },
                  "required": [
                    "status"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This agent doesn't exist at Google yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "AGENT_NOT_MAPPED"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/request-launch": {
      "post": {
        "summary": "Request carrier launch",
        "description": "**This is what makes an agent reach non-test devices.** Reachability is carrier-scoped, so an agent launched on two carriers is invisible on a third.\n\n`carriers` replaces the whole launch set at Google rather than adding to it — previously requested carriers are re-sent automatically, because omitting them would silently drop those launches while the request still succeeded.\n\nVerification must have been *submitted* first, but it only *completes* during launch approval: requiring `verified` here would deadlock, since verification never starts until a launch exists.",
        "operationId": "post_v1_agents_id_request-launch",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "carriers": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    },
                    "description": "Carrier names, e.g. `[\"T-Mobile\",\"AT&T\"]`. See `GET /v1/carriers`."
                  },
                  "confirm": {
                    "type": "string",
                    "enum": [
                      "irreversible"
                    ]
                  }
                },
                "required": [
                  "carriers"
                ]
              },
              "example": {
                "carriers": [
                  "T-Mobile",
                  "AT&T"
                ],
                "confirm": "irreversible"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Accepted by Google.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "status": {
                      "type": "string",
                      "enum": [
                        "launching"
                      ]
                    },
                    "carriers": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "The complete launch set now on file."
                    },
                    "carriedForward": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      },
                      "description": "Previously requested carriers re-sent to avoid dropping them."
                    },
                    "note": {
                      "type": "string"
                    },
                    "nextStep": {
                      "type": "string"
                    },
                    "warning": {
                      "type": "string",
                      "description": "Google accepted the launch but our own records could not be updated. **Do not retry** — poll `GET /v1/agents/{id}/launch` for the authoritative state."
                    },
                    "bookkeepingErrors": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    }
                  },
                  "required": [
                    "ok",
                    "status",
                    "carriers"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`carriers` is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Not confirmed (`CONFIRMATION_REQUIRED`), verification never submitted (`VERIFICATION_NOT_SUBMITTED`), or the questionnaire is incomplete (`QUESTIONNAIRE_INCOMPLETE`). Nothing happened.",
            "content": {
              "application/json": {
                "schema": {
                  "oneOf": [
                    {
                      "$ref": "#/components/schemas/ConfirmationRequired"
                    },
                    {
                      "$ref": "#/components/schemas/Error"
                    }
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        },
        "x-irreversible": "The launch questionnaire cannot be edited after submission without going through RBM support, and supporting documents can no longer be added or removed.",
        "x-confirm-required": true
      }
    },
    "/v1/agents/{id}/launch": {
      "get": {
        "summary": "Poll per-carrier launch state",
        "description": "Reconciles our records with Google's in both directions. `needsAttention` names the rejected and suspended carriers explicitly — they used to read as `launching`, so nobody would ever have gone looking.",
        "operationId": "get_v1_agents_id_launch",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Per-carrier state.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "agentStatus": {
                      "type": "string"
                    },
                    "carriers": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "carrier": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string",
                            "enum": [
                              "pending",
                              "launching",
                              "launched",
                              "rejected",
                              "suspended"
                            ]
                          },
                          "googleState": {
                            "type": [
                              "string",
                              "null"
                            ]
                          }
                        },
                        "required": [
                          "carrier",
                          "status"
                        ]
                      }
                    },
                    "reach": {
                      "type": "string",
                      "description": "Plain-language summary of who can actually be reached."
                    },
                    "needsAttention": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "carrier": {
                            "type": "string"
                          },
                          "status": {
                            "type": "string"
                          }
                        }
                      },
                      "description": "Rejected or suspended carriers — these need a human."
                    }
                  },
                  "required": [
                    "agentStatus",
                    "carriers"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This agent doesn't exist at Google yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "AGENT_NOT_MAPPED"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/carriers": {
      "get": {
        "summary": "Carriers and how each approves",
        "description": "The part that matters is `managementType`. `GOOGLE_MANAGED` carriers clear through Google in days; `CARRIER_MANAGED` ones run their own approval and require a commercial agreement with the carrier before they will deliver. That decides whether a launch is a form to fill in or a contract to negotiate.",
        "operationId": "get_v1_carriers",
        "tags": [
          "Google & carriers"
        ],
        "parameters": [
          {
            "name": "country",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by country code or name substring."
          }
        ],
        "responses": {
          "200": {
            "description": "Carriers.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "total": {
                      "type": "number"
                    },
                    "googleManaged": {
                      "type": "number"
                    },
                    "carrierManaged": {
                      "type": "number"
                    },
                    "carriers": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Carrier"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "total",
                    "carriers"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/partner": {
      "get": {
        "summary": "Our partner record at Google",
        "description": "Where the partner half of the verification contact comes from. Read rather than held in env, so changing the partner email in the Console changes what verification requests send.\n\n`partnerWebhookKnown: false` means `partners.get` returned no capabilities block — **not** that no webhook is registered.\n\n`partnerWebhookKnown: true` with `partnerWebhook: null` is the readable case and it is worth acting on: Google has no partner-level URI to push inbound messages to, so replies to any agent under this partner that lacks its own webhook are dropped. Outbound is unaffected, which is what makes it easy to miss. This is deployment-level configuration, not something an API caller can set — `GET /v1/agents/{id}/launch-readiness` reports the same fact as a blocking `inbound` check, alongside when inbound was last actually received.",
        "operationId": "get_v1_partner",
        "tags": [
          "Google & carriers"
        ],
        "responses": {
          "200": {
            "description": "Partner record.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "name": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "displayName": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "company": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "contactEmails": {
                      "type": "array",
                      "items": {
                        "type": "string"
                      }
                    },
                    "technicalContact": {
                      "type": [
                        "object",
                        "null"
                      ],
                      "additionalProperties": true
                    },
                    "partnerWebhook": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The partner-level inbound webhook, when readable."
                    },
                    "partnerWebhookKnown": {
                      "type": "boolean",
                      "description": "False means Google returned no capabilities block, not that none is set. True with a null webhook means Google genuinely has none, and inbound is dead."
                    },
                    "webhookNote": {
                      "type": "string"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "Missing `RBM_SERVICE_ACCOUNT_JSON` or `RBM_PARTNER_ID` — the error names which.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "RBM_NOT_CONFIGURED",
                  "missing": [
                    "RBM_PARTNER_ID"
                  ]
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/attachments": {
      "get": {
        "summary": "List supporting documents",
        "description": "Google gives us create and delete but **no list**, so our own record is the only one. Five per agent, PDF only, ≤50 MB.",
        "operationId": "get_v1_agents_id_attachments",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Documents on file.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "attachments": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "id": {
                            "type": "string"
                          },
                          "rbm_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "gcs_url": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "display_name": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "size_bytes": {
                            "type": "number"
                          },
                          "content_type": {
                            "type": "string"
                          },
                          "uploaded_at": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "id"
                        ]
                      }
                    },
                    "remaining": {
                      "type": "number",
                      "description": "How many more Google will accept."
                    },
                    "locked": {
                      "type": "boolean",
                      "description": "True once a launch request exists — documents can no longer be added or removed."
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "attachments",
                    "remaining",
                    "locked"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Upload a supporting document",
        "description": "Authorisation letters and similar; Google says they streamline approval. Send the PDF as the raw body with `content-type: application/pdf`, or as JSON `{ contentBase64, displayName }` so an agent can upload without constructing a multipart request.\n\nCan only be done before a launch request exists.",
        "operationId": "post_v1_agents_id_attachments",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          },
          {
            "name": "displayName",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Label for the document, when posting raw bytes."
          }
        ],
        "requestBody": {
          "required": true,
          "description": "Or post the raw PDF with `content-type: application/pdf`.",
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "contentBase64": {
                    "type": "string",
                    "description": "The PDF, base64-encoded."
                  },
                  "displayName": {
                    "type": "string"
                  }
                },
                "required": [
                  "contentBase64"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Uploaded.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "id": {
                      "type": "string"
                    },
                    "rbmName": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "gcsUrl": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "remaining": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "ok",
                    "id"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Empty file, missing `contentBase64`, or not a PDF.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NOT_A_PDF"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Agent isn't at Google yet, a launch request already exists, or the per-agent limit is reached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "413": {
            "description": "Over Google's 50 MB limit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "FILE_TOO_LARGE"
                }
              }
            }
          },
          "415": {
            "description": "Google accepts PDFs only.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNSUPPORTED_MEDIA_TYPE"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the upload. If the agent has never requested verification, the response says so — these documents attach to a verification record.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/attachments/{attachmentId}": {
      "delete": {
        "summary": "Remove a supporting document",
        "description": "Refused once a launch request exists.",
        "operationId": "delete_v1_agents_id_attachments_attachmentId",
        "tags": [
          "Agents"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          },
          {
            "name": "attachmentId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "From `GET /v1/agents/{id}/attachments`."
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent or document.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "A launch request already exists.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "LAUNCH_SUBMITTED"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/agents/{id}/webhook": {
      "get": {
        "summary": "Read this agent's Google inbound routing",
        "description": "**This is Google's inbound routing for one agent — it is not how you receive messages from Cadence.** It tells Google to push this agent's traffic to a URL of your own instead of to the partner-level endpoint that Cadence normally receives on, which means Cadence stops seeing that agent's replies and stops driving its flows.\n\nIf what you want is your application to learn about inbound replies, you want a `flow` (the server answers for you), `GET /v1/events` (SSE), or `GET /v1/conversations/{id}/answers` (polling) — see the guide at https://cadencercs.com/docs.html#receiving.\n\nThe verification token is input-only at Google and can't be read back.",
        "operationId": "get_v1_agents_id_webhook",
        "tags": [
          "Google inbound routing"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Current routing.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhookUri": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "Null when this agent uses the partner-level endpoint."
                    },
                    "integrationId": {
                      "type": [
                        "string",
                        "null"
                      ]
                    },
                    "integrations": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "additionalProperties": true
                      },
                      "description": "Google's own view, best-effort."
                    },
                    "effective": {
                      "type": "string",
                      "description": "Plain-language statement of where this agent's traffic goes."
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "webhookUri",
                    "effective"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "put": {
        "summary": "Point this agent's Google traffic at your own endpoint",
        "description": "**This is Google's inbound routing for one agent — it is not how you receive messages from Cadence.** It tells Google to push this agent's traffic to a URL of your own instead of to the partner-level endpoint that Cadence normally receives on, which means Cadence stops seeing that agent's replies and stops driving its flows.\n\nIf what you want is your application to learn about inbound replies, you want a `flow` (the server answers for you), `GET /v1/events` (SSE), or `GET /v1/conversations/{id}/answers` (polling) — see the guide at https://cadencercs.com/docs.html#receiving.\n\nWe issue the verification token rather than accepting one, so it is always high-entropy. It is **derived, not stored** — re-issuing returns the same value; `?rotate=true` invalidates the old one. Your endpoint must echo the token on Google's verification handshake, and every push is signed with it.",
        "operationId": "put_v1_agents_id_webhook",
        "tags": [
          "Google inbound routing"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          },
          {
            "name": "rotate",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "true",
                "false"
              ]
            },
            "description": "`true` bumps the token version, invalidating the previous token."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "webhookUri": {
                    "type": "string",
                    "description": "A public `https://` URL. Google posts to it directly."
                  }
                },
                "required": [
                  "webhookUri"
                ]
              },
              "example": {
                "webhookUri": "https://example.com/rbm"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Configured. The token is shown once.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "webhookUri": {
                      "type": "string"
                    },
                    "integrationId": {
                      "type": "string"
                    },
                    "verificationToken": {
                      "type": "string",
                      "description": "Shown exactly once, for the same reason an API key is."
                    },
                    "nextStep": {
                      "type": "string"
                    },
                    "ourEndpoint": {
                      "type": "string",
                      "description": "Point Google here instead if you would rather Cadence keep receiving."
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok",
                    "webhookUri",
                    "verificationToken"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`webhookUri` must be a public https:// URL.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This agent doesn't exist at Google yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "AGENT_NOT_MAPPED"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured, or `WEBHOOK_SIGNING_SECRET` is unset.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Return this agent to partner-level routing",
        "description": "Inbound traffic falls back to the partner-level webhook, which is Cadence's — so flows start being driven again.",
        "operationId": "delete_v1_agents_id_webhook",
        "tags": [
          "Google inbound routing"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Agent id."
          }
        ],
        "responses": {
          "200": {
            "description": "Removed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "effective": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such agent, or this agent has no webhook of its own.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "502": {
            "description": "Google rejected the request.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "RBM not configured.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations": {
      "get": {
        "summary": "List conversations",
        "description": "The measurement surface: each row carries the message counts and direction split that decide what it cost, so reply rate, completion and session-trigger rate are all computable from the public API. A brand-scoped key sees only its own brand, whatever it asks for.",
        "operationId": "get_v1_conversations",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "brandId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by brand."
          },
          {
            "name": "agentId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by agent."
          },
          {
            "name": "state",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Filter by conversation state."
          },
          {
            "name": "since",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "ISO 8601 lower bound on `createdAt`."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer"
            },
            "description": "Maximum rows."
          }
        ],
        "responses": {
          "200": {
            "description": "Conversations, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversations": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Conversation"
                      }
                    },
                    "count": {
                      "type": "number"
                    }
                  },
                  "required": [
                    "conversations",
                    "count"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "No usable session cookie, session token or API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Open a conversation and send the first message",
        "description": "Sandbox keys simulate delivery — nothing reaches a phone — so this is safe to call with fake E.164 numbers while you build.\n\n**To get two-way messaging without keeping a process alive, attach a `flow`.** Cadence then answers every reply on its own inbound webhook, so a recipient who replies an hour later, after your deploy, still gets the next step. Without one, nothing is answered for you: you drive the conversation yourself over `GET /v1/events`.\n\n`optin` is required and records the consent provenance you are asserting.\n\nThree fields worth knowing about, all optional and none obvious: **`idempotencyKey`** makes a retry safe — without it a cron that retries double-sends and double-bills, which is the single most expensive omission on this route. **`turnCeiling`** caps how long a conversation may run (default 30). **`phiSafe`** enforces PHI-safe-minimal content for clinical use.",
        "operationId": "post_v1_conversations",
        "tags": [
          "Conversations"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/OpenConversationRequest"
              },
              "example": {
                "agentId": "basal-checkin",
                "brandId": "brd_…",
                "to": "+12025550123",
                "useCase": "conversational",
                "optin": {
                  "method": "app_signup",
                  "capturedAt": "2026-08-22T17:00:00Z"
                },
                "flow": {
                  "id": "daily-checkin",
                  "name": "Daily check-in",
                  "disclaimer": "Msg&data rates may apply. Reply HELP for help, STOP to stop.",
                  "steps": [
                    {
                      "id": "weight",
                      "text": "Morning! What's your weight today?"
                    },
                    {
                      "id": "felt",
                      "text": "How did yesterday feel?",
                      "suggestions": [
                        "Easy",
                        "About right",
                        "Hard"
                      ]
                    }
                  ],
                  "closing": "Logged — see you tomorrow."
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Opened, and the first message sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversationId": {
                      "type": "string"
                    },
                    "providerMsgId": {
                      "type": "string",
                      "description": "Opening provider message ID; absent when a flow-only opening is queued."
                    },
                    "channel": {
                      "type": "string",
                      "enum": [
                        "rcs",
                        "sms",
                        "queued"
                      ]
                    },
                    "channelConfidence": {
                      "type": "string",
                      "enum": [
                        "provisional",
                        "committed"
                      ],
                      "description": "`provisional` means we chose a channel and have no confirmation yet. It becomes `committed` when the channel is proven: on the first delivery receipt from the handset on RCS, or on an SMS fallback **caused by the recipient** — no RCS on their device, or your agent not launched on their carrier.\n\nA fallback caused by *us* — a rejected payload, a bad credential, a timeout — leaves this `provisional`, because it says nothing about the recipient. Committing there would mark a perfectly reachable handset SMS-only for the rest of the conversation on the strength of our own bug, and report it as a fact about their phone. See `fellBack.ourFault` on the send response."
                    },
                    "trustLevel": {
                      "type": "string",
                      "enum": [
                        "verified_rcs",
                        "branded_sms",
                        "unbranded_sms"
                      ]
                    },
                    "fellBack": {
                      "$ref": "#/components/schemas/FellBack",
                      "description": "Present only when RCS was attempted and the opening went out over SMS. A `fellback` event is emitted as well."
                    },
                    "serverDriven": {
                      "type": "boolean",
                      "description": "Present when a flow is attached — Cadence answers replies for you."
                    },
                    "cardSent": {
                      "type": "boolean",
                      "description": "Whether `flow.card` was rendered. RCS only."
                    }
                  },
                  "required": [
                    "conversationId",
                    "channel"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Validation failed, or `opening` is missing with no flow attached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "BAD_REQUEST",
                  "message": "2 problems with the request body.",
                  "problems": [
                    "to: must be E.164",
                    "optin: Required"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Brand not found.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "Recipient opted out, or an idempotency conflict.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "422": {
            "description": "The flow can't be run, or the opening isn't step one. **Nothing was sent and no conversation was opened.**",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowInvalid"
                },
                "example": {
                  "error": "FLOW_INVALID",
                  "message": "The flow can't be run. Nothing was sent and no conversation was opened.",
                  "problems": [
                    "steps[1].followUps[0].when matches none of that step's suggestions"
                  ],
                  "opened": false,
                  "conversationId": null
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "This deployment can't send on a key of this kind.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "BACKEND_NOT_CONFIGURED"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}": {
      "get": {
        "summary": "Conversation detail",
        "description": "404 rather than 403 on someone else's conversation: the id is the only secret protecting a transcript, and a 403 would confirm it is real.",
        "operationId": "get_v1_conversations_id",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          }
        ],
        "responses": {
          "200": {
            "description": "The conversation.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Conversation"
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No conversation with that id on this key or account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "CONVERSATION_NOT_FOUND"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/messages": {
      "post": {
        "summary": "Send a message",
        "description": "Later turns go out under the same agent as the opening, resolved from the conversation itself rather than from this request. Unknown body fields are rejected rather than ignored.\n\n**A CLOSED conversation is still sendable**, on purpose — delivering a summary after a flow's closing is a real pattern. The response then carries `conversationState: \"CLOSED\"` and a `warning`, because there are two costs: it bills against the same conversation, so on one the recipient never replied to it can push past the included message count into overage; and Google's 24-hour session runs from the *recipient's* last message, not from our state, so if theirs was over 24 hours ago this opens a new billable session. Open a new conversation if you want a clean record.\n\nA CLOSED_OPTOUT conversation is not sendable, and never becomes so by asserting consent.",
        "operationId": "post_v1_conversations_id_messages",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/SendMessageRequest"
              },
              "example": {
                "text": "Logged — 182.4 lb.",
                "suggestions": [
                  "Undo",
                  "Thanks"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Sent.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "providerMsgId": {
                      "type": "string"
                    },
                    "channel": {
                      "type": "string",
                      "enum": [
                        "rcs",
                        "sms"
                      ],
                      "description": "The leg this message actually went out on. Always present, so a rich send that degraded to text is visible from the response alone."
                    },
                    "deliveredText": {
                      "type": "string",
                      "description": "What the recipient received on that leg: the fallback text on SMS, the composed text on RCS."
                    },
                    "degraded": {
                      "type": "object",
                      "properties": {
                        "at": {
                          "type": "string",
                          "enum": [
                            "conversation"
                          ]
                        },
                        "cause": {
                          "type": "string",
                          "enum": [
                            "channel_sms"
                          ]
                        },
                        "channelConfidence": {
                          "type": "string",
                          "enum": [
                            "provisional",
                            "committed"
                          ]
                        },
                        "note": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "at",
                        "cause",
                        "note"
                      ],
                      "description": "Present when rich content (media, card, carousel, chips) was sent into a conversation already on SMS. Not a fallback — nothing failed on this send — but the picture went as a link and the chips as a numbered list. `channelConfidence: committed` means the recipient cannot receive RCS; `provisional` means an earlier attempt failed on our side and a later message may go rich again."
                    },
                    "fellBack": {
                      "$ref": "#/components/schemas/FellBack",
                      "description": "Present only when RCS was attempted and this message went out over SMS instead. When `ourFault` is true the conversation stays on RCS and the next rich send is fine once the reason is fixed; `mediaRejected` names the file when that reason was your media URL. A `fellback` event carrying the same object is emitted to webhooks and the SSE stream."
                    },
                    "conversationState": {
                      "type": "string",
                      "description": "The conversation's state at the time of sending."
                    },
                    "warning": {
                      "type": "string",
                      "description": "Present only when sending into a conversation that had already CLOSED — see the description."
                    }
                  },
                  "required": [
                    "providerMsgId",
                    "channel"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Validation failed, or the turn ceiling was hit.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "TURN_CEILING_EXCEEDED"
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No conversation with that id on this key or account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The recipient opted out.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "RECIPIENT_OPTED_OUT"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "get": {
        "summary": "Message history",
        "description": "Per-message `channel`, so an RCS→SMS fallback is visible without subscribing to events.\n\n**Use `deliveredText`, not `body`.** `body` is the composed RCS text and stays that way on a message that went out over SMS, where the numbered fallback is what was actually read.",
        "operationId": "get_v1_conversations_id_messages",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          }
        ],
        "responses": {
          "200": {
            "description": "Transcript.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversationId": {
                      "type": "string"
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "conversationId",
                    "messages"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No conversation with that id on this key or account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/messages/{messageId}/delivery": {
      "get": {
        "summary": "Message delivery evidence",
        "description": "Read delivery state without message content. receipt is attributable RBM webhook evidence or null; legacy deliveryState alone is not this evidence. observedAt is when Cadence recorded the receipt, not handset delivery time. Sandbox evidence does not prove real handset delivery.",
        "operationId": "get_v1_conversations_id_messages_messageId_delivery",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          },
          {
            "name": "messageId",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Provider message id returned by send or open."
          }
        ],
        "responses": {
          "200": {
            "description": "Delivery evidence.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversationId": {
                      "type": "string"
                    },
                    "providerMsgId": {
                      "type": "string"
                    },
                    "channel": {
                      "type": "string"
                    },
                    "deliveryState": {
                      "type": "string"
                    },
                    "environment": {
                      "type": "string"
                    },
                    "receipt": {
                      "oneOf": [
                        {
                          "type": "object",
                          "properties": {
                            "state": {
                              "type": "string",
                              "enum": [
                                "delivered",
                                "read"
                              ]
                            },
                            "observedAt": {
                              "type": "string"
                            }
                          },
                          "required": [
                            "state",
                            "observedAt"
                          ]
                        },
                        {
                          "type": "null"
                        }
                      ]
                    }
                  },
                  "required": [
                    "conversationId",
                    "providerMsgId",
                    "channel",
                    "deliveryState",
                    "environment",
                    "receipt"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "Conversation not owned, or outbound provider message not found in it.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/answers": {
      "get": {
        "summary": "Structured flow answers",
        "description": "**The reason to run a flow.** `/messages` returns raw text with no step id and no chip payload, so results were otherwise only recoverable by re-parsing a transcript and guessing which reply answered which question.\n\nSafe to poll; also announced as a `completed` event on `GET /v1/events`.",
        "operationId": "get_v1_conversations_id_answers",
        "tags": [
          "Flows"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          }
        ],
        "responses": {
          "200": {
            "description": "Answers so far.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/FlowAnswers"
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No conversation with that id on this key or account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "This conversation has no flow attached.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NO_FLOW",
                  "remediation": "Open it with a `flow`, or read GET /v1/conversations/{id}/messages for raw text."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/simulate-inbound": {
      "post": {
        "summary": "Deliver a reply as though the recipient sent it",
        "description": "**Sandbox keys only.** This is how you exercise a flow — every branch, follow-up, closing and opt-out — without creating an agent at Google (irreversible) and waiting weeks for a carrier launch.\n\nIt runs the real inbound path rather than a lookalike, so what you see in sandbox is what the webhook will do. Refused on live keys: fabricating a message *from* a real person writes words they never sent into their transcript and consent record.",
        "operationId": "post_v1_conversations_id_simulate-inbound",
        "tags": [
          "Flows"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "text": {
                    "type": "string",
                    "description": "Something the recipient typed, e.g. `\"182.4\"`, `\"STOP\"` or `\"START\"`."
                  },
                  "optionIndex": {
                    "type": "number",
                    "description": "A chip they tapped, **1-based** against the reply chips currently on offer. Delivered as a real `postback`, so handler code branching on `intent` can be exercised before live traffic. Out of range is refused, with the offered chips in the body. Action chips have no index — use `postbackData` for those."
                  },
                  "postbackData": {
                    "type": "string",
                    "description": "A chip from **any recent message**, named by the slug we generated when we sent it. This is how you rehearse a *late tap*: someone reads the morning message and answers after the next one has already gone out. `optionIndex` cannot express that — it means \"a chip on the newest message\" — and a late tap behaves differently enough to be worth a test of its own. An unknown slug is refused with the ones we do know."
                  }
                }
              },
              "example": {
                "text": "About right"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "What actually happened, so you can assert on it rather than poll.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "delivered": {
                      "type": "string",
                      "description": "The text that was delivered — for a chip tap, its label."
                    },
                    "as": {
                      "type": "string",
                      "enum": [
                        "text",
                        "postback"
                      ]
                    },
                    "optionIndex": {
                      "type": "number",
                      "description": "Echoed when a chip was tapped."
                    },
                    "state": {
                      "type": "string",
                      "description": "The conversation's state afterwards."
                    },
                    "lastOutbound": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "What Cadence sent back, **as the recipient would have received it** on their channel — the numbered fallback on the SMS leg."
                    },
                    "lastOutboundComposed": {
                      "type": [
                        "string",
                        "null"
                      ],
                      "description": "The same message as composed, before fallback rendering."
                    },
                    "flow": {
                      "$ref": "#/components/schemas/FlowAnswers"
                    }
                  },
                  "required": [
                    "ok",
                    "delivered",
                    "state"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`text` is required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such conversation on this key or account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "The conversation is CLOSED. Deliberately asymmetric with `POST /messages`, which still sends into one: a real inbound would not be routed to a closed conversation, so simulating it would tell you something untrue. An **opted-out** conversation (`CLOSED_OPTOUT`) is still simulatable, because production does route a START to one — otherwise the resubscribe path carriers require could not be rehearsed at all.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "CONVERSATION_CLOSED"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/close": {
      "post": {
        "summary": "Close a conversation",
        "description": "Stops turn counting. A completed flow closes itself.",
        "operationId": "post_v1_conversations_id_close",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          }
        ],
        "responses": {
          "200": {
            "description": "Closed.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No conversation with that id on this key or account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/typing": {
      "post": {
        "summary": "Show the typing indicator",
        "description": "Shows the RCS typing bubble on the recipient's handset while you work out what to say.\n\nBuilt for agents rather than scripted flows. A flow replies in milliseconds and needs nothing; an agent calling a model takes seconds, and to the person holding the phone those seconds look identical to nothing happening.\n\n**Send it immediately before the work, not on a timer.** RCS has no \"stopped typing\" event — the indicator expires by itself after a few seconds and your next message clears it. If the work turns out to be fast, skip it: a bubble that fades with no message behind it reads worse than no bubble at all.\n\n**Never billed, and never fatal.** A conversation on SMS, a backend that cannot send events, and a rejection from Google all answer `200` with `ok: false` and a reason. The correct outcome of a broken typing indicator is a message with no bubble, not a caller who thinks their conversation is broken — so there is no need to branch on channel before calling it.\n\nWorks on a sandbox key, where it answers `simulated: true` and reaches no handset.",
        "operationId": "post_v1_conversations_id_typing",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          }
        ],
        "responses": {
          "200": {
            "description": "Whether an indicator was shown. `ok: false` is an outcome, not an error.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean",
                      "description": "False means nothing was shown, and that the conversation is fine."
                    },
                    "reason": {
                      "type": "string",
                      "description": "`CHANNEL_NOT_RCS`, `NOT_SUPPORTED` or `REJECTED`, when `ok` is false."
                    },
                    "simulated": {
                      "type": "boolean",
                      "description": "Present on a sandbox key."
                    },
                    "expiresInSecHint": {
                      "type": "integer",
                      "description": "Roughly how long the bubble lasts."
                    },
                    "note": {
                      "type": "string"
                    },
                    "message": {
                      "type": "string"
                    },
                    "channel": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No conversation with that id on this key or account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Faster than the indicator itself lasts, so re-sending changes nothing.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/detail": {
      "get": {
        "summary": "Full transcript with lifecycle events",
        "description": "Messages and lifecycle events on one timeline. Session-authed, for the dashboard.",
        "operationId": "get_v1_conversations_id_detail",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          }
        ],
        "responses": {
          "200": {
            "description": "Conversation, messages and events.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversation": {
                      "$ref": "#/components/schemas/Conversation"
                    },
                    "messages": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/Message"
                      }
                    },
                    "events": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "type": {
                            "type": "string"
                          },
                          "channel": {
                            "type": [
                              "string",
                              "null"
                            ]
                          },
                          "payload": {
                            "type": [
                              "object",
                              "null"
                            ],
                            "additionalProperties": true
                          },
                          "created_at": {
                            "type": "string"
                          }
                        },
                        "required": [
                          "type"
                        ]
                      }
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such conversation on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/preview": {
      "get": {
        "summary": "Rendered preview of a conversation",
        "description": "HTML by default, using the brand's real identity. `?format=json` returns the turns and whether the transcript demonstrates opt-out — worth checking before attaching it to a submission that can't be edited.",
        "operationId": "get_v1_conversations_id_preview",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          },
          {
            "name": "format",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string",
              "enum": [
                "json"
              ]
            },
            "description": "`json` for data instead of HTML."
          }
        ],
        "responses": {
          "200": {
            "description": "JSON when `format=json`; otherwise an HTML page.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversationId": {
                      "type": "string"
                    },
                    "turns": {
                      "type": "array",
                      "items": {
                        "type": "object",
                        "properties": {
                          "direction": {
                            "type": "string",
                            "enum": [
                              "in",
                              "out"
                            ]
                          },
                          "body": {
                            "type": "string"
                          },
                          "channel": {
                            "type": "string"
                          }
                        }
                      }
                    },
                    "demonstratesOptOut": {
                      "type": "boolean"
                    }
                  }
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No conversation with that id on this key or account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "No messages yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NOTHING_TO_PREVIEW"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/share": {
      "post": {
        "summary": "Publish the preview at a public link",
        "description": "Google asks for the agent's use case \"uploaded to a publicly accessible link\" — a reviewer opens it with no account, so an authenticated preview cannot satisfy that however good it looks. Opt-in and revocable, because a transcript is someone's messages.",
        "operationId": "post_v1_conversations_id_share",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          }
        ],
        "responses": {
          "200": {
            "description": "Public link.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "url": {
                      "type": "string"
                    },
                    "demonstratesOptOut": {
                      "type": "boolean",
                      "description": "Launch review checks for a STOP exchange specifically."
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "url",
                    "demonstratesOptOut"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No conversation with that id on this key or account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "409": {
            "description": "No messages yet.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "NOTHING_TO_PREVIEW"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/conversations/{id}/share/revoke": {
      "post": {
        "summary": "Revoke a published preview",
        "description": "The link stops resolving immediately.",
        "operationId": "post_v1_conversations_id_share_revoke",
        "tags": [
          "Conversations"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Conversation id."
          }
        ],
        "responses": {
          "200": {
            "description": "Revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No conversation with that id on this key or account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/events": {
      "get": {
        "summary": "Server-sent event stream of inbound activity",
        "description": "For an application that wants to react to replies in its own process. **It requires holding the connection open** — if you would rather not, attach a `flow` and let Cadence answer, then read `GET /v1/conversations/{id}/answers`.\n\nEvents: `ready` on connect, `ping` every 15s, then `reply`, `fellback`, `help`, `optout` and `completed`. Scoped to conversations this key or account owns — `agentId` is a convenience filter, not a boundary.",
        "operationId": "get_v1_events",
        "tags": [
          "Flows"
        ],
        "parameters": [
          {
            "name": "agentId",
            "in": "query",
            "required": false,
            "schema": {
              "type": "string"
            },
            "description": "Only relay events for this agent."
          }
        ],
        "responses": {
          "200": {
            "description": "An `text/event-stream`. Each event's `data` is the JSON below.",
            "content": {
              "text/event-stream": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "conversationId": {
                      "type": "string"
                    },
                    "agentId": {
                      "type": "string"
                    },
                    "type": {
                      "type": "string",
                      "enum": [
                        "reply",
                        "fellback",
                        "help",
                        "optout",
                        "optin",
                        "completed"
                      ]
                    },
                    "intent": {
                      "type": "string",
                      "enum": [
                        "free_text",
                        "postback",
                        "optin",
                        "optout",
                        "help"
                      ],
                      "description": "How the reply was classified. A `reply` event carries `free_text` or `postback` in practice: `optout` and `help` are delivered as their own event types rather than as a reply, and `optin` (START) reverses an opt-out and is reported on the reply that carried it."
                    },
                    "body": {
                      "type": "string",
                      "description": "What they sent."
                    },
                    "value": {
                      "type": "string",
                      "description": "A chip's postback payload, when the reply was a tap."
                    },
                    "trustLevel": {
                      "type": "string"
                    },
                    "optionIndex": {
                      "type": "number",
                      "description": "**1-based** position in the chips we offered, resolved server-side — the same number the SMS fallback names. Prefer `optionLabel` over indexing."
                    },
                    "optionLabel": {
                      "type": "string"
                    },
                    "completion": {
                      "type": "number",
                      "description": "On `completed`: the fraction of base steps answered."
                    }
                  },
                  "required": [
                    "conversationId",
                    "agentId",
                    "type"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Missing, unknown or revoked API key.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "INVALID_API_KEY",
                  "message": "Unknown or revoked key. Get one at POST /v1/signup."
                }
              }
            }
          },
          "403": {
            "description": "Key suspended, or scoped to a different brand than the one addressed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks": {
      "get": {
        "summary": "List delivery webhooks",
        "description": "**We POST to you.** This is the surface for an application that has no process to keep running — a Cloud Function, a Lambda, anything that wakes on a request. `GET /v1/events` needs a held connection; a `flow` answers for you but cannot consult your own model. This does neither: it hands the event to your endpoint and lets you decide.\n\nNot to be confused with `PUT /v1/agents/{id}/webhook`, which points *Google* at a URL instead of at Cadence and switches off flow handling entirely.",
        "operationId": "get_v1_webhooks",
        "tags": [
          "Delivery webhooks"
        ],
        "responses": {
          "200": {
            "description": "Your endpoints, with health.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhooks": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/DeliveryWebhook"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "webhooks"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "post": {
        "summary": "Register a delivery webhook",
        "description": "**We POST to you.** This is the surface for an application that has no process to keep running — a Cloud Function, a Lambda, anything that wakes on a request. `GET /v1/events` needs a held connection; a `flow` answers for you but cannot consult your own model. This does neither: it hands the event to your endpoint and lets you decide.\n\nNot to be confused with `PUT /v1/agents/{id}/webhook`, which points *Google* at a URL instead of at Cadence and switches off flow handling entirely.\n\nThe signing secret is returned **once** and cannot be read back — it is derived from a server secret rather than stored. Rotate with `PATCH /v1/webhooks/{id} {\"rotateSecret\": true}`.\n\nDelivery is at-least-once: one attempt plus six retries spanning about nine hours (~10s, 1m, 5m, 30m, 2h, 6h after each failure). Anything other than a 2xx is a failure; so is taking longer than 10s. `nextAttemptAt` is the earliest we will retry, not a promise — the loop wakes on an interval, so delivery lands at or shortly after it. After 20 consecutive events exhaust their retries the endpoint is disabled with a reason.",
        "operationId": "post_v1_webhooks",
        "tags": [
          "Delivery webhooks"
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string",
                    "description": "A public `https://` endpoint. Loopback and private ranges are refused — the payload carries message bodies and phone numbers."
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string",
                      "enum": [
                        "reply",
                        "fellback",
                        "help",
                        "optout",
                        "optin",
                        "completed"
                      ]
                    },
                    "description": "Defaults to all six. An unknown name is rejected rather than silently subscribing to nothing. **Subscribe to `optin` if you cache consent**: `optout` sets your flag and nothing else clears it."
                  },
                  "brandId": {
                    "type": "string",
                    "description": "Narrow to one brand. Omit for every brand on the account."
                  },
                  "description": {
                    "type": "string"
                  }
                },
                "required": [
                  "url"
                ]
              },
              "example": {
                "url": "https://basal.ai/api/cadence",
                "events": [
                  "reply",
                  "completed",
                  "optout"
                ]
              }
            }
          }
        },
        "responses": {
          "201": {
            "description": "Registered. The secret is shown once.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DeliveryWebhook"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "secret": {
                          "type": "string",
                          "description": "Shown once. Sign with this."
                        },
                        "verify": {
                          "type": "string",
                          "description": "How to verify, in one sentence."
                        },
                        "note": {
                          "type": "string"
                        },
                        "nextStep": {
                          "type": "string"
                        }
                      },
                      "required": [
                        "secret"
                      ]
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad URL, or an unknown event name.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "BAD_REQUEST",
                  "message": "url must be https:// — the payload carries message bodies and phone numbers."
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "403": {
            "description": "A brand-scoped key tried to subscribe for another brand, or the key isn't linked to an account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such brand on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "BRAND_NOT_FOUND"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "503": {
            "description": "`WEBHOOK_SIGNING_SECRET` is unset on this deployment.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "WEBHOOK_SECRET_MISSING"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}": {
      "get": {
        "summary": "One delivery webhook, with health",
        "description": "`health.consecutiveFailures` counts events that exhausted every retry, not attempts.",
        "operationId": "get_v1_webhooks_id",
        "tags": [
          "Delivery webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Webhook id."
          }
        ],
        "responses": {
          "200": {
            "description": "The endpoint.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/DeliveryWebhook"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such webhook on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "patch": {
        "summary": "Update, disable, re-enable or rotate",
        "description": "Setting `active: true` on a disabled endpoint also clears its failure history — otherwise it would be disabled again by its past rather than by anything it did after the fix.\n\n`brandId` is fixed at creation: delete and re-create to change scope. Unknown fields are rejected rather than ignored.",
        "operationId": "patch_v1_webhooks_id",
        "tags": [
          "Delivery webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Webhook id."
          }
        ],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "url": {
                    "type": "string"
                  },
                  "events": {
                    "type": "array",
                    "items": {
                      "type": "string"
                    }
                  },
                  "description": {
                    "type": [
                      "string",
                      "null"
                    ]
                  },
                  "active": {
                    "type": "boolean",
                    "description": "`false` pauses delivery; `true` resumes and clears the failure count."
                  },
                  "rotateSecret": {
                    "type": "boolean",
                    "description": "Issues a new signing secret and returns it once. The previous one stops verifying immediately."
                  }
                }
              },
              "example": {
                "active": true
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Updated. Carries `secret` only when rotated.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/DeliveryWebhook"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "secret": {
                          "type": "string",
                          "description": "Only present when `rotateSecret` was true."
                        },
                        "note": {
                          "type": "string"
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Bad URL, unknown event name, or unknown field.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNKNOWN_FIELDS"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such webhook on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      },
      "delete": {
        "summary": "Delete a delivery webhook",
        "description": "Queued deliveries for it go too. To stop delivery without losing the record, `PATCH` it to `active: false`.",
        "operationId": "delete_v1_webhooks_id",
        "tags": [
          "Delivery webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Webhook id."
          }
        ],
        "responses": {
          "200": {
            "description": "Deleted.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such webhook on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/test": {
      "post": {
        "summary": "Send a signed test event now",
        "description": "Delivers a `ping` immediately and reports exactly what your endpoint did — status, body, or the transport error. Signature verification is the part integrators get wrong, usually by signing a re-serialised body, and finding that out from a silently missing event is an awful way to learn it. The ping is queued like any other event, so a failure is retried normally.",
        "operationId": "post_v1_webhooks_id_test",
        "tags": [
          "Delivery webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Webhook id."
          }
        ],
        "responses": {
          "200": {
            "description": "Your endpoint accepted it.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    },
                    "deliveryId": {
                      "type": "string"
                    },
                    "eventId": {
                      "type": "string"
                    },
                    "status": {
                      "type": [
                        "number",
                        "null"
                      ]
                    },
                    "error": {
                      "type": [
                        "string",
                        "null"
                      ]
                    }
                  },
                  "required": [
                    "ok",
                    "deliveryId",
                    "eventId"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such webhook on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/webhooks/{id}/deliveries": {
      "get": {
        "summary": "Recent delivery attempts",
        "description": "Each attempt with your endpoint's own status and the first 2 kB of its response. A webhook you cannot inspect is one you debug by guessing.",
        "operationId": "get_v1_webhooks_id_deliveries",
        "tags": [
          "Delivery webhooks"
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "Webhook id."
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 20
            },
            "description": "1–100, default 20."
          }
        ],
        "responses": {
          "200": {
            "description": "Attempts, newest first.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "webhookId": {
                      "type": "string"
                    },
                    "deliveries": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/WebhookDelivery"
                      }
                    },
                    "note": {
                      "type": "string"
                    }
                  },
                  "required": [
                    "webhookId",
                    "deliveries"
                  ]
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "404": {
            "description": "No such webhook on this account.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/readiness/check": {
      "post": {
        "summary": "Check a brand against carrier review",
        "description": "Also checks agent artwork when `logoUrl`/`bannerUrl` are given, which is the point: the only other place artwork is inspected is `PATCH /agents/{id}/google`, and that requires the irreversible `sync-google` — which is the step that *sends* the artwork. Checking here is the last moment a mistake is free.\n\nPublic and unauthenticated. Carrier review rejects on a small, predictable set of issues — missing privacy or consent disclosures, non-SMS \"terms\", brand/entity mismatch, too-new domains. A clean submission lands in days; one rejection costs weeks. Also generates the policy text a brand is missing.",
        "operationId": "post_v1_readiness_check",
        "tags": [
          "Readiness"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/ReadinessCheckRequest"
              },
              "example": {
                "brand": "Basal",
                "email": "you@example.com",
                "website": "https://basal.ai",
                "useCase": "daily weight check-ins"
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Report, plus a share token for a linkable URL.",
            "content": {
              "application/json": {
                "schema": {
                  "allOf": [
                    {
                      "$ref": "#/components/schemas/ReadinessReport"
                    },
                    {
                      "type": "object",
                      "properties": {
                        "leadId": {
                          "type": "string"
                        },
                        "shareToken": {
                          "type": "string",
                          "description": "Use with `GET /v1/readiness/report/{token}`."
                        }
                      }
                    }
                  ]
                }
              }
            }
          },
          "400": {
            "description": "Validation failed.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited per IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/readiness/report/{token}": {
      "get": {
        "summary": "Fetch a shared readiness report",
        "description": "Report body only — never the email or lead metadata.",
        "operationId": "get_v1_readiness_report_token",
        "tags": [
          "Readiness"
        ],
        "security": [],
        "parameters": [
          {
            "name": "token",
            "in": "path",
            "required": true,
            "schema": {
              "type": "string"
            },
            "description": "From `POST /v1/readiness/check`."
          }
        ],
        "responses": {
          "200": {
            "description": "The report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/ReadinessReport"
                }
              }
            }
          },
          "404": {
            "description": "That report link is invalid or was revoked.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/readiness/help": {
      "post": {
        "summary": "Ask for help with a report",
        "description": "Flags the report for follow-up.",
        "operationId": "post_v1_readiness_help",
        "tags": [
          "Readiness"
        ],
        "security": [],
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "leadId": {
                    "type": "string"
                  },
                  "note": {
                    "type": "string",
                    "description": "≤2000 characters."
                  }
                },
                "required": [
                  "leadId"
                ]
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Flagged.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "400": {
            "description": "`leadId` required.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "404": {
            "description": "No such report.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "429": {
            "description": "Rate limited per IP.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/track": {
      "post": {
        "summary": "Page-view beacon",
        "description": "Path and referrer only — no IP, no user agent. Never fails a page.",
        "operationId": "post_v1_track",
        "tags": [
          "Readiness"
        ],
        "security": [],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": {
                "type": "object",
                "properties": {
                  "path": {
                    "type": "string"
                  },
                  "referrer": {
                    "type": "string"
                  }
                }
              }
            }
          }
        },
        "responses": {
          "200": {
            "description": "Always.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "properties": {
                    "ok": {
                      "type": "boolean"
                    }
                  },
                  "required": [
                    "ok"
                  ]
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    },
    "/v1/usage": {
      "get": {
        "summary": "Usage and cost for a calendar month",
        "description": "Sandbox conversations are counted and never billed.",
        "operationId": "get_v1_usage",
        "tags": [
          "Accounts"
        ],
        "parameters": [
          {
            "name": "monthsAgo",
            "in": "query",
            "required": false,
            "schema": {
              "type": "integer",
              "default": 0
            },
            "description": "0 is the current month."
          }
        ],
        "responses": {
          "200": {
            "description": "Usage.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Usage"
                }
              }
            }
          },
          "401": {
            "description": "Not signed in, and no account-linked API key was sent.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                },
                "example": {
                  "error": "UNAUTHENTICATED",
                  "message": "Sign in, or send an account-linked API key as `Authorization: Bearer sk_…`."
                }
              }
            }
          },
          "500": {
            "description": "Unexpected server error. The body is always JSON.",
            "content": {
              "application/json": {
                "schema": {
                  "$ref": "#/components/schemas/Error"
                }
              }
            }
          }
        }
      }
    }
  }
}
