openapi: 3.1.0
info:
  title: TalkChief Integration APIs
  version: "1.0.0"
  summary: Click-to-Call, AI Transcription, and the completed-call CDR webhook.
  description: |
    Machine-readable contract for the TalkChief integration APIs.

    **Base URL.** The host is not published. Your account's base URL is shown in
    **Settings → Integration** alongside your keys; substitute it for the
    `baseUrl` server variable before generating a client.

    **Keys.** Each integration carries its own 64-character key, generated in
    Settings → Integration, so one can be rotated without disturbing the other.
    Rotation takes effect immediately — anything still using the old key starts
    failing at once, so deploy the new key straight after rotating.
  contact:
    name: TalkChief support
    email: support@talkchief.io
    url: https://talkchief.io/en/developers
  license:
    name: Proprietary — for authorized TalkChief customers
    url: https://talkchief.io/en/legal

servers:
  - url: "{baseUrl}"
    description: Account base URL from Settings → Integration
    variables:
      baseUrl:
        default: https://YOUR-BASE-URL
        description: >-
          Provided in the TalkChief dashboard under Settings → Integration.
          The production host is not published.

tags:
  - name: Click-to-Call
    description: Ring a member and connect them to a destination.
  - name: AI Transcription
    description: Submit a recording and retrieve a diarized transcript.
  - name: Billing
    description: Balance and reconciliation for AI transcription.
  - name: CDR Webhook
    description: >-
      TalkChief calls you. Not an endpoint you call — documented here so the
      payload can be generated as a type. Configure in Settings → Integration.

security: []

paths:
  /calls/click-to-call:
    post:
      tags: [Click-to-Call]
      operationId: clickToCall
      summary: Ring a member and connect them to a destination
      description: |
        Rings the member, then connects them to `destination`. Accepts JSON or
        form-encoded fields.

        **Errors arrive with HTTP 200.** This endpoint keeps the legacy
        contract: branch on `success` and `error_code` in the body, not on the
        HTTP status.
      security:
        - ApiKeyHeader: []
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/ClickToCallRequest"}
            examples:
              byEmail:
                summary: Place the call as a member identified by email
                value:
                  api_key: YOUR_KEY
                  destination: "972599796989"
                  email: agent@yourcompany.com
              byExtensionWithCrmReference:
                summary: Identify the member by extension and echo a CRM record id
                value:
                  api_key: YOUR_KEY
                  destination: "97145550000"
                  extension: "501"
                  auto_answer: "false"
                  custom_param:
                    crm_record_id: "A-10422"
          application/x-www-form-urlencoded:
            schema: {$ref: "#/components/schemas/ClickToCallRequest"}
      responses:
        "200":
          description: >-
            Always returned, for success and failure alike. Inspect `success`
            and `error_code` in the body.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/ClickToCallResponse"}

  /ai/transcriptions:
    post:
      tags: [AI Transcription]
      operationId: createTranscription
      summary: Submit a recording for transcription
      description: |
        Returns an `id` to poll with `GET /ai/transcriptions/{id}`. Append
        `?wait=1` to hold the request open instead of polling.

        Billed at $0.025 per minute with a 60-second minimum, prorated per
        second after that. A balance of at least $5 is required; below it
        nothing is submitted and nothing is charged.
      security:
        - AiApiKey: []
      parameters:
        - name: wait
          in: query
          required: false
          description: Set to `1` to hold the request open until the transcript is ready instead of polling.
          schema: {type: integer, enum: [1]}
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/TranscriptionRequest"}
      responses:
        "200":
          description: Transcription accepted, or completed when `wait=1` was used.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Transcription"}
        "402":
          description: Balance below the $5 minimum. Nothing was submitted and nothing was charged.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
        "401":
          $ref: "#/components/responses/Unauthorized"

  /ai/transcriptions/{id}:
    get:
      tags: [AI Transcription]
      operationId: getTranscription
      summary: Retrieve a transcription
      description: Transcription is diarized, so each utterance carries a speaker.
      security:
        - AiApiKey: []
      parameters:
        - name: id
          in: path
          required: true
          description: Identifier returned when the recording was submitted.
          schema: {type: string}
      responses:
        "200":
          description: Current state of the transcription.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Transcription"}
        "404":
          description: No transcription with that id for this account.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Error"}
        "401":
          $ref: "#/components/responses/Unauthorized"

  /ai/billing/balance:
    get:
      tags: [Billing]
      operationId: getBalance
      summary: Current balance in USD
      security:
        - AiApiKey: []
      responses:
        "200":
          description: Current balance.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Balance"}
        "401":
          $ref: "#/components/responses/Unauthorized"

  /ai/billing/charge:
    post:
      tags: [Billing]
      operationId: chargeBalance
      summary: Charge the balance
      description: >-
        A completed transcription charges itself. This endpoint exists for
        reconciliation.
      security:
        - AiApiKey: []
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/ChargeRequest"}
      responses:
        "200":
          description: Resulting balance after the charge.
          content:
            application/json:
              schema: {$ref: "#/components/schemas/Balance"}
        "401":
          $ref: "#/components/responses/Unauthorized"

