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.
Authentication
API keys, scopes, rate limits, the error format and request IDs.
Every request to the API carries an API key as a bearer token:
curl https://api.hypamoon.com/api/v1/project \
-H "Authorization: Bearer $HYPAMOON_API_KEY"await fetch("https://api.hypamoon.com/api/v1/project", {
headers: { Authorization: `Bearer ${process.env.HYPAMOON_API_KEY}` },
});requests.get(
"https://api.hypamoon.com/api/v1/project",
headers={"Authorization": f"Bearer {os.environ['HYPAMOON_API_KEY']}"},
)There's no other way to authenticate: no query parameter, no cookie. Keep keys on your server; never ship them in a browser or mobile app.
Keys
A key belongs to one project. A project has its own sites and apps, data, media, domains, email, numbers, call plan, assistant and calls; if you build two apps, use two projects.
| Prefix | Mode | |
|---|---|---|
hm_live_ |
Live | Real numbers, real calls and SMS, real costs within your spending cap. |
hm_test_ |
Test | A test project that never reaches the phone network and costs nothing. Sites and data are real here too. Test numbers come from the fictional range +447700900000 to +447700900999. See Test mode. |
New projects since October 2026 are one project: a hm_test_ key on it can
never do anything real, and phone, outbound calls, SMS, email to other people,
payments and buying domains each go live on their own at
https://app.hypamoon.com/go-live. Older projects come as a pair, a test project
and a live project, each with its own keys. When you sign up at https://app.hypamoon.com, you get your first test
key right away, with every scope. You can create more keys in the portal under
API keys: test keys at any time, and live keys once your company (or, for a
personal account, you) is verified under Verification. A test key on the live
project, or a live key on the test project, returns 403 with key_mode_mismatch.
The prefix is followed by 32 letters and digits. Anything else is rejected without a lookup.
We store only a hash of the key. You see the full key once, when it is
created; after that we can only show its first characters (for example
hm_live_4f9Q) to help you recognize it. Lost a key? Revoke it and create a
new one. A revoked key returns 401 immediately.
Scopes
Each key has a list of scopes. The first test key has all of them. When you create a key in the portal, you select its scopes; the read scopes are enough for a dashboard.
| Scope | Allows | Read-only |
|---|---|---|
project:read |
GET /project, the balance, the agents, and the MCP server |
yes |
numbers:read |
GET /numbers, GET /numbers/available, GET /numbers/{id}/call-plan |
yes |
calls:read |
GET /calls, GET /calls/{id}, including transcripts, and POST /test/calls in test mode |
yes |
messages:read |
SMS messages and emails, with their status | yes |
chat:read |
Chat conversations and chat widget tokens | yes |
sites:read |
Sites, their versions, domains, apps, the media library and sending domains | yes |
data:read |
Collections and their documents | yes |
project:write |
Setting the project's company details and managing agents | no |
numbers:write |
Claiming numbers and setting call plans | no |
assistant:write |
Configuring the voice AI assistant | no |
webhooks:write |
Creating, listing, deleting, enabling and testing webhook endpoints | no |
messages:write |
Sending SMS and email | no |
calls:write |
Outbound calls made by the assistant | no |
chat:write |
Starting chats and managing chat widget tokens | no |
sites:write |
Publishing sites, and managing domains, apps, the media library and sending domains | no |
data:write |
Creating collections and writing documents | no |
Give each key only what it needs. A key that only reads summaries for a
dashboard doesn't need webhooks:write. A request without the right scope
gets 403 with missing_scope.
Limit a key to some sites and apps
By default a key works on the whole project. In the portal, under API keys, an owner or admin can limit a key to selected sites and apps: when you create it, or later with Access next to the key. The app of a site comes along, and so does the data the site or app uses (the collections its forms post to, and the collections of the app). The limit holds on every route and every MCP tool, also for a key made by an owner:
- lists (
GET /sites,GET /apps,GET /collections) show only what the key may use; - a site, app or collection outside the limit returns
403withforbiddenand auser_messagelike "This API key is limited to: Newme2day site. An owner or admin can change that in the portal under API keys."; - anything for the whole project returns the same
403: billing and the balance, the team, domains, email, numbers and calls, webhooks, agents, secrets, a new site or app, a new collection, and working on a client project of an agency.
GET /project (get_project) tells the key what it is limited to, in
access: null for the whole project, or the sites, apps and collections with
limited_by (api_key, connection, or member when the person behind it is
limited in the team). A limited key gets only the project's id, name, edition,
mode and access there. A change to the limit applies from the next request.
Chats you connected with OAuth (Claude, ChatGPT, Grok) work the same way: limit each one in the portal under Connect your chat, after you connected it. If the person behind a key or connection is limited in the team too, the key gets what both allow.
Rate limits
Each key can make 120 requests per 60 seconds. If you go over the
limit, you get 429 with a Retry-After header (in seconds):
HTTP/1.1 429 Too Many Requests
Content-Type: application/problem+json; charset=utf-8
Retry-After: 60
X-Request-Id: req_8Kq2mXvN4pR7tL1wZ9cBEvery authenticated request counts, even one that fails because of a missing
scope. Wait the number of seconds in Retry-After before you try again. For a stream of events, use
webhooks instead of polling GET /calls.
Errors
All errors use one format,
RFC 9457 problem details, with the
content type application/problem+json:
{
"type": "https://hypamoon.com/docs/errors/missing_scope",
"title": "Forbidden",
"status": 403,
"detail": "This API key lacks the 'webhooks:write' scope.",
"request_id": "req_8Kq2mXvN4pR7tL1wZ9cB"
}| Field | |
|---|---|
type |
A URL to the page that explains this error and how to fix it. Branch on this, not on detail. |
title |
The HTTP status in words. |
status |
The HTTP status code, the same as on the response. |
detail |
One human-readable sentence, specific to this request. The wording may change. |
request_id |
The same value as the X-Request-Id header. |
All error types are listed on the Errors page.
The checks run in a fixed order, so you always get the first one that fails:
missing or unknown key (401), revoked key or deleted project (401),
project not enabled for the API (403), rate limit (429), scope (403),
then the request itself (400, 404, 409).
Request IDs
Every response, success or error, has an X-Request-Id header. Include it
when you contact us; it's how we find your request in our logs.
You can send your own X-Request-Id, for example the ID your system already
uses for the job. We use it if it's 1 to 64 characters long and contains only letters, digits,
- and _. Otherwise, we generate one: req_ followed by 20 characters.
curl -i https://api.hypamoon.com/api/v1/calls \
-H "Authorization: Bearer $HYPAMOON_API_KEY" \
-H "X-Request-Id: sync-job-2026-10-01-0001"const res = await fetch("https://api.hypamoon.com/api/v1/calls", {
headers: {
Authorization: `Bearer ${process.env.HYPAMOON_API_KEY}`,
"X-Request-Id": "sync-job-2026-10-01-0001",
},
});
console.log(res.headers.get("x-request-id"));res = requests.get(
"https://api.hypamoon.com/api/v1/calls",
headers={
"Authorization": f"Bearer {os.environ['HYPAMOON_API_KEY']}",
"X-Request-Id": "sync-job-2026-10-01-0001",
},
)
print(res.headers["X-Request-Id"])Responses from the API are never cached (Cache-Control: no-store).