{
  "$schema": "https://json-schema.org/draft/2020-12/schema",
  "$id": "https://artifacts.apis.io/corebee-ai/json-schema/corebee-ai-bridge-create-schema.json",
  "title": "BridgeCreate",
  "description": "Start a business-first two-party bridge. business_number and consumer_number are required E.164 numbers.",
  "x-generated": "2026-11-04",
  "x-method": "derived",
  "x-generator": "derive-json-schema.py",
  "x-source": "openapi/corebee-ai-openapi.yml#/components/schemas/BridgeCreate",
  "type": "object",
  "required": [
    "business_number",
    "consumer_number"
  ],
  "properties": {
    "business_number": {
      "type": "string",
      "description": "E.164 number dialed FIRST (the business)."
    },
    "consumer_number": {
      "type": "string",
      "description": "E.164 number dialed only after a live human is confirmed on the business leg."
    },
    "from_number": {
      "type": "string",
      "description": "Optional E.164 caller-ID. Must be a Telnyx DID owned by the credential org; a default connected DID is used when omitted."
    },
    "prep_line": {
      "type": "string",
      "maxLength": 500,
      "description": "Optional short line about why the consumer is calling. On the AI-conversation leg it becomes the agent's reasonForCall: spoken as soon as the business agrees to help, before the consumer is bridged in. ⚠️ ONLY THE FIRST ~180 CHARACTERS ARE SPOKEN ON THAT LEG. Longer text is cut at the last sentence boundary inside that budget (or, failing one, at a word boundary), so anything after it is never heard: measured on live traffic, prep lines average ~300 characters and ~40% of the text was discarded unannounced. Put the specifics the person answering needs, the vehicle, the request, the reference, in the FIRST sentence. The full text IS spoken on a briefing-only call (up to 500 characters, in the closing message) and on the legacy leg, verbatim with the required AI-disclosure prefixed."
    },
    "dtmf_plan": {
      "type": "array",
      "maxItems": 1,
      "description": "Optional single IVR navigation step. Multi-step plans are rejected with 400 multi_step_ivr_unsupported: the bridge cannot pace steps 2..N today, so a multi-step plan would burn a channel and stall. When absent, IVR navigation is skipped. Digits use 0-9, A-D, *, # and the in-string pauses w (0.5s) / W (1s).",
      "items": {
        "$ref": "#/$defs/BridgeDtmfStep"
      }
    },
    "idempotency_key": {
      "type": "string",
      "maxLength": 255,
      "description": "Optional client key ([A-Za-z0-9._:-]). Reusing a key for a live session returns 409 with a reference to the existing session."
    },
    "request_recording_consent": {
      "type": "boolean",
      "default": false,
      "description": "Optional. The A/B toggle for consumer recording on THIS call. When true AND consumer recording is enabled server-side, the consumer hears a spoken recording-consent disclosure before being bridged; if they stay on the line (implied consent) the bridged consumer<->business conversation is recorded and transcribed, and if they hang up during the disclosure the call is simply not recorded (it is never dropped for declining). Necessary but NOT sufficient: recording is additionally gated by a server-side flag that is OFF by default pending legal sign-off, so in current production this field records nothing and the bridge is byte-identical whether it is true or false. Default false is the control arm. A non-boolean value is rejected with 400 validation_error."
    },
    "consent": {
      "allOf": [
        {
          "$ref": "#/$defs/BridgeConsent"
        }
      ],
      "description": "Optional evidence that the consumer agreed to be called. VALIDATED WHEN PRESENT: a malformed blob is a 400 validation_error rather than being stored, because a consent record that looks like evidence and is not is worse in an audit than no record at all. Omitting it does not refuse the dial today."
    },
    "briefing_only": {
      "type": "boolean",
      "default": false,
      "description": "Optional test flag. When true the business is dialed and AMD-verified and the prep line is spoken, then the session COMPLETES without ever dialing the consumer and without bridging, so nothing is billed and no second person is called. Absent or false is the full production flow. A non-boolean value is a 400 validation_error, never coerced. Note that briefing-only sessions are excluded from GET /voice/bridge/usage, including from its `total`."
    },
    "metadata": {
      "type": "object",
      "additionalProperties": true,
      "description": "Free-form bag echoed back on the status projection. `customer_ref` (string, <=255 chars) is the one key we treat specially: it is your correlation id, it is echoed on webhooks and on the usage feed, and it is what customer_ref= filters on. No other key in here is interpreted."
    },
    "voice": {
      "type": "object",
      "additionalProperties": false,
      "description": "OPTIONAL. The voice and lines for THIS call only, so a dealer keeps its voice whichever of your numbers dials it. Leave it out and the call uses the calling number's voice and lines, exactly as before. Send at least one field. Each field you send wins over the same field on the voice profile you name; lines are merged key by key, and are then merged over the calling number's own lines. Nothing here is stored on your account: it applies to this call and is recorded on it.\n\nA voice_id must be a voice we have already checked for your organization: one from GET /voice/bridge/voices, or one you saved on a voice profile, on one of your numbers, or as your organization default. Any other voice is refused with 400 voice_not_verified (save it in one of those places first; saving checks it). A custom voice you revoked is refused the same way, wherever it is still saved. Creating a call never waits on a voice check.\n\nmetadata.voice_stamp is reserved: we record the call's voice under that key, so a value you send there is removed.\n\nRETRIES: send an idempotency_key whenever you send `voice`. Without one, a retry is matched on the request body plus the resolved voice settings, so a voice profile edited between your first attempt and a retry makes the retry a NEW call.",
      "properties": {
        "profile_id": {
          "type": "string",
          "pattern": "^vp_[a-z2-7]{12}$",
          "description": "A voice profile from GET /voice/bridge/profiles. Its voice, lines, speed, volume and language apply unless this object sends the same field. Unknown id: 404 profile_not_found. Voice profiles switched off for your organization: 403 feature_not_enabled."
        },
        "voice_id": {
          "type": "string",
          "format": "uuid",
          "description": "The agent voice for this call. Must be a recommended voice (GET /voice/bridge/voices) or one already saved and checked for your organization; otherwise 400 voice_not_verified. Not a voice id at all: 400 voice_not_found."
        },
        "speech_overrides": {
          "type": "object",
          "description": "The agent lines for this call: {\"templates\": {<key>: <line>}, \"variables\": {\"dealership\": ..., \"department\": ...}}. Same keys and rules as PATCH /voice/bridge/numbers/instructions, at most 2048 bytes. Refused lines give 400 invalid_speech_overrides naming each problem.",
          "properties": {
            "templates": {
              "type": "object",
              "additionalProperties": {
                "type": "string"
              }
            },
            "variables": {
              "type": "object",
              "additionalProperties": {
                "type": "string"
              }
            }
          }
        },
        "speed": {
          "type": "number",
          "minimum": 0.7,
          "maximum": 1.3,
          "description": "Speaking pace for this call, 0.7 to 1.3 (1.0 is normal). Refused with field_not_yet_supported until switched on for this deployment."
        },
        "volume": {
          "type": "number",
          "minimum": 0.5,
          "maximum": 1.5,
          "description": "Speaking level for this call, 0.5 to 1.5 (1.0 is normal). Refused with field_not_yet_supported until switched on for this deployment."
        },
        "language": {
          "type": "string",
          "enum": [
            "en",
            "es"
          ],
          "description": "'en' is accepted (it is what every call speaks today); 'es' is refused with field_not_yet_supported until switched on."
        }
      }
    }
  },
  "$defs": {
    "BridgeConsent": {
      "type": "object",
      "description": "Partner-captured evidence that the consumer asked to be connected. All three fields are required ONCE the object is supplied. Extra keys are preserved (ip, user_agent, form version, ...), bounded by the total serialized size.",
      "required": [
        "method",
        "captured_at",
        "reference"
      ],
      "properties": {
        "method": {
          "type": "string",
          "maxLength": 64,
          "description": "How consent was captured, e.g. web_form, recorded_call, sms_reply. Deliberately NOT an enum: the legal taxonomy of consent is yours to assert, and refusing a method string we had not enumerated would throw away real evidence over a vocabulary quibble. We bound its shape, not its meaning."
        },
        "captured_at": {
          "type": "string",
          "format": "date-time",
          "description": "ISO-8601 instant at which consent was captured, normalized on validation. A timestamp in the future is rejected, consent captured after the call it justifies is not consent, with about five minutes of tolerance for a fast partner clock."
        },
        "reference": {
          "type": "string",
          "maxLength": 200,
          "description": "Pointer to the evidence on your side: a record id, a URL, a recording id."
        }
      },
      "additionalProperties": true
    },
    "BridgeDtmfStep": {
      "type": "object",
      "required": [
        "digits"
      ],
      "properties": {
        "digits": {
          "type": "string",
          "description": "Digits 0-9, A-D, *, # plus in-string pauses w (0.5s) / W (1s)."
        },
        "wait_ms": {
          "type": "integer",
          "description": "Advisory wait after the step."
        },
        "note": {
          "type": "string"
        }
      }
    }
  }
}