> ## Documentation Index
> Fetch the complete documentation index at: https://docs.ringee.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Click-to-call

> Open a secure Ringee dialer for a contact from your CRM

Click-to-call exchanges a contact for a **short-lived, signed dialer URL**. Your agent opens that URL and Ringee handles caller ID selection, credit checks and the WebRTC call.

<Info>
  Click-to-call is the fastest way to add calling to an existing CRM: one server-side request, one redirect, no frontend SDK. If you want the dialer to live **inside** your own UI instead of a Ringee tab, use the [Dialer SDK](/dialer-sdk/overview).
</Info>

<Warning>
  Your frontend only needs `dialerUrl` and `expiresAt`. Never return `sessionToken` to the browser, never persist it, and never write it to logs — it authorizes a call on the agent's behalf.
</Warning>

## Agent resolution

| Workspace    | Behavior                                                                                                                                               |
| ------------ | ------------------------------------------------------------------------------------------------------------------------------------------------------ |
| Personal     | The workspace owner always places the call. `agentEmail` is ignored.                                                                                   |
| Organization | `agentEmail` (or `ownerEmail`) must match a member of that organization, or the request fails with <span className="rg-badge rg-sensitive">422</span>. |

<Warning>
  Take `agentEmail` from your **authenticated session**, not from a value the browser sent. Otherwise any user could place calls as any colleague.
</Warning>

`ownerExternalId` is accepted in the payload but is not yet wired to a resolution strategy — sending it without `agentEmail` returns `422`. Use `agentEmail`.

## Caller ID resolution

When you do not send `fromNumber`, Ringee picks one in this order:

1. the agent's first active **verified caller ID** in that workspace;
2. any **purchased number** on the workspace;
3. otherwise the request fails with `422 No caller ID or purchased number is available for this workspace.`

## Wiring the button

<Steps>
  <Step title="Keep the call server-side">
    The request must be made by your backend (or an Edge Function). The `cik_live_` key must never reach the browser.
  </Step>

  <Step title="Pre-open the tab">
    Open a blank tab **before** the async request so popup blockers do not swallow it, then navigate it once you have the URL.

    ```ts theme={null}
    const tab = window.open("", "_blank");

    try {
      const { dialerUrl } = await fetch("/api/ringee/click-to-call", {
        method: "POST",
        headers: { "Content-Type": "application/json" },
        body: JSON.stringify({ contactExternalId: contact.id }),
      }).then((r) => r.json());

      tab!.location.href = dialerUrl;
    } catch (error) {
      tab?.close();
      showError(error);
    }
    ```
  </Step>

  <Step title="Guard the button">
    Disable it when the contact has no valid phone number, or when the contact is on the Do Not Call list. Ringee blocks DNC numbers server-side too, but failing early is a better experience.
  </Step>

  <Step title="Never auto-dial">
    Do not request a dialer URL on page load. Calls must start from a real click.
  </Step>
</Steps>

## Errors

| Status                                               | Cause                                                                                                         |
| ---------------------------------------------------- | ------------------------------------------------------------------------------------------------------------- |
| <span className="rg-badge rg-sensitive">400</span>   | Neither `contactExternalId` (linked to a contact) nor `phoneNumber` was provided.                             |
| <span className="rg-badge rg-destructive">401</span> | Missing or invalid `X-Ringee-Api-Key`.                                                                        |
| <span className="rg-badge rg-sensitive">422</span>   | `agentEmail` missing on an organization workspace, no member matched the email, or no caller ID is available. |

## Next steps

<CardGroup cols={2}>
  <Card title="Receive the call back" icon="webhook" href="/api/outbound-webhooks">
    Log the call in your CRM when it ends
  </Card>

  <Card title="Embed the dialer instead" icon="phone" href="/dialer-sdk/overview">
    Keep agents inside your own UI
  </Card>
</CardGroup>


## OpenAPI

````yaml POST /api/integrations/custom/click-to-call
openapi: 3.1.0
info:
  title: Ringee Public API
  version: 1.0.0
  description: >-
    The Custom Integrations API. Push contacts and companies from your CRM into
    Ringee, and open a secure dialer for a contact.


    Authenticate every request with the `cik_live_` secret key of a Custom
    Integration. The key must never reach a browser.
  contact:
    name: Ringee
    url: https://ringee.io
servers:
  - url: https://api.ringee.io
    description: Ringee Cloud
  - url: '{backendUrl}'
    description: Self-hosted — your BACKEND_URL
    variables:
      backendUrl:
        default: https://ringee.example.com
        description: The BACKEND_URL of your deployment
security:
  - ApiKeyAuth: []
