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
- You expose an HTTPS endpoint that accepts a
phonequery parameter - In the Phone.inc dashboard, you add the URL under Integrations → Custom Endpoint
- On every incoming call, we
GETyour URL with the caller's phone number appended - Your endpoint returns JSON matching the schema below
- 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.
+4571999900becomes%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
| Field | Type | Description |
|---|---|---|
provider | string | A machine-readable slug for your system (lowercase, no spaces) |
label | string | Human-readable name shown in the UI |
source | string | Account or workspace identifier |
Optional fields
| Field | Type | Description |
|---|---|---|
url | string | Link to the matched record in your system |
contact | object | Identity to create or update in Phone.inc's contacts |
sections | array | Array 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"
}
}
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Contact name, up to 128 characters |
email | string | No | Valid email address, truncated to 320 characters before validation. Invalid values are omitted. |
company_name | string | No | Business or employer name, up to 128 characters |
note | string | No | Note 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:
| Key | Type | Required | Description |
|---|---|---|---|
label | string | Yes | The label shown to the left of the value |
value | string, number, or money object | Yes | The value to display |
format | string | No | How to render the value (see format hints) |
url | string | No | Makes 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
| Format | Value type | Rendering |
|---|---|---|
text | String (default if omitted) | Rendered as-is |
money | { "amount": "89", "currency": "DKK" } | Locale-formatted with currency symbol |
date | ISO 8601 string | Formatted date |
email | String | Clickable mailto link |
phone | String (E.164) | Tap-to-call on mobile |
badge | String | Colored 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:
| Limit | Maximum |
|---|---|
| Sections per response | 5 |
| Fields per fields section | 15 |
| Columns per table section | 8 |
| Rows per table section | 10 |
| Value string length | 500 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 response | Our behavior |
|---|---|
| HTTP 200 with valid JSON | Display the data |
| HTTP 200 with empty/invalid JSON | Skip silently (no error shown) |
| HTTP 404 or empty body | Skip silently — caller not found in your system |
| HTTP 429 | Retry on next call, log as rate-limited |
| HTTP 5xx or timeout | Log as transient error, retry on next call |
| Connection refused | Log 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)