webhooks:
  callCompleted:
    post:
      tags: [CDR Webhook]
      operationId: cdrWebhook
      summary: A completed call record, delivered to your endpoint
      description: |
        TalkChief POSTs a record of every completed call to an endpoint of your
        choice as soon as the call ends. Nothing to poll, no key to send.
        Configure it in Settings → Integration → CDR Webhook: endpoint, method
        (POST, PUT or GET), body format (JSON or form data) and retry count.

        **Deduplicate on `providerCallId`.** A call is delivered once, but a
        delivery we could not confirm is retried, so the same call can arrive
        twice carrying the same id. Treat it as your primary key and the retry
        becomes harmless. Never deduplicate on `legCallId` — one call is made of
        several legs and that names only one of them.

        **Responding.** Any `2xx` is treated as delivered. Anything else is
        retried with backoff; sustained failure disables the hook and Settings
        shows the reason.

        JSON bodies are UTF-8 and never ASCII-escaped, so non-Latin names arrive
        readable.
      requestBody:
        required: true
        content:
          application/json:
            schema: {$ref: "#/components/schemas/CallRecord"}
            examples:
              answeredOutbound:
                summary: Answered outbound call
                value:
                  providerCallId: 2d489b415666b49444c2395700bcfd82@acme.talkchief.io
                  direction: outbound
                  status: answered
                  from: "501"
                  to: "+972542421166"
                  startedAt: "2026-07-30T14:22:27+03:00"
                  answeredAt: "2026-07-30T14:22:42+03:00"
                  endedAt: "2026-07-30T14:22:53+03:00"
                  durationSeconds: 11
                  ringingSeconds: 4
                  totalSeconds: 26
                  agentExtension: "501"
                  agentName: Issam Nasrallah
                  agentEmail: agent@yourcompany.com
                  agentTeam: Sales
                  callerName: Issam Nasrallah
                  recording: null
                  recordingAvailable: false
                  queue: null
                  hangupCause: NORMAL_CLEARING
                  hangupCode: "200"
      responses:
        "2XX":
          description: Treated as delivered.