paths:
  /api/integrations/custom/click-to-call:
    post:
      tags:
        - CRM to Ringee
      summary: Open a dialer for a contact
      description: >-
        Exchange a contact for a short-lived, signed dialer URL. Your agent
        opens that URL and Ringee handles caller ID selection, credit checks and
        the WebRTC call.


        Send at least one of `contactExternalId` (linked to a contact that has a
        phone number) or `phoneNumber`.
      operationId: createClickToCallSession
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: '#/components/schemas/ClickToCallRequest'
            examples:
              byContact:
                summary: By synced CRM contact
                value:
                  contactExternalId: ext_contact_42
                  agentEmail: rep@example.com
              byNumber:
                summary: By raw phone number
                value:
                  phoneNumber: '+14155550123'
                  agentEmail: rep@example.com
                  fromNumber: '+14155550100'
      responses:
        '201':
          description: A dialer session was created.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/ClickToCallResult'
              examples:
                created:
                  summary: Dialer session created
                  value:
                    contactId: 6b2f8f1e-6a2e-4f1c-9a3d-2d1b0c7e4a55
                    agentUserId: a91c4d77-1f0b-4a2e-8c3d-77b1e2f4a9c0
                    toNumber: '+14155550123'
                    fromNumber: '+14155550100'
                    dialerUrl: >-
                      https://app.ringee.io/dashboard/dialer?session=eyJhbGciOi...&to=%2B14155550123&from=%2B14155550100
                    sessionToken: eyJhbGciOi...
                    expiresAt: '2026-05-23T14:35:00.000Z'
        '400':
          description: >-
            Neither `contactExternalId` (linked to a contact) nor `phoneNumber`
            was provided.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '401':
          description: Missing or invalid `X-Ringee-Api-Key`.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
        '422':
          description: >-
            `agentEmail` is missing on an organization workspace, no member
            matched the email, or no caller ID is available.
          content:
            application/json:
              schema:
                $ref: '#/components/schemas/Error'
components:
  schemas:
    ClickToCallRequest:
      type: object
      properties:
        contactExternalId:
          type: string
          description: >-
            The contact's id in your CRM. Resolves to a Ringee contact if you
            previously synced it with `contact.upserted`.
          examples:
            - ext_contact_42
          example: ext_contact_42
        phoneNumber:
          type: string
          description: >-
            Destination number. Required unless `contactExternalId` resolves to
            a contact that already has one.
          examples:
            - '+14155550123'
          example: '+14155550123'
        agentEmail:
          type: string
          description: >-
            Email of the Ringee user who will place the call. **Required for
            organization workspaces.** Take it from your authenticated session,
            never from a value the browser sent.
          examples:
            - rep@example.com
          example: rep@example.com
        fromNumber:
          type: string
          description: >-
            Caller ID to use. Defaults to the agent's first active verified
            caller ID, then any purchased number on the workspace.
          examples:
            - '+14155550100'
          example: '+14155550100'
        ownerEmail:
          type: string
          description: >-
            Alias for `agentEmail`, used when your CRM models this as a record
            owner.
        ownerExternalId:
          type: string
          description: >-
            Accepted but not yet wired to a resolution strategy. Sending it
            without `agentEmail` returns 422 — use `agentEmail`.
    ClickToCallResult:
      type: object
      required:
        - contactId
        - agentUserId
        - toNumber
        - fromNumber
        - dialerUrl
        - sessionToken
        - expiresAt
      properties:
        contactId:
          type:
            - string
            - 'null'
          description: >-
            The Ringee contact, when `contactExternalId` resolved to one. `null`
            otherwise.
          examples:
            - 6b2f8f1e-6a2e-4f1c-9a3d-2d1b0c7e4a55
          example: 6b2f8f1e-6a2e-4f1c-9a3d-2d1b0c7e4a55
        agentUserId:
          type: string
          description: The Ringee user who will place the call.
          examples:
            - a91c4d77-1f0b-4a2e-8c3d-77b1e2f4a9c0
          example: a91c4d77-1f0b-4a2e-8c3d-77b1e2f4a9c0
        toNumber:
          type: string
          description: Normalized destination in E.164.
          examples:
            - '+14155550123'
          example: '+14155550123'
        fromNumber:
          type: string
          description: The caller ID Ringee selected.
          examples:
            - '+14155550100'
          example: '+14155550100'
        dialerUrl:
          type: string
          format: uri
          description: The URL to open. Valid for **5 minutes**.
          examples:
            - >-
              https://app.ringee.io/dashboard/dialer?session=eyJhbGciOi...&to=%2B14155550123&from=%2B14155550100
          example: >-
            https://app.ringee.io/dashboard/dialer?session=eyJhbGciOi...&to=%2B14155550123&from=%2B14155550100
        sessionToken:
          type: string
          description: >-
            The signed session, already embedded in `dialerUrl`. Never return it
            to the browser, persist it, or write it to logs — it authorizes a
            call on the agent's behalf.
        expiresAt:
          type: string
          format: date-time
          description: Expiry of the dialer session.
          examples:
            - '2026-05-23T14:35:00.000Z'
          example: '2026-05-23T14:35:00.000Z'
    Error:
      type: object
      properties:
        statusCode:
          type: integer
          examples:
            - 400
          example: 400
        message:
          type: string
          description: What went wrong.
          examples:
            - >-
              Must provide either contactExternalId (linked to a contact) or
              phoneNumber
          example: >-
            Must provide either contactExternalId (linked to a contact) or
            phoneNumber
        error:
          type: string
          examples:
            - Bad Request
          example: Bad Request
  securitySchemes:
    ApiKeyAuth:
      type: apiKey
      in: header
      name: X-Ringee-Api-Key
      description: >-
        The `cik_live_` secret key of a Custom Integration. Shown once at
        creation. Server-side only.

````