Base URL: https://app.linehq.co.uk/api/v1. Send an API key from the LineHQ portal in the Authorization: Bearer header of every request. Responses are JSON, and records are addressed by their public_id.
Download the whole reference as openapi.json (OpenAPI 3.1), or read it as Markdown. Back to the developer overview.
Contents
- Account
- Numbers
- Calls
- Text messages
- Contacts
- To-dos
- Notifications
- Message slips
- Job requests
- Blocked callers
- Analytics
- Billing
- Webhooks
Account
Who the key belongs to and the account's prepaid balance.
The key's account, user and permissions
GET
/me
Who this API key belongs to, the permissions it holds, and the private channel to subscribe to for live events. Call it first to check a key works.
Example request
curl
curl "https://app.linehq.co.uk/api/v1/me" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/me", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/me",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The key's identity.
{
"tenant": {
"name": "Acme Plumbing Ltd",
"status": "active"
},
"user": {
"name": "Sam Patel",
"email": "sam@acmeplumbing.co.uk",
"role": "owner"
},
"abilities": [
"read",
"contacts:read"
],
"can_transcribe": false,
"realtime": {
"enabled": true,
"channel": "private-tenant.4821",
"key": "linehq",
"host": "app.linehq.co.uk",
"port": 443,
"scheme": "https"
}
}
The prepaid balance
GET
/balance
The account's prepaid wallet balance, in pence.
Example request
curl
curl "https://app.linehq.co.uk/api/v1/balance" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/balance", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/balance",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The balance.
{
"balance_pence": 2500,
"currency": "GBP"
}
Numbers
The account's phone numbers and how each one answers calls.
List numbers
GET
/numbers
Every number on the account that hasn't been released, ordered by number.
Example request
curl
curl "https://app.linehq.co.uk/api/v1/numbers" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/numbers", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/numbers",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The account's numbers.
{
"data": [
{
"public_id": "8kQ2mVn4Lr7Tz1Xc9Bw3Hd6Fs0Jy5Pa2",
"e164": "+442035550123",
"label": "Sales",
"status": "assigned",
"sms_capable": true,
"sms_ready": true,
"cli_eligible": true,
"is_default_caller_id": true
}
]
}
Get a number
GET
/numbers/{number}
One number, with its business, caller ID and text-message status.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required |
path | string | The number's public_id, from List numbers.
|
Example request
curl
curl "https://app.linehq.co.uk/api/v1/numbers/NUMBER_ID" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/numbers/NUMBER_ID", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/numbers/NUMBER_ID",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The number.
{
"public_id": "8kQ2mVn4Lr7Tz1Xc9Bw3Hd6Fs0Jy5Pa2",
"e164": "+442035550123",
"label": "Sales",
"status": "assigned",
"ownership": "platform",
"business_profile": {
"public_id": "Vb7Nq1Zr4Kx8Lm2Tc6Wd0Hs3Jf9Pg5Ya",
"name": "Acme Plumbing"
},
"release_at": null,
"sms_capable": true,
"sms_ready": true,
"cli_eligible": true,
"cli_verified": false,
"is_default_caller_id": true,
"forward_cli": {
"mode": "auto",
"shows": "own",
"own_e164": "+442035550123",
"platform_e164": null,
"own_available": true
}
}
Get a number's routing
GET
/numbers/{number}/routing
How a number answers calls: the default route, any scheduled routes (the first matching window wins), and a temporary override while one is set. Read-only; routing is changed in the portal or the LineHQ app.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
number required |
path | string | The number's public_id, from List numbers.
|
Example request
curl
curl "https://app.linehq.co.uk/api/v1/numbers/NUMBER_ID/routing" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/numbers/NUMBER_ID/routing", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/numbers/NUMBER_ID/routing",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The number's routing.
{
"number": {
"public_id": "8kQ2mVn4Lr7Tz1Xc9Bw3Hd6Fs0Jy5Pa2",
"e164": "+442035550123",
"status": "assigned",
"voicemail_transcription_enabled": true,
"call_transcription_enabled": false,
"privacy_mode_enabled": false
},
"default": {
"mode": "forward",
"recording_enabled": true,
"voicemail_fallback": true,
"whisper": false,
"ai_cover": "off",
"forward_targets": [
{
"kind": "pstn",
"e164": "+447700900001",
"staff_label": "Sam",
"ring_seconds": 25
}
],
"ivr": {
"options": []
}
},
"schedules": [
{
"name": "Weekends",
"windows": [
{
"days": [
6,
7
],
"start": "09:00",
"end": "17:30"
}
],
"mode": "voicemail",
"recording_enabled": false,
"voicemail_fallback": false,
"whisper": false,
"ai_cover": "off",
"forward_targets": [],
"ivr": {
"options": []
}
}
],
"override": null
}
Calls
Calls, their recordings, transcripts and AI summaries.
Needs the Integrations add-on (included on Scale; available on Mobile and Reception)
List calls
GET
/calls
The account's calls, newest first, each with its AI summary once one exists. Calls our call protection filtered out are left out unless you ask for them with activity.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
activity |
query | one of "business", "filtered", "all"
|
business (the default) for real calls, filtered for calls call protection screened out, or all.
|
search |
query | string | Part of the caller's or the dialled number, as international digits, or of the caller's name. |
period |
query | one of "all", "today", "7d", "30d", "month", "custom"
|
today, 7d, 30d, month (this calendar month), or custom with date_from and date_to.
|
date_from |
query | string | With period=custom, the first day to include (YYYY-MM-DD).
|
date_to |
query | string | With period=custom, the last day to include (YYYY-MM-DD).
|
status |
query | one of "answered", "no-answer", "voicemail", "failed", "in-progress", "busy", "blocked"
|
Only calls with this outcome. |
sentiment |
query | one of "positive", "neutral", "negative"
|
Only calls whose summary has this sentiment. |
urgency |
query | one of "low", "normal", "high"
|
Only calls whose summary has this urgency. |
page |
query | integer | The page to return, from 1. |
per_page |
query | integer | Records per page, up to 100. Defaults to 25. |
Example request
curl
curl "https://app.linehq.co.uk/api/v1/calls" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/calls", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/calls",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 A page of calls.
{
"data": [
{
"public_id": "Qm4Tz8Kc1Lp6Wr3Nx9Bv2Hd7Fs5Jy0Ga",
"from": "+447700900123",
"caller_display_name": "Jo Smith",
"to": "+442035550123",
"to_name": "Sales",
"direction": "inbound",
"disposition": "answered",
"started_at": "2026-10-14T09:30:12+01:00",
"forward_seconds": 184,
"total_rated_amount_pence": 0,
"recording_public_id": "Rk3Lm8Nq2Tz6Vb1Xc5Wd9Hs4Jf0Pg7Ya",
"transcript": {
"status": "completed",
"summary_status": "completed",
"summary": "Jo from Harbour Cafe needs a boiler repair today.",
"intent": "Book a boiler repair",
"sentiment": "neutral",
"urgency": "high",
"action_items": [
"Call Jo back to book a visit today"
],
"callback_number": "+447700900123"
}
}
],
"current_page": 1,
"last_page": 1,
"per_page": 25,
"total": 1,
"next_page_url": null,
"prev_page_url": null
}
Get a call
GET
/calls/{call}
One call, in the same shape as a row of List calls.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
call required |
path | string | The call's public_id, from List calls.
|
Example request
curl
curl "https://app.linehq.co.uk/api/v1/calls/CALL_ID" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/calls/CALL_ID", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/calls/CALL_ID",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The call.
{
"public_id": "Qm4Tz8Kc1Lp6Wr3Nx9Bv2Hd7Fs5Jy0Ga",
"from": "+447700900123",
"caller_display_name": "Jo Smith",
"to": "+442035550123",
"to_name": "Sales",
"direction": "inbound",
"disposition": "answered",
"started_at": "2026-10-14T09:30:12+01:00",
"forward_seconds": 184,
"total_rated_amount_pence": 0,
"recording_public_id": "Rk3Lm8Nq2Tz6Vb1Xc5Wd9Hs4Jf0Pg7Ya",
"transcript": {
"status": "completed",
"summary_status": "completed",
"summary": "Jo from Harbour Cafe needs a boiler repair today.",
"intent": "Book a boiler repair",
"sentiment": "neutral",
"urgency": "high",
"action_items": [
"Call Jo back to book a visit today"
],
"callback_number": "+447700900123"
}
}
Transcribe a call
POST
/calls/{call}/transcribe
Start a transcription and AI summary of a recorded call. It is paid: minutes come from the plan's allowance, then any prepaid transcription bundle, then the prepaid balance, and they count towards the account's daily spending limit. Needs a key with the transcribe permission, which only an owner, or a member the owner allows to start transcriptions, can create. It returns straight away; read the summary from Get a call once summary_status is completed. Asking again for a call already transcribed costs nothing.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
call required |
path | string | The call's public_id, from List calls.
|
Example request
curl
curl -X POST "https://app.linehq.co.uk/api/v1/calls/CALL_ID/transcribe" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/calls/CALL_ID/transcribe", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.post(
"https://app.linehq.co.uk/api/v1/calls/CALL_ID/transcribe",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
202 Queued, or already done.
{
"call_public_id": "Qm4Tz8Kc1Lp6Wr3Nx9Bv2Hd7Fs5Jy0Ga",
"status": "pending",
"summary_status": "pending"
}
Get a recording link
GET
/recordings/{recording}/stream-url
A signed link to a call's recording audio, valid for 10 minutes. Ask for a new one each time you play or download it.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
recording required |
path | string | The recording's public_id, a call's recording_public_id.
|
Example request
curl
curl "https://app.linehq.co.uk/api/v1/recordings/RECORDING_ID/stream-url" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/recordings/RECORDING_ID/stream-url", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/recordings/RECORDING_ID/stream-url",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The link.
{
"url": "https://app.linehq.co.uk/media/recordings/Rk3Lm8Nq2Tz6Vb1Xc5Wd9Hs4Jf0Pg7Ya?expires=1760450400&signature=0f3c9b",
"expires_at": "2026-10-14T14:00:00+01:00"
}
Get a transcript
GET
/recordings/{recording}/transcript
The full transcript of a recording, with timed, speaker-labelled segments and the AI summary. An empty object means the recording hasn't been transcribed.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
recording required |
path | string | The recording's public_id, a call's recording_public_id.
|
Example request
curl
curl "https://app.linehq.co.uk/api/v1/recordings/RECORDING_ID/transcript" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/recordings/RECORDING_ID/transcript", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/recordings/RECORDING_ID/transcript",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The transcript, or `{}`.
{
"status": "completed",
"text": "Hi, it's Jo from Harbour Cafe. Our boiler's stopped working, could someone come out today?",
"language": "en",
"minutes": 1,
"rated_amount_pence": 0,
"summary_status": "completed",
"summary": "Jo from Harbour Cafe needs a boiler repair today.",
"segments": [
{
"start": 0,
"end": 6.4,
"speaker": "customer",
"text": "Hi, it's Jo from Harbour Cafe. Our boiler's stopped working, could someone come out today?"
}
],
"intent": "Book a boiler repair",
"sentiment": "neutral",
"urgency": "high",
"action_items": [
"Call Jo back to book a visit today"
],
"callback_number": "+447700900123"
}
Price a transcription
GET
/recordings/{recording}/transcript/estimate
What transcribing this recording would cost, and whether the account can afford it now.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
recording required |
path | string | The recording's public_id, a call's recording_public_id.
|
Example request
curl
curl "https://app.linehq.co.uk/api/v1/recordings/RECORDING_ID/transcript/estimate" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/recordings/RECORDING_ID/transcript/estimate", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/recordings/RECORDING_ID/transcript/estimate",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The price.
{
"available": true,
"minutes": 4,
"cost_pence": 0,
"rate_pence_per_min": 4,
"within_allowance": true,
"bundle_minutes": 0,
"balance_pence": 2500,
"affordable": true,
"existing_status": null
}
Text messages
Text-message conversations on the account's numbers.
Needs the Integrations add-on (included on Scale; available on Mobile and Reception)
List text conversations
GET
/sms/conversations
The account's text-message threads, most recent first. One thread per number and other party.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page |
query | integer | The page to return, from 1. |
per_page |
query | integer | Records per page, up to 100. Defaults to 25. |
Example request
curl
curl "https://app.linehq.co.uk/api/v1/sms/conversations" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/sms/conversations", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/sms/conversations",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 A page of conversations.
{
"data": [
{
"public_id": "Tc5Wd9Hs4Jf0Pg7YaRk3Lm8Nq2Tz6Vb1X",
"counterparty_e164": "+447700900123",
"number_e164": "+442035550123",
"number_label": "Sales",
"last_message_at": "2026-10-14T09:12:00+01:00",
"last_message_preview": "Running 10 minutes late, sorry!",
"unread_count": 1
}
],
"current_page": 1,
"last_page": 1,
"per_page": 25,
"total": 1,
"next_page_url": null,
"prev_page_url": null
}
List a conversation's messages
GET
/sms/conversations/{conversation}/messages
The messages in one thread, oldest first, with the thread itself.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
conversation required |
path | string | The conversation's public_id, from List text conversations.
|
page |
query | integer | The page to return, from 1. |
per_page |
query | integer | Messages per page, up to 200. Defaults to 100. |
Example request
curl
curl "https://app.linehq.co.uk/api/v1/sms/conversations/CONVERSATION_ID/messages" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/sms/conversations/CONVERSATION_ID/messages", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/sms/conversations/CONVERSATION_ID/messages",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 A page of messages.
{
"data": [
{
"conversation_public_id": "Tc5Wd9Hs4Jf0Pg7YaRk3Lm8Nq2Tz6Vb1X",
"direction": "inbound",
"from_e164": "+447700900123",
"to_e164": "+442035550123",
"body": "Running 10 minutes late, sorry!",
"status": "received",
"segments": 1,
"received_at": "2026-10-14T09:12:00+01:00"
}
],
"conversation": {
"public_id": "Tc5Wd9Hs4Jf0Pg7YaRk3Lm8Nq2Tz6Vb1X",
"counterparty_e164": "+447700900123",
"number_e164": "+442035550123",
"number_label": "Sales",
"last_message_at": "2026-10-14T09:12:00+01:00",
"last_message_preview": "Running 10 minutes late, sorry!",
"unread_count": 1
},
"current_page": 1,
"last_page": 1,
"per_page": 100,
"total": 1,
"next_page_url": null,
"prev_page_url": null
}
Contacts
The contact directory LineHQ names callers from. Keep it in step with your own system.
Needs the Integrations add-on (included on Scale; available on Mobile and Reception)
List contacts
GET
/contacts
The account's contact directory, most recently changed first. LineHQ uses it to name callers. Needs a key with the contacts:read permission.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
search |
query | string | Part of the display name, company or email address, or of a phone number as international digits (+44…, not 07…).
|
source |
query | string | Only contacts from this source, such as the one your system writes them with. |
page |
query | integer | The page to return, from 1. |
per_page |
query | integer | Records per page, up to 100. Defaults to 25. |
Example request
curl
curl "https://app.linehq.co.uk/api/v1/contacts" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/contacts", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/contacts",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 A page of contacts.
{
"data": [
{
"public_id": "Lm8Nq2Tz6Vb1Xc5Wd9Hs4Jf0Pg7YaRk3",
"display_name": "Jo Smith",
"first_name": "Jo",
"last_name": "Smith",
"company": "Harbour Cafe",
"email": "jo@harbourcafe.co.uk",
"notes": null,
"source": "acme-crm",
"external_id": "10442",
"source_created_at": null,
"source_updated_at": null,
"phones": [
{
"e164": "+447700900123",
"raw_number": "07700 900123",
"label": "mobile",
"is_primary": true
}
],
"links": [],
"created_at": "2026-10-01T10:00:00+01:00",
"updated_at": "2026-10-14T08:45:00+01:00"
}
],
"current_page": 1,
"last_page": 1,
"per_page": 25,
"total": 1,
"next_page_url": null,
"prev_page_url": null
}
Create or update a contact
POST
/contacts
Add a contact. Send external_id (your system's id for it) and the same record can be sent again safely: a contact with the same source and external_id is updated rather than added twice, and the answer is still 201. Without external_id every request adds a new contact. Phone numbers that can't be read as a phone number are dropped. Needs a key with the contacts:write permission.
Request body
| Field | Type | Description |
|---|---|---|
source |
string | Your system's name. With external_id, it identifies the contact so a repeat send updates it.
|
external_id |
string or null | |
display_name required |
string | |
first_name |
string or null | |
last_name |
string or null | |
company |
string or null | |
email |
string or null | |
notes |
string or null | |
external_url |
string or null | A link to the contact in your system. |
source_created_at |
string or null | |
source_updated_at |
string or null | |
phones required |
array of object |
Example request
curl
curl -X POST "https://app.linehq.co.uk/api/v1/contacts" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"source": "acme-crm",
"external_id": "10442",
"display_name": "Jo Smith",
"company": "Harbour Cafe",
"email": "jo@harbourcafe.co.uk",
"phones": [
{
"number": "07700 900123",
"label": "mobile",
"is_primary": true
}
]
}'JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/contacts", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"source": "acme-crm",
"external_id": "10442",
"display_name": "Jo Smith",
"company": "Harbour Cafe",
"email": "jo@harbourcafe.co.uk",
"phones": [
{
"number": "07700 900123",
"label": "mobile",
"is_primary": true
}
]
}),
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.post(
"https://app.linehq.co.uk/api/v1/contacts",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
json={
"source": "acme-crm",
"external_id": "10442",
"display_name": "Jo Smith",
"company": "Harbour Cafe",
"email": "jo@harbourcafe.co.uk",
"phones": [
{
"number": "07700 900123",
"label": "mobile",
"is_primary": True,
},
],
},
)
response.raise_for_status()
print(response.json())Example response
201 The contact.
{
"public_id": "Lm8Nq2Tz6Vb1Xc5Wd9Hs4Jf0Pg7YaRk3",
"display_name": "Jo Smith",
"first_name": "Jo",
"last_name": "Smith",
"company": "Harbour Cafe",
"email": "jo@harbourcafe.co.uk",
"notes": null,
"source": "acme-crm",
"external_id": "10442",
"source_created_at": null,
"source_updated_at": null,
"phones": [
{
"e164": "+447700900123",
"raw_number": "07700 900123",
"label": "mobile",
"is_primary": true
}
],
"links": [],
"created_at": "2026-10-01T10:00:00+01:00",
"updated_at": "2026-10-14T08:45:00+01:00"
}
Create or update many contacts
POST
/contacts/bulk-upsert
Up to 500 contacts at once, each matched on source and external_id like Create or update a contact. The batch is checked first, and one invalid contact rejects it all with 422; a contact that then fails to save is counted in failed_count and the rest are kept. Needs a key with the contacts:write permission.
Request body
| Field | Type | Description |
|---|---|---|
source required |
string | Your system's name. Used for every contact that doesn't give its own. |
contacts required |
array of object |
Example request
curl
curl -X POST "https://app.linehq.co.uk/api/v1/contacts/bulk-upsert" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"source": "acme-crm",
"contacts": [
{
"external_id": "10442",
"display_name": "Jo Smith",
"phones": [
{
"number": "+447700900123"
}
]
},
{
"external_id": "10443",
"display_name": "Priya Shah",
"company": "Shah Dental",
"phones": [
{
"number": "020 7946 0991",
"label": "office"
}
]
}
]
}'JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/contacts/bulk-upsert", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"source": "acme-crm",
"contacts": [
{
"external_id": "10442",
"display_name": "Jo Smith",
"phones": [
{
"number": "+447700900123"
}
]
},
{
"external_id": "10443",
"display_name": "Priya Shah",
"company": "Shah Dental",
"phones": [
{
"number": "020 7946 0991",
"label": "office"
}
]
}
]
}),
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.post(
"https://app.linehq.co.uk/api/v1/contacts/bulk-upsert",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
json={
"source": "acme-crm",
"contacts": [
{
"external_id": "10442",
"display_name": "Jo Smith",
"phones": [
{
"number": "+447700900123",
},
],
},
{
"external_id": "10443",
"display_name": "Priya Shah",
"company": "Shah Dental",
"phones": [
{
"number": "020 7946 0991",
"label": "office",
},
],
},
],
},
)
response.raise_for_status()
print(response.json())Example response
202 How many were saved.
{
"imported_count": 2,
"failed_count": 0
}
Replace a contact
PUT
/contacts/{contact}
Replace a contact's details and phone numbers with the ones sent. Its source and external_id stay as they were. Needs a key with the contacts:write permission.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
contact required |
path | string | The contact's public_id.
|
Request body
| Field | Type | Description |
|---|---|---|
source |
string | Your system's name. With external_id, it identifies the contact so a repeat send updates it.
|
external_id |
string or null | |
display_name required |
string | |
first_name |
string or null | |
last_name |
string or null | |
company |
string or null | |
email |
string or null | |
notes |
string or null | |
external_url |
string or null | A link to the contact in your system. |
source_created_at |
string or null | |
source_updated_at |
string or null | |
phones required |
array of object |
Example request
curl
curl -X PUT "https://app.linehq.co.uk/api/v1/contacts/CONTACT_ID" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"display_name": "Jo Smith",
"company": "Harbour Cafe & Bakery",
"phones": [
{
"number": "+447700900123",
"label": "mobile",
"is_primary": true
}
]
}'JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/contacts/CONTACT_ID", {
method: "PUT",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"display_name": "Jo Smith",
"company": "Harbour Cafe & Bakery",
"phones": [
{
"number": "+447700900123",
"label": "mobile",
"is_primary": true
}
]
}),
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.put(
"https://app.linehq.co.uk/api/v1/contacts/CONTACT_ID",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
json={
"display_name": "Jo Smith",
"company": "Harbour Cafe & Bakery",
"phones": [
{
"number": "+447700900123",
"label": "mobile",
"is_primary": True,
},
],
},
)
response.raise_for_status()
print(response.json())Example response
200 The contact.
{
"public_id": "Lm8Nq2Tz6Vb1Xc5Wd9Hs4Jf0Pg7YaRk3",
"display_name": "Jo Smith",
"first_name": "Jo",
"last_name": "Smith",
"company": "Harbour Cafe",
"email": "jo@harbourcafe.co.uk",
"notes": null,
"source": "acme-crm",
"external_id": "10442",
"source_created_at": null,
"source_updated_at": null,
"phones": [
{
"e164": "+447700900123",
"raw_number": "07700 900123",
"label": "mobile",
"is_primary": true
}
],
"links": [],
"created_at": "2026-10-01T10:00:00+01:00",
"updated_at": "2026-10-14T08:45:00+01:00"
}
Delete a contact
DELETE
/contacts/{contact}
Remove a contact from the directory. Needs a key with the contacts:write permission.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
contact required |
path | string | The contact's public_id.
|
Example request
curl
curl -X DELETE "https://app.linehq.co.uk/api/v1/contacts/CONTACT_ID" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/contacts/CONTACT_ID", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.delete(
"https://app.linehq.co.uk/api/v1/contacts/CONTACT_ID",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 Deleted.
{
"deleted": true
}
To-dos
Follow-ups the AI summary finds in calls.
Needs the Integrations add-on (included on Scale; available on Mobile and Reception)
List to-dos
GET
/tasks
Follow-ups the AI summary found in the account's calls, newest first. Open ones by default.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status |
query | one of "open", "done", "all"
|
open (the default), done or all.
|
page |
query | integer | The page to return, from 1. |
per_page |
query | integer | Records per page, up to 100. Defaults to 25. |
Example request
curl
curl "https://app.linehq.co.uk/api/v1/tasks" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/tasks", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/tasks",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 A page of to-dos.
{
"data": [
{
"public_id": "Wd9Hs4Jf0Pg7YaRk3Lm8Nq2Tz6Vb1Xc5",
"text": "Call Jo back to book a visit today",
"status": "open",
"callback_e164": "+447700900123",
"sentiment": "neutral",
"urgency": "high",
"summary_snippet": "Jo from Harbour Cafe needs a boiler repair today.",
"created_at": "2026-10-14T09:34:02+01:00",
"updated_at": "2026-10-14T09:34:02+01:00",
"completed_at": null,
"done_by": null,
"call": {
"public_id": "Qm4Tz8Kc1Lp6Wr3Nx9Bv2Hd7Fs5Jy0Ga",
"from_e164": "+447700900123",
"to_e164": "+442035550123",
"number_label": "Sales",
"started_at": "2026-10-14T09:30:12+01:00"
}
}
],
"current_page": 1,
"last_page": 1,
"per_page": 25,
"total": 1,
"next_page_url": null,
"prev_page_url": null
}
Tick off or reopen a to-do
PATCH
/tasks/{task}
Mark a to-do done, recording the key's user as the one who did it, or open it again. Needs a key with the inbox:write permission.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
task required |
path | string | The to-do's public_id.
|
Request body
| Field | Type | Description |
|---|---|---|
status required |
one of "open", "done"
|
Example request
curl
curl -X PATCH "https://app.linehq.co.uk/api/v1/tasks/TASK_ID" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"status": "done"
}'JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/tasks/TASK_ID", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"status": "done"
}),
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.patch(
"https://app.linehq.co.uk/api/v1/tasks/TASK_ID",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
json={
"status": "done",
},
)
response.raise_for_status()
print(response.json())Example response
200 The to-do.
{
"public_id": "Wd9Hs4Jf0Pg7YaRk3Lm8Nq2Tz6Vb1Xc5",
"text": "Call Jo back to book a visit today",
"status": "open",
"callback_e164": "+447700900123",
"sentiment": "neutral",
"urgency": "high",
"summary_snippet": "Jo from Harbour Cafe needs a boiler repair today.",
"created_at": "2026-10-14T09:34:02+01:00",
"updated_at": "2026-10-14T09:34:02+01:00",
"completed_at": null,
"done_by": null,
"call": {
"public_id": "Qm4Tz8Kc1Lp6Wr3Nx9Bv2Hd7Fs5Jy0Ga",
"from_e164": "+447700900123",
"to_e164": "+442035550123",
"number_label": "Sales",
"started_at": "2026-10-14T09:30:12+01:00"
}
}
Notifications
The notifications behind each user's bell in the portal.
Needs the Integrations add-on (included on Scale; available on Mobile and Reception)
List notifications
GET
/notifications
The key's user's notifications, the same ones as the bell in the portal, newest first, with how many are unread.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
unread |
query | one of 0, 1
|
1 for unread notifications only.
|
page |
query | integer | The page to return, from 1. |
per_page |
query | integer | Records per page, up to 100. Defaults to 25. |
Example request
curl
curl "https://app.linehq.co.uk/api/v1/notifications" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/notifications", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/notifications",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 A page of notifications.
{
"data": [
{
"id": "9b2f6c1e-3d4a-4f8b-9c7d-2e1a5b6c7d8e",
"type": "missed_call",
"title": "Missed call from +447700900123",
"body": "To Sales, 09:30",
"url": "https://app.linehq.co.uk/dashboard",
"read": false,
"created_at": "2026-10-14T09:31:00+01:00",
"read_at": null
}
],
"unread_count": 1,
"current_page": 1,
"last_page": 1,
"per_page": 25,
"total": 1,
"next_page_url": null,
"prev_page_url": null
}
Mark a notification read
POST
/notifications/{notification}/read
Needs a key with the inbox:write permission.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
notification required |
path | string | The notification's id.
|
Example request
curl
curl -X POST "https://app.linehq.co.uk/api/v1/notifications/NOTIFICATION_ID/read" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/notifications/NOTIFICATION_ID/read", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.post(
"https://app.linehq.co.uk/api/v1/notifications/NOTIFICATION_ID/read",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 Marked read.
{
"id": "9b2f6c1e-3d4a-4f8b-9c7d-2e1a5b6c7d8e",
"read": true,
"unread_count": 0
}
Mark every notification read
POST
/notifications/read-all
Needs a key with the inbox:write permission.
Example request
curl
curl -X POST "https://app.linehq.co.uk/api/v1/notifications/read-all" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/notifications/read-all", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.post(
"https://app.linehq.co.uk/api/v1/notifications/read-all",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 All read.
{
"unread_count": 0
}
Message slips
Messages callers leave with the virtual receptionist.
Needs the Integrations add-on (included on Scale; available on Mobile and Reception)
List message slips
GET
/reception-messages
Messages callers left with the virtual receptionist, newest first. The recordings play through Get a recording link.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
search |
query | string | Part of the caller's number, as international digits. |
period |
query | one of "all", "today", "7d", "30d", "month", "custom"
|
all (the default), today, 7d, 30d, month (this calendar month), or custom with date_from and date_to.
|
date_from |
query | string | |
date_to |
query | string | |
status |
query | one of "unheard", "heard"
|
unheard or heard.
|
sentiment |
query | one of "positive", "neutral", "negative"
|
|
urgency |
query | one of "low", "normal", "high"
|
|
page |
query | integer | The page to return, from 1. |
per_page |
query | integer | Message slips per page, up to 100. Defaults to 20. |
Example request
curl
curl "https://app.linehq.co.uk/api/v1/reception-messages" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/reception-messages", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/reception-messages",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 A page of message slips.
{
"data": [
{
"public_id": "Hs4Jf0Pg7YaRk3Lm8Nq2Tz6Vb1Xc5Wd9",
"from_e164": "+447700900123",
"department": "Sales",
"received_at": "2026-10-14T18:42:00+01:00",
"heard_at": null,
"name_recording_public_id": "Nq2Tz6Vb1Xc5Wd9Hs4Jf0Pg7YaRk3Lm8",
"message_recording_public_id": "Pg7YaRk3Lm8Nq2Tz6Vb1Xc5Wd9Hs4Jf0",
"transcript_status": "completed",
"caller_name": "Jo Smith",
"summary_status": "completed",
"sentiment": "neutral",
"urgency": "normal"
}
],
"current_page": 1,
"last_page": 1,
"per_page": 20,
"total": 1,
"next_page_url": null,
"prev_page_url": null
}
Job requests
Jobs picked out of message slips and calls.
Needs the Integrations add-on (included on Scale; available on Mobile and Reception)
List job requests
GET
/job-requests
Jobs the AI picked out of message slips and calls (who, what, where and when), newest first. Open ones by default.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
status |
query | one of "open", "new", "confirmed", "done", "declined", "all"
|
open (the default: new and confirmed), one status (new, confirmed, done, declined), or all.
|
page |
query | integer | The page to return, from 1. |
per_page |
query | integer | Job requests per page, up to 100. Defaults to 20. |
Example request
curl
curl "https://app.linehq.co.uk/api/v1/job-requests" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/job-requests", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/job-requests",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 A page of job requests.
{
"data": [
{
"public_id": "Yk3Lm8Nq2Tz6Vb1Xc5Wd9Hs4Jf0Pg7Ra",
"status": "new",
"caller_name": "Jo Smith",
"caller_e164": "+447700900123",
"contact_e164": "+447700900123",
"line_e164": "+442035550123",
"line_label": "Sales",
"job_type": "Boiler repair",
"description": "Boiler stopped working, no hot water.",
"address": "4 Harbour Street, Hull",
"postcode": "HU1 1AA",
"preferred_when": "Tomorrow morning",
"preferred_date": "2026-10-15",
"preferred_time_window": "morning",
"received_at": "2026-10-14T18:43:10+01:00",
"done_at": null,
"done_by": null,
"message_recording_public_id": "Pg7YaRk3Lm8Nq2Tz6Vb1Xc5Wd9Hs4Jf0",
"call_public_id": "Qm4Tz8Kc1Lp6Wr3Nx9Bv2Hd7Fs5Jy0Ga"
}
],
"current_page": 1,
"last_page": 1,
"per_page": 20,
"total": 1,
"next_page_url": null,
"prev_page_url": null
}
Blocked callers
Numbers whose calls the account rejects or sends straight to voicemail.
Needs the Integrations add-on (included on Scale; available on Mobile and Reception)
List blocked callers
GET
/blocked-callers
Numbers the account has blocked, newest first. Each says what its calls get.
Example request
curl
curl "https://app.linehq.co.uk/api/v1/blocked-callers" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/blocked-callers", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/blocked-callers",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 Every blocked caller.
{
"data": [
{
"public_id": "Kc1Lp6Wr3Nx9Bv2Hd7Fs5Jy0GaQm4Tz8",
"e164": "+447700900666",
"action": "reject",
"label": "Sales calls",
"number": null,
"created_at": "2026-10-02"
}
]
}
Analytics
Call totals over time.
Call analytics
GET
/analytics
Call totals for the last 7, 30 or 90 days: outcomes, calls per day and per hour, calls per number, and the mood of summarised calls. Calls call protection filtered out are counted separately in summary.filtered.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
days |
query | one of 7, 30, 90
|
7, 30 (the default) or 90.
|
number |
query | string | Only calls to this number: its public_id, from List numbers. A public_id that names none of the account's live numbers is a 404. by_number always covers every number.
|
Example request
curl
curl "https://app.linehq.co.uk/api/v1/analytics" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/analytics", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/analytics",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The totals.
{
"days": 7,
"summary": {
"total": 46,
"filtered": 3,
"answered": 38,
"missed": 5,
"voicemail": 2,
"blocked": 1,
"answer_rate": 83,
"avg_forward_seconds": 151
},
"by_day": {
"2026-10-07": 4,
"2026-10-08": 5,
"2026-10-09": 7,
"2026-10-10": 6,
"2026-10-11": 2,
"2026-10-12": 1,
"2026-10-13": 11,
"2026-10-14": 10
},
"by_hour": [
0,
0,
0,
0,
0,
0,
0,
0,
3,
6,
7,
5,
4,
3,
5,
4,
3,
2,
0,
0,
0,
0,
0,
0
],
"by_number": [
{
"public_id": "8kQ2mVn4Lr7Tz1Xc9Bw3Hd6Fs0Jy5Pa2",
"e164": "+442035550123",
"label": "Sales",
"total": 46,
"answered": 38
}
],
"sentiment": {
"positive": 12,
"neutral": 20,
"negative": 3
}
}
Billing
Movements on the prepaid balance, and receipts.
List balance movements
GET
/billing/ledger
Every change to the prepaid balance, newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
page |
query | integer | The page to return, from 1. |
per_page |
query | integer | Records per page, up to 100. Defaults to 25. |
Example request
curl
curl "https://app.linehq.co.uk/api/v1/billing/ledger" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/billing/ledger", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/billing/ledger",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 A page of balance movements.
{
"data": [
{
"type": "charge",
"amount_pence": -12,
"balance_after_pence": 2488,
"note": "Forwarded call 7d3e9a41-2b6c-4f85-9e17-c0a4b8d25f6e",
"created_at": "2026-10-14T09:33:20+01:00"
},
{
"type": "topup",
"amount_pence": 2500,
"balance_after_pence": 2500,
"note": "Calling credit",
"created_at": "2026-10-01T10:00:00+01:00"
}
],
"current_page": 1,
"last_page": 1,
"per_page": 25,
"total": 2,
"next_page_url": null,
"prev_page_url": null
}
List receipts
GET
/billing/receipts
The account's last 48 payments, newest first.
Example request
curl
curl "https://app.linehq.co.uk/api/v1/billing/receipts" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/billing/receipts", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/billing/receipts",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The receipts.
{
"data": [
{
"number": "LHQ-261001-K7Q2XM",
"kind": "topup",
"amount_pence": 2500,
"description": "Calling credit",
"paid_at": "2026-10-01T10:00:00+01:00"
}
]
}
Webhooks
Webhook endpoints LineHQ posts events to, and their deliveries. Owners only, with an API key holding the webhooks:manage permission. Adding an endpoint, sending a test and resending need the Integrations add-on; the rest keep working after it lapses, so an owner can tidy up.
List webhook endpoints
GET
/webhooks
The account's webhook endpoints. Owners only, with a key holding the webhooks:manage permission, because an endpoint's address can be a secret.
Example request
curl
curl "https://app.linehq.co.uk/api/v1/webhooks" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/webhooks", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The endpoints, and what can be added.
{
"can_add": true,
"events": [
"call.ended",
"voicemail.received",
"transcript.ready"
],
"max": 5,
"endpoints": [
{
"public_id": "Fs5Jy0GaQm4Tz8Kc1Lp6Wr3Nx9Bv2Hd7",
"name": "Acme CRM",
"url": "https://crm.acmeplumbing.co.uk/hooks/linehq",
"events": [
"call.ended",
"transcript.ready"
],
"status": "active",
"failing_since": null
}
]
}
Add a webhook endpoint
POST
/webhooks
Add an endpoint. Its signing secret is returned once, here; store it to check each delivery's signature. The address must be public HTTPS. At most 20 a minute.
Request body
| Field | Type | Description |
|---|---|---|
name required |
string | |
url required |
string | An https:// address on the public internet.
|
events required |
array of one of "call.ended", "voicemail.received", "transcript.ready"
|
Example request
curl
curl -X POST "https://app.linehq.co.uk/api/v1/webhooks" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"name": "Acme CRM",
"url": "https://crm.acmeplumbing.co.uk/hooks/linehq",
"events": [
"call.ended",
"transcript.ready"
]
}'JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/webhooks", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"name": "Acme CRM",
"url": "https://crm.acmeplumbing.co.uk/hooks/linehq",
"events": [
"call.ended",
"transcript.ready"
]
}),
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.post(
"https://app.linehq.co.uk/api/v1/webhooks",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
json={
"name": "Acme CRM",
"url": "https://crm.acmeplumbing.co.uk/hooks/linehq",
"events": [
"call.ended",
"transcript.ready",
],
},
)
response.raise_for_status()
print(response.json())Example response
201 Added. The secret is never shown again.
{
"endpoint": {
"public_id": "Fs5Jy0GaQm4Tz8Kc1Lp6Wr3Nx9Bv2Hd7",
"name": "Acme CRM",
"url": "https://crm.acmeplumbing.co.uk/hooks/linehq",
"events": [
"call.ended",
"transcript.ready"
],
"status": "active",
"failing_since": null
},
"secret": "whsec_3f9c2b7a1d8e4f60b5c2a9e7d1f3b8c4"
}
Remove a webhook endpoint
DELETE
/webhooks/{endpoint}
Remove the endpoint. Deliveries to it stop at once.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint required |
path | string | The endpoint's public_id, from List webhooks.
|
Example request
curl
curl -X DELETE "https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID", {
method: "DELETE",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);Python
import os
import requests
response = requests.delete(
"https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()Example response
204 Removed.
Change a webhook endpoint
PATCH
/webhooks/{endpoint}
Change any of its name, address or events, or pause and resume it.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint required |
path | string | The endpoint's public_id, from List webhooks.
|
Request body
| Field | Type | Description |
|---|---|---|
name |
string | |
url |
string | |
events |
array of one of "call.ended", "voicemail.received", "transcript.ready"
|
|
status |
one of "active", "paused"
|
Example request
curl
curl -X PATCH "https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json" \
-H "Content-Type: application/json" \
-d '{
"status": "paused"
}'JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID", {
method: "PATCH",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
"Content-Type": "application/json",
},
body: JSON.stringify({
"status": "paused"
}),
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.patch(
"https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
json={
"status": "paused",
},
)
response.raise_for_status()
print(response.json())Example response
200 The endpoint.
{
"endpoint": {
"public_id": "Fs5Jy0GaQm4Tz8Kc1Lp6Wr3Nx9Bv2Hd7",
"name": "Acme CRM",
"url": "https://crm.acmeplumbing.co.uk/hooks/linehq",
"events": [
"call.ended",
"transcript.ready"
],
"status": "active",
"failing_since": null
}
}
Replace a webhook endpoint's secret
POST
/webhooks/{endpoint}/rotate
Issue a new signing secret, returned once, here. Deliveries are signed with the new one from now on.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint required |
path | string | The endpoint's public_id, from List webhooks.
|
Example request
curl
curl -X POST "https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/rotate" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/rotate", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.post(
"https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/rotate",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The new secret.
{
"endpoint": {
"public_id": "Fs5Jy0GaQm4Tz8Kc1Lp6Wr3Nx9Bv2Hd7",
"name": "Acme CRM",
"url": "https://crm.acmeplumbing.co.uk/hooks/linehq",
"events": [
"call.ended",
"transcript.ready"
],
"status": "active",
"failing_since": null
},
"secret": "whsec_8d1e4a7c2f9b3e60a5d2c8f1b7e4a9c3"
}
Send a test event
POST
/webhooks/{endpoint}/test
Queue a signed test delivery to an active endpoint. At most 10 a minute.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint required |
path | string | The endpoint's public_id, from List webhooks.
|
Example request
curl
curl -X POST "https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/test" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/test", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.post(
"https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/test",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
202 Queued.
{
"queued": true
}
List deliveries
GET
/webhooks/{endpoint}/deliveries
The endpoint's latest 100 deliveries from the last 30 days, newest first.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint required |
path | string | The endpoint's public_id, from List webhooks.
|
Example request
curl
curl "https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/deliveries" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/deliveries", {
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.get(
"https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/deliveries",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
200 The deliveries.
{
"deliveries": [
{
"event": "call.ended",
"event_id": "2c9e7b14-5f3a-4d8e-b1c6-7a9f0e2d4b63",
"status": "failed",
"attempts": 6,
"response_code": 500,
"last_error": "HTTP 500",
"created_at": "2026-10-14T09:33:21+01:00",
"next_attempt_at": null
}
]
}
Resend a delivery
POST
/webhooks/{endpoint}/deliveries/{eventId}/resend
Send a failed or cancelled delivery again, as a fresh delivery with the same event id so your receiver can spot the repeat. At most 30 a minute.
Parameters
| Name | In | Type | Description |
|---|---|---|---|
endpoint required |
path | string | The endpoint's public_id, from List webhooks.
|
eventId required |
path | string | The delivery's event_id.
|
Example request
curl
curl -X POST "https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/deliveries/EVENT_ID/resend" \
-H "Authorization: Bearer $LINEHQ_API_KEY" \
-H "Accept: application/json"JavaScript
const response = await fetch("https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/deliveries/EVENT_ID/resend", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.LINEHQ_API_KEY}`,
Accept: "application/json",
},
});
if (!response.ok) throw new Error(`LineHQ answered ${response.status}`);
const data = await response.json();Python
import os
import requests
response = requests.post(
"https://app.linehq.co.uk/api/v1/webhooks/ENDPOINT_ID/deliveries/EVENT_ID/resend",
headers={"Authorization": f"Bearer {os.environ['LINEHQ_API_KEY']}", "Accept": "application/json"},
)
response.raise_for_status()
print(response.json())Example response
202 Queued.
{
"queued": true
}