Start for free. Sign up without a card and you'll get a test project and a test key right away. Once your company (or, for a personal account, you) is verified, you can create a live key in the portal.
Calls
The call object, the summary, the transcript and how to page through calls.
A call is one phone call to one of your project's numbers. The list, the detail view and the webhooks all use the same object with the same field names, so what you store from a webhook matches what you fetch later.
The call object
{
"id": "7f1c2d3e-4b5a-4c6d-8e7f-901a2b3c4d5e",
"direction": "inbound",
"from": "+447700900456",
"to": "+3185•••••42",
"status": "ai_handled",
"created_at": "2026-09-21T14:10:02.118Z",
"started_at": "2026-09-21T14:10:02.118Z",
"answered_at": "2026-09-21T14:10:03.004Z",
"ended_at": "2026-09-21T14:12:51.770Z",
"duration_sec": 169,
"hangup_cause": "normal_clearing",
"has_summary": true,
"summary_ready_at": null,
"test": false
}| Field | Type | |
|---|---|---|
id |
string (UUID) | The call. Use it with GET /calls/{id}. |
direction |
string or null | inbound or outbound. Version 1 only has inbound calls. |
from |
string or null | The caller, in E.164. Can be null for a withheld number. |
to |
string or null | Your number that was called, in E.164. |
status |
string or null | See statuses. |
created_at |
timestamp | When the call reached us. Calls are sorted by this. |
started_at |
timestamp or null | When the call started. |
answered_at |
timestamp or null | When the platform answered. See the note below. |
ended_at |
timestamp or null | When the call ended. |
duration_sec |
integer or null | Length of the call in seconds. |
hangup_cause |
string or null | Why the call ended, as reported by the network, for example normal_clearing. |
has_summary |
boolean | Whether the call has a summary. |
summary_ready_at |
timestamp or null | When post-call processing finished. Null for calls the AI assistant handled; check has_summary instead. |
test |
boolean | true for a call simulated in test mode. Also included in webhooks, so you can keep test traffic separate. |
Statuses
| Status | Meaning |
|---|---|
ringing |
The call is coming in and has not been answered yet. |
active |
The call is in progress. |
completed |
The call was answered and has ended. |
missed |
Nobody answered. |
failed |
The call could not be connected, or it dropped. |
voicemail |
The caller left a voicemail. |
ai_handled |
The voice AI assistant handled the call. |
New statuses may be added. Make sure your code handles a status it doesn't recognize, for example by showing it as is.
The summary
{
"text": "Caller wants a table for four on Friday at 19:00.",
"topics": ["reservation"],
"sentiment": "positive",
"action_items": ["Confirm the reservation by text message"],
"next_step": "Send confirmation"
}| Field | Type | |
|---|---|---|
text |
string or null | A short summary of the call. |
topics |
string[] | What the call was about. |
sentiment |
string or null | positive, neutral or negative. |
action_items |
string[] | Things someone should do after the call. |
next_step |
string or null | The single most important follow-up. |
The transcript
The transcript is plain text and only available on GET /calls/{id}. It's
never sent in a webhook: a webhook can be stored and retried for almost two
days, and a transcript shouldn't outlive the call's retention period. After a
call.summary.ready webhook, fetch the call if you need the transcript.
When a call's retention period ends, the call is deleted, along with its transcript and summary. Copy anything you need to keep.
List calls
GET /calls returns the project's calls, newest first. Requires the
calls:read scope. The list doesn't include transcripts or summaries;
has_summary tells you whether a call is worth fetching.
| Query | |
|---|---|
limit |
1 to 100, default 25. |
cursor |
The next_cursor from the previous page. Omit it for the first page. |
{
"data": [
{
"id": "7f1c2d3e-4b5a-4c6d-8e7f-901a2b3c4d5e",
"direction": "inbound",
"from": "+447700900456",
"to": "+3185•••••42",
"status": "ai_handled",
"created_at": "2026-09-21T14:10:02.118Z",
"started_at": "2026-09-21T14:10:02.118Z",
"answered_at": "2026-09-21T14:10:03.004Z",
"ended_at": "2026-09-21T14:12:51.770Z",
"duration_sec": 169,
"hangup_cause": "normal_clearing",
"has_summary": true,
"summary_ready_at": null,
"test": false
}
],
"has_more": true,
"next_cursor": "eyJ0IjoiMjAyNi0wOS0yMVQxNDoxMDowMi4xMTgrMDA6MDAiLCJpZCI6IjdmMWMyZDNlIn0"
}Keep requesting with cursor=<next_cursor> until has_more is false. The
cursor is opaque; don't construct or modify it. A cursor keeps working as new
calls come in: they appear on the first page, not partway through your
paging.
curl "https://api.hypamoon.com/api/v1/calls?limit=100" \
-H "Authorization: Bearer $HYPAMOON_API_KEY"async function* allCalls() {
let cursor: string | null = null;
do {
const url = new URL("https://api.hypamoon.com/api/v1/calls");
url.searchParams.set("limit", "100");
if (cursor) url.searchParams.set("cursor", cursor);
const res = await fetch(url, {
headers: { Authorization: `Bearer ${process.env.HYPAMOON_API_KEY}` },
});
if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);
const page = await res.json();
yield* page.data;
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
}def all_calls():
cursor = None
while True:
params = {"limit": 100}
if cursor:
params["cursor"] = cursor
res = requests.get(
"https://api.hypamoon.com/api/v1/calls",
params=params,
headers={"Authorization": f"Bearer {os.environ['HYPAMOON_API_KEY']}"},
)
res.raise_for_status()
page = res.json()
yield from page["data"]
if not page["has_more"]:
break
cursor = page["next_cursor"]Get one call
GET /calls/{id} returns the call object plus transcript and summary
(both can be null). Requires the calls:read scope.
{
"id": "7f1c2d3e-4b5a-4c6d-8e7f-901a2b3c4d5e",
"direction": "inbound",
"from": "+447700900456",
"to": "+3185•••••42",
"status": "ai_handled",
"created_at": "2026-09-21T14:10:02.118Z",
"started_at": "2026-09-21T14:10:02.118Z",
"answered_at": "2026-09-21T14:10:03.004Z",
"ended_at": "2026-09-21T14:12:51.770Z",
"duration_sec": 169,
"hangup_cause": "normal_clearing",
"has_summary": true,
"summary_ready_at": null,
"test": false,
"transcript": "Assistent: U spreekt met de digitale assistent van Acme Bookings. Waarmee kan ik u helpen?\nBeller: Hi, I'd like a table for four on Friday.",
"summary": {
"text": "Caller wants a table for four on Friday at 19:00.",
"topics": ["reservation"],
"sentiment": "positive",
"action_items": ["Confirm the reservation by text message"],
"next_step": "Send confirmation"
}
}A call that doesn't exist, or belongs to another project, returns 404
(not-found). We don't say which, so IDs from other
projects can't be probed.
What is not in the API
Internal call legs (forwarding to your team's app or browser phone) don't appear as separate calls in the API. You see one call per caller, not one per phone that rang.