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, and source. Include contact, sections, or both — a contact-only response is valid and is normalized with an empty sections array:

{
  "provider": "internal_crm",
  "label": "Internal CRM",
  "source": "acme-corp",
  "url": "https://crm.acme.com/contacts/456",
  "contact": {
    "name": "Jamie Rivera",
    "email": "jamie@example.com",
    "company_name": "Acme Inc.",
    "note": "VIP customer"
  },
  "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

Optional fields

FieldTypeDescription
urlstringLink to the matched record in your system
contactobjectIdentity to create or update in Phone.inc's contacts
sectionsarrayArray of section objects (see below). Omitted or null values become an empty array.

Cache the matched contact

Include a contact object when the caller matched by your endpoint should be cached in Phone.inc's address book:

{
  "contact": {
    "name": "Jamie Rivera",
    "email": "jamie@example.com",
    "company_name": "Acme Inc.",
    "note": "VIP customer"
  }
}
FieldTypeRequiredDescription
namestringYesContact name, up to 128 characters
emailstringNoValid email address, truncated to 320 characters before validation. Invalid values are omitted.
company_namestringNoBusiness or employer name, up to 128 characters
notestringNoNote copied to the contact, up to 10,000 characters

The phone query parameter from our request becomes the contact's phone number. Do not return a separate phone inside contact.

When the phone number has no Phone.inc contact, we create one. When a contact already exists, we fill only blank fields. Data from your endpoint never overwrites a name, email, company name, or note saved by a user.

The contact object controls address-book caching. The sections array controls what employees see in caller context. You can return either or both in the same response. A response without contact continues to work exactly as before.

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": "niklas@example.com", "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}`,
    contact: {
      name: customer.name,
      email: customer.email,
      company_name: customer.companyName,
      note: customer.note,
    },
    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)