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.
Test mode
Build against the real API without a real number, real calls or real costs. Test keys, test numbers, simulated calls and going live.
In test mode, you build against the same API, with the same routes, checks and errors, but nothing reaches the phone network and nothing is charged. You don't need a payment method. Sites and data are real: a site you publish with a test key is online for everyone, also on your own domain once its nameservers point to Hypamoon. Your numbers are test numbers, and calls are simulated on request, complete with a transcript, a summary and the real webhooks. SMS, email, payments and buying or transferring a domain are simulated too.
You get a test key as soon as you sign up at https://app.hypamoon.com.
New projects since October 2026 are one project: sites, data and apps are
always 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. A hm_test_ key can never do anything real.
Older projects can still have a separate test project; the rest of this page
describes that pair.
Start every integration here. Once it works end to end, swap the test key for the live key; nothing else in your code changes.
Test keys and test projects
Every project comes as a pair:
| Test project | Live project | |
|---|---|---|
| Key | hm_test_... |
hm_live_... |
| How you get it | as soon as you sign up | in the portal, once you're verified |
| Scopes | all of them | the ones you select when you create it |
| Sites and data | real and online | real and online |
| Numbers | test numbers, active immediately |
real numbers |
| Calls | simulated with POST /test/calls |
real callers |
| Costs | none, no payment method needed | within your spending cap |
| Voice AI | settings are saved and checked, not built | always included |
The two never mix. Test numbers, call plans, webhooks and simulated calls stay in
the test project, and the live key never sees them. A test key on the live
project, or a live key on the test project, returns
403 key_mode_mismatch. When the live project
is deleted, its test keys stop working too.
GET /project tells you which mode you're in:
curl https://api.hypamoon.com/api/v1/project \
-H "Authorization: Bearer $HYPAMOON_API_KEY"const res = await fetch("https://api.hypamoon.com/api/v1/project", {
headers: { Authorization: `Bearer ${process.env.HYPAMOON_API_KEY}` },
});
const project = await res.json(); // { mode: "test", live_project_id: "...", spending_limit_eur: null, ... }project = requests.get(
"https://api.hypamoon.com/api/v1/project",
headers={"Authorization": f"Bearer {os.environ['HYPAMOON_API_KEY']}"},
).json() # {"mode": "test", "live_project_id": "...", "spending_limit_eur": None, ...}Test numbers
Test numbers run from +447700900000 to +447700900999, the range Ofcom
reserves for use in drama. No phone anywhere has one of these numbers, so a test can never ring
a real person. All test projects share this range.
GET /numbers/availablereturns free test numbers withtest: true.POST /numbersclaims one, with{ "phone_number": "+447700900042" }from the search. TheIdempotency-Keyworks the same as it does live. A test number isactiveimmediately.- A test project can have up to three test numbers
(
422 test_number_limit_reached). Reuse the ones you have; simulated calls work on any of them.
Of the checks that run before an order, only the company details check applies
in test mode
(422 company_details_missing), so the
steps match live. There's no spending cap, payment method or billing check, and
nothing is ordered from the carrier.
Every number and every call has a test field. It's true for test numbers
and simulated calls, including inside webhooks, so your code can keep test
traffic separate.
What else works differently
| Request | In test mode |
|---|---|
PUT /project/company |
Checked and saved the same way; nothing is sent to the carrier. |
PUT /assistant |
Saved and checked; sync is skipped_test_mode. The assistant is built when you go live. |
PUT /numbers/{id}/call-plan |
Checked and saved; assistant_sync is skipped_test_mode. A plan with ai_agent still needs the assistant first, but not voice AI. |
| Forward targets in a plan | Only test numbers: free ones from the range, or your own test numbers (forward_target_not_allowed). |
Webhooks, GET /calls, GET /calls/{id} |
The same as live. |
Simulate a call
Nobody can call a test number, so you trigger the call with
POST /test/calls. Pick a scenario:
| Scenario | What happens | Webhooks |
|---|---|---|
reservation |
A caller books a table. | call.completed, then call.summary.ready |
callback_request |
A caller asks to be called back. | call.completed, then call.summary.ready |
question |
A caller asks whether you're open on Sunday. | call.completed, then call.summary.ready |
missed |
Nobody picked up: no transcript and no summary. | call.completed only |
curl -X POST https://api.hypamoon.com/api/v1/test/calls \
-H "Authorization: Bearer $HYPAMOON_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "number_id": "'$NUMBER_ID'", "scenario": "callback_request" }'const res = await fetch("https://api.hypamoon.com/api/v1/test/calls", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.HYPAMOON_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ number_id: numberId, scenario: "callback_request" }),
});
const call = await res.json(); // 201, the call as GET /calls/{id} returns itcall = requests.post(
"https://api.hypamoon.com/api/v1/test/calls",
headers={"Authorization": f"Bearer {os.environ['HYPAMOON_API_KEY']}"},
json={"number_id": number_id, "scenario": "callback_request"},
).json() # 201, the call as GET /calls/{id} returns itThe response is 201 with the full call, including transcript, summary
and test: true. The webhooks go out to your endpoints just as they would for
a real call, signed the same way, and arrive within about a minute.
number_idis one of your test numbers, fromGET /numbers.fromis optional: the caller's number, from+447700900000to+447700900999.- The transcript and the summary are fixed sample texts, in English when your
assistant's first language is
enand in Dutch otherwise. The first line is your assistant's actual greeting. - It requires a test key and the
calls:readscope. A live key gets403 test_key_required. - At most 30 simulated calls per hour per test project.
Going live
Once the webhook arrives, your signature check passes and you've stored the summary, you're ready:
- Set up the live side yourself in the portal at https://app.hypamoon.com:
complete verification (Verification), top up your balance or save a card
(Balance), set the spending cap (Billing), and create a live key with the
scopes you need (API keys). If any of these is missing, you'll get
402 payment_mandate_missing,402 insufficient_balance,422 spend_cap_missingor403 missing_scope. - Replace the test key with the live key (
hm_live_...). Connected through OAuth in a chat app instead? Then no key is involved: in the portal, open Connect your chat and choose Switch to live next to that chat, or reconnect Hypamoon and choose the live project on the consent screen. Never paste an API key into a chat. Your code stays the same; only the data changes. Forward targets become real phone numbers, and the number order uses a newIdempotency-Keybecause it's a new order. - Set up the live project: company details, a number, the assistant, the call plan and your webhook endpoint. It starts empty; nothing carries over from the test project. During the beta, a live project can have up to three numbers.
- Call your number from your own phone.
POST /test/callsdoes not exist in live mode.
Keep the test key for your own tests and CI. The Quickstart walks through all of this with code.