components:
  securitySchemes:
    ApiKeyHeader:
      type: apiKey
      in: header
      name: X-API-Key
      description: >-
        Click-to-Call key (64 characters). May also travel in the body as
        `api_key`. Manage it in Settings → Integration.
    AiApiKey:
      type: apiKey
      in: header
      name: X-AI-Api-Key
      description: AI Transcription & Billing key (64 characters), from Settings → Integration.

  responses:
    Unauthorized:
      description: Missing or invalid key.
      content:
        application/json:
          schema: {$ref: "#/components/schemas/Error"}

  schemas:
    ClickToCallRequest:
      type: object
      required: [api_key, destination]
      properties:
        api_key:
          type: string
          description: Your Click-to-Call API key (64 characters). May be sent as the `X-API-Key` header instead.
          minLength: 64
          maxLength: 64
        destination:
          type: string
          description: Number to dial, digits only, at least 9 digits. Include the country code.
          pattern: "^[0-9]{9,}$"
          examples: ["972599796989"]
        email:
          type: string
          format: email
          description: >-
            Email of the member who places the call. Required unless
            `extension` is sent instead.
        extension:
          type: string
          description: >-
            Required only when `email` is omitted. Extension of the member who
            places the call (up to 4 digits). Ignored if `email` is also sent.
          maxLength: 4
        auto_answer:
          type: string
          enum: ["true", "false"]
          default: "true"
          description: >-
            `true` auto-answers on the member's desk phone; `false` rings all
            their devices.
        custom_param:
          type: object
          additionalProperties: true
          description: Key/value pairs echoed back and attached to the call, e.g. your CRM record id.
      anyOf:
        - required: [email]
        - required: [extension]

    ClickToCallResponse:
      type: object
      properties:
        success:
          type: boolean
          description: Branch on this, not on the HTTP status — failures also return HTTP 200.
        error_code:
          type: [string, "null"]
          description: Present when `success` is false.
        message:
          type: string

    TranscriptionRequest:
      type: object
      description: >-
        Recording source for transcription. The accepted source fields for your
        account are documented with your keys in Settings → Integration.
      additionalProperties: true

    Transcription:
      type: object
      properties:
        id:
          type: string
          description: Poll this with `GET /ai/transcriptions/{id}`.
        status:
          type: string
          description: Processing state of the job.
        language:
          type: [string, "null"]
          description: Detected language, where supported.
        utterances:
          type: array
          description: Diarized transcript — each utterance carries a speaker.
          items: {$ref: "#/components/schemas/Utterance"}

    Utterance:
      type: object
      properties:
        speaker: {type: string}
        text: {type: string}
        start: {type: number, description: Seconds from the start of the recording.}
        end: {type: number, description: Seconds from the start of the recording.}

    Balance:
      type: object
      properties:
        balance:
          type: number
          description: Current balance in USD. At least $5 is required to submit a transcription.
        currency: {type: string, examples: ["USD"]}

    ChargeRequest:
      type: object
      required: [amount]
      properties:
        amount: {type: number, description: Amount in USD to charge against the balance.}
        reference: {type: string, description: Your reconciliation reference.}

    Error:
      type: object
      properties:
        error: {type: string}
        error_code: {type: [string, "null"]}
        message: {type: string}

    CallRecord:
      type: object
      description: |
        `durationSeconds` is talk time, not wall time. Two identities always
        hold: `endedAt − answeredAt = durationSeconds` and
        `endedAt − startedAt = totalSeconds`.
      required:
        [providerCallId, direction, status, from, to, startedAt, durationSeconds, agentExtension, recordingAvailable]
      properties:
        providerCallId:
          type: string
          description: >-
            Stable unique id for the call. Use it to deduplicate — a call is
            reported once, and a redelivery carries the same value.
        direction:
          type: string
          enum: [inbound, outbound, internal]
        status:
          type: string
          enum: [answered, busy, no-answer, cancelled, abandoned, voicemail, congestion, failed]
        from:
          type: string
          description: Calling number in E.164 where it is a real number; internal extensions are sent as dialled.
        to:
          type: string
          description: Called number, same rules as `from`.
        startedAt:
          type: string
          format: date-time
          description: ISO-8601 with offset, in your account timezone.
        answeredAt:
          type: [string, "null"]
          format: date-time
          description: When the call was answered. Null when it never was.
        endedAt:
          type: [string, "null"]
          format: date-time
          description: When the call ended.
        durationSeconds:
          type: integer
          description: >-
            Billed talk time. Not `endedAt − startedAt`, which also contains the
            ringing.
        ringingSeconds:
          type: [integer, "null"]
          description: >-
            How long it rang, as the platform reports it for this call. Do not
            expect it to equal `answeredAt − startedAt`: a call is made of
            several legs and this figure describes one of them.
        totalSeconds:
          type: [integer, "null"]
          description: Whole call, ringing included. Always equals `endedAt − startedAt`.
        agentExtension:
          type: string
          description: The internal party — whoever answered an inbound call or placed an outbound one.
        agentName: {type: [string, "null"], description: Member name, UTF-8.}
        agentEmail:
          type: [string, "null"]
          format: email
          description: Member email, a stable identifier beside the extension.
        agentTeam: {type: [string, "null"], description: Team the member belongs to.}
        callerName: {type: [string, "null"], description: Caller ID name as presented, UTF-8.}
        recording:
          type: [string, "null"]
          format: uri
          description: URL of the recording, when one exists.
        recordingAvailable:
          type: boolean
          description: True exactly when `recording` carries a URL — the two always agree, so you can branch on either.
        queue: {type: [string, "null"], description: Queue the call came through, when applicable.}
        hangupCause:
          type: [string, "null"]
          description: Raw platform cause, e.g. `NORMAL_CLEARING` — kept so nothing is lost in the flattening of `status`.
        hangupCode:
          type: [string, "null"]
          description: SIP response code, e.g. `200` or `486`.
        legCallId:
          type: [string, "null"]
          description: Underlying leg id. Diagnostic only — do not deduplicate on it, a call has several.
        accountId: {type: [string, "null"], description: Your platform account id.}
