Custom Caller Context

Connect any internal system — a CRM, helpdesk, ERP, or custom database — to Phone.inc's caller context by hosting a single JSON endpoint. When a call comes in, we call your URL with the phone number and display the response to your team.

How it works

  1. You expose an HTTPS endpoint that accepts a phone query parameter
  2. In the Phone.inc dashboard, you add the URL under Integrations → Custom Endpoint
  3. On every incoming call, we GET your URL with the caller's phone number appended
  4. Your endpoint returns JSON matching the schema below
  5. Your team sees the data in the sidebar before they pick up

Setting it up

Go to Integrations in your Phone.inc dashboard, find "Custom Endpoint" in the available integrations, and click Connect. Enter your endpoint URL and save.

If your endpoint requires authentication, bake credentials directly into the URL:

https://your-api.com/caller-lookup?token=abc123

We'll append &phone=+4571999900 to that URL on each call.

Endpoint contract

Request

We make a GET request to your URL with the caller's phone number appended as a query parameter:

GET https://your-api.com/caller-lookup?phone=%2B4571999900
  • Phone numbers are URL-encoded E.164 format (e.g. +4571999900 becomes %2B4571999900)
  • If your URL already has query parameters, we append &phone=...
  • We set a timeout of 2.5 seconds
  • We send Accept: application/json

Response

Return a JSON object with provider, label, source, and sections:

{
  "provider": "internal_crm",
  "label": "Internal CRM",
  "source": "acme-corp",
  "url": "https://crm.acme.com/contacts/456",
  "sections": [
    {
      "type": "fields",
      "fields": [
        { "label": "Account Manager", "value": "Sarah Jensen" },
        { "label": "Contract Value", "value": { "amount": "24000", "currency": "DKK" }, "format": "money" },
        { "label": "Renewal", "value": "2026-09-01T00:00:00Z", "format": "date" }
      ]
    },
    {
      "type": "table",
      "label": "Open Tickets",
      "columns": [
        { "key": "id", "label": "Ticket" },
        { "key": "subject", "label": "Subject" },
        { "key": "status", "label": "Status", "format": "badge" }
      ],
      "rows": [
        {
          "url": "https://crm.acme.com/tickets/78",
          "values": { "id": "#78", "subject": "Invoice question", "status": "Open" }
        },
        {
          "url": "https://crm.acme.com/tickets/65",
          "values": { "id": "#65", "subject": "Delivery delay", "status": "Resolved" }
        }
      ]
    }
  ]
}

Required fields

FieldTypeDescription
providerstringA machine-readable slug for your system (lowercase, no spaces)
labelstringHuman-readable name shown in the UI
sourcestringAccount or workspace identifier
sectionsarrayArray of section objects (see below)

Optional fields

FieldTypeDescription
urlstringLink to the matched record in your system

Section types

Fields section

A vertical list of label/value pairs.

{
  "type": "fields",
  "label": "Customer Info",
  "fields": [
    { "label": "Name", "value": "Niklas Stephenson" },
    { "label": "Email", "value": "[email protected]", "format": "email" },
    { "label": "Phone", "value": "+4571999900", "format": "phone" }
  ]
}

Each field has:

KeyTypeRequiredDescription
labelstringYesThe label shown to the left of the value
valuestring, number, or money objectYesThe value to display
formatstringNoHow to render the value (see format hints)
urlstringNoMakes the value a clickable link

Table section

A compact table with column headers and rows. Columns are rendered in a two-line layout per row: the first half of columns stacks on the left, the second half on the right. For example, with four columns [Order, Date, Total, Status], each row renders as:

#1001            $89.00
Apr 28           Paid

Order your columns so the primary identifier is first and the most important secondary value is in the right-hand half (at position ceil(columns.length / 2)).

{
  "type": "table",
  "label": "Recent Orders",
  "columns": [
    { "key": "name", "label": "Order" },
    { "key": "date", "label": "Date", "format": "date" },
    { "key": "total", "label": "Total", "format": "money" }
  ],
  "rows": [
    {
      "url": "https://your-system.com/orders/1001",
      "values": {
        "name": "#1001",
        "date": "2026-04-28T12:00:00Z",
        "total": { "amount": "89", "currency": "DKK" }
      }
    }
  ]
}

Format hints

FormatValue typeRendering
textString (default if omitted)Rendered as-is
money{ "amount": "89", "currency": "DKK" }Locale-formatted with currency symbol
dateISO 8601 stringFormatted date
emailStringClickable mailto link
phoneString (E.164)Tap-to-call on mobile
badgeStringColored status pill

To make any value clickable, use the field-level url property instead of a format hint. The url property works with every format — for example, a badge value with a url renders as a pill that links to the given URL.

Limits

We enforce these limits to keep the UI fast and readable:

LimitMaximum
Sections per response5
Fields per fields section15
Columns per table section8
Rows per table section10
Value string length500 characters

Data beyond these limits is silently truncated. Unknown keys in the JSON are silently ignored — we will never reject a payload for having extra fields, so you can safely add internal metadata without breaking the integration.

Error handling

Your responseOur behavior
HTTP 200 with valid JSONDisplay the data
HTTP 200 with empty/invalid JSONSkip silently (no error shown)
HTTP 404 or empty bodySkip silently — caller not found in your system
HTTP 429Retry on next call, log as rate-limited
HTTP 5xx or timeoutLog as transient error, retry on next call
Connection refusedLog as transient error

We never mark the integration as permanently broken for transient errors. Your team won't see error states for occasional blips.

Example implementation

A minimal Node.js/Express endpoint:

const express = require('express')
const app = express()

app.get('/caller-lookup', (req, res) => {
  const phone = req.query.phone
  if (!phone) return res.status(400).json({ error: 'phone required' })

  // Look up the caller in your system
  const customer = db.findCustomerByPhone(phone)
  if (!customer) return res.status(404).json({})

  res.json({
    provider: 'my_crm',
    label: 'My CRM',
    source: 'production',
    url: `https://crm.example.com/customers/${customer.id}`,
    sections: [
      {
        type: 'fields',
        fields: [
          { label: 'Name', value: customer.name },
          { label: 'Email', value: customer.email, format: 'email' },
          { label: 'Account type', value: customer.tier, format: 'badge' },
        ],
      },
    ],
  })
})

app.listen(3000)