Loading…
Developer guide
Create a key in Phone Agent, then call the API from your server to manage the agent profile and connect your business tools.
Test keys, draft agent settings, simulated connectors, contact records and their preflight are available. Preflight checks only the federal baseline window using the supplied time zone; it does not check the National DNCL or stricter local rules and never authorizes a call. Agent settings are shared across environments within each account or organization. Real phone calls and live CRM operations remain disabled.
01 · Quick start
curl --request GET 'https://reismaker.ca/api/v1/telephone/capabilities' \
--header "Authorization: Bearer <YOUR_TEST_KEY>"Each key has an environment (test or live), an expiry date and scopes. Revoke it in the portal; the full secret cannot be retrieved again.
const apiKey = process.env.REISMAKER_API_KEY;
if (!apiKey?.startsWith("rmk_tel_test_")) {
throw new Error("Set a ReisMaker Test key in REISMAKER_API_KEY.");
}
const idempotencyKey = process.env.REISMAKER_IDEMPOTENCY_KEY;
if (!idempotencyKey || !/^[A-Za-z0-9][A-Za-z0-9_-]{0,79}$/.test(idempotencyKey)) {
throw new Error("Set a stable operation ID in REISMAKER_IDEMPOTENCY_KEY.");
}
const apiBaseUrl = "https://reismaker.ca/api/v1/telephone";
async function request(path, init = {}) {
const response = await fetch(apiBaseUrl + path, {
...init,
headers: {
Authorization: "Bearer " + apiKey,
"Content-Type": "application/json",
...(init.headers ?? {})
}
});
const payload = await response.json().catch(() => null);
if (!response.ok) {
const code = typeof payload?.detail?.code === "string" ? payload.detail.code : "request_failed";
throw new Error("ReisMaker API request failed (" + response.status + ", " + code + ")");
}
return payload;
}
await request("/capabilities");
const created = await request("/calls", {
method: "POST",
headers: { "Idempotency-Key": idempotencyKey },
body: JSON.stringify({
direction: "outbound",
scenario: "Synthetic demo: the prospect already uses a service and asks for facts before discussing."
})
});
if (created.call?.simulated !== true || typeof created.call.id !== "string") {
throw new Error("Expected a simulated test call.");
}
const { call } = await request("/calls/" + encodeURIComponent(created.call.id));
console.log({ id: call.id, status: call.status, simulated: call.simulated });Set REISMAKER_API_KEY in your server’s secret manager and REISMAKER_IDEMPOTENCY_KEY to a stable, unique ID for this operation. This Test key needs telephone:catalog:read, telephone:calls:write and telephone:calls:read. Persist the ID before sending and reuse it after a timeout or network error; create a new one for a new operation. The script contacts no phone number or external service.
Alongside polling, an organization can register HTTPS webhooks to receive events. Network delivery requires SAAS_TELEPHONE_WEBHOOK_DELIVERY_ENABLED=true and stays off by default.
{
"name": "Alex",
"business_name": "Demo business",
"language": "en-CA",
"voice": "marin",
"offer_summary": "Demonstration service. Confirm details using the sourced facts below.",
"offer_facts": [
{
"claim": "A 14-day trial is available.",
"source": "Example terms, section 2"
}
],
"human_contact": {
"name": "Alise Dupont",
"phone": "+15145550123",
"email": "alise.dupont@example.com",
"postal_address": "100 Example Street, Montreal, Quebec"
},
"opening_line": "Hello, I can explain the service. Is now a good time?",
"disclosure": "I am an AI virtual assistant for Demo business."
}curl --request PUT 'https://reismaker.ca/api/v1/telephone/agent' \
--header "Authorization: Bearer <YOUR_TEST_KEY>" \
--header 'Content-Type: application/json' \
--data-binary @agent.jsonEach claim can include a source supplied by the business. ReisMaker does not verify the source; keep the profile current and include only authorized facts. Supported voices are marin, cedar, alloy, and verse; Marin and Cedar are recommended for the best quality. The optional human_contact field accepts a representative's name, E.164 phone number, and email or mailing address. The agent shares these details only when asked for a representative or how to contact the business; no call transfer takes place.
02 · Reference
Base URL: https://reismaker.ca/api/v1/telephone
| Method and route | Scope | Purpose |
|---|---|---|
GET /capabilities | telephone:catalog:read | Read languages, voices, scopes and feature status. |
GET /agentPUT /agent | telephone:agents:readtelephone:agents:write | Read or update the draft profile, including company name and AI disclosure. Publishing requires a Production organization key (owner/manager) and ready server safeguards; outbound calls stay disabled. |
POST /callsGET /callsGET /calls/{call_id} | telephone:calls:writetelephone:calls:read | Create a simulated request or read a call’s status, voice seconds, Luna token counts, and settled amount. Pages stay within the key’s environment. |
GET /events | telephone:events:read + clé d’organisation | Read events retained even without a webhook. Pass the next cursor as after; Test and Production streams are separate. |
POST /calls/{call_id}/reconcile | telephone:calls:write + rôle owner/manager | Approve a higher CAD reserve for a complete measured receipt above the original reserve. The exact measured cost is then settled without placing the call again. |
GET /integrationsPOST /integrationsDELETE /integrations/{integration_id} | telephone:integrations:readtelephone:integrations:write | List, register or revoke a company integration. |
POST /integrations/{id}/operations/{operation_id} | telephone:integrations:execute + permission métier | Run a registered operation with an idempotency key and its input schema. |
GET /webhooksPOST /webhooksDELETE /webhooks/{endpoint_id} | telephone:webhooks:readtelephone:webhooks:write + rôle owner/manager | Manage an organization HTTPS destination. Its signing secret is shown once at creation. |
GET /webhooks/{endpoint_id}/deliveries | telephone:webhooks:read + rôle owner/manager | Read status and recent attempts without exposing event contents. |
POST /contacts/do-not-call | telephone:contacts:write + clé Production d’organisation et rôle owner/manager | Add a number to the internal suppression list. Entries are idempotent and permanent in V1. |
POST /contacts/consents | telephone:contacts:write + clé Production d’organisation et rôle owner/manager | Record provenance metadata and a client attestation of consent for an AI sales call. |
POST /contacts/preflight | telephone:contacts:read + clé Production d’organisation et rôle owner/manager | Check internal suppression, matching-market metadata and the federal baseline window. campaign_sector and call_reason add a narrow check for Quebec funeral calls. The route never authorizes a call. |
GET /transport/sip-trunkPUT /transport/sip-trunkDELETE /transport/sip-trunk | telephone:transport:readtelephone:transport:write + rôle owner/manager | Configure or revoke the organization’s SIP trunk with a Production key. Credentials are encrypted and never returned. |
GET /transport/inbound-routesPOST /transport/inbound-routesDELETE /transport/inbound-routes/{route_id} | telephone:transport:readtelephone:transport:write + rôle owner/manager | Create, list or revoke inbound SIP routing secrets. The secret is shown once. |
You can also create and revoke a route in Phone Agent → Connect your phone system after selecting an organization. Copy the secret into your PBX’s secret store; the PBX must remove any caller-supplied header and add X-ReisMaker-Inbound-Route itself. The portal does not place calls.
Contact, appointment and order scopes depend on the data being accessed. An operation needs both the execute scope and its business-data scope.
The GET /events feed retains events even when no webhook is configured. The key needs telephone:events:read and must belong to an organization. Read them in order with after and next_cursor; use has_more to check whether another page is immediately available.
Available events are telephone.call.created, telephone.call.status_changed, telephone.call.usage_settled, and telephone.integration.execution.completed. Each request is signed in X-ReisMaker-Signature (HMAC-SHA256 over timestamp, event ID, and raw body) and includes an Idempotency-Key. Verify the signature against the raw body and deduplicate deliveries; an event may be retried. Payloads exclude phone numbers, transcripts, and raw CRM responses.
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhook(rawBody, eventId, signatureHeader, secret) {
const [timestampField, signatureField] = String(signatureHeader || "").split(",");
const timestampText = timestampField?.startsWith("t=") ? timestampField.slice(2) : "";
const suppliedHex = signatureField?.startsWith("v1=") ? signatureField.slice(3) : "";
if (!/^\d{1,12}$/.test(timestampText) || !/^[0-9a-f]{64}$/.test(suppliedHex)) return false;
const timestamp = Number(timestampText);
if (!Number.isSafeInteger(timestamp) || Math.abs(Date.now() / 1000 - timestamp) > 300) return false;
const expected = createHmac("sha256", secret)
.update(`${timestampText}.${eventId}.`, "utf8")
.update(rawBody)
.digest();
const supplied = Buffer.from(suppliedHex, "hex");
return expected.length === supplied.length && timingSafeEqual(expected, supplied);
}{
"call": {
"id": "<CALL_ID>",
"environment": "live",
"direction": "outbound",
"status": "completed",
"transfer_status": "not_requested",
"scenario": "",
"billing_status": "usage_settled",
"customer_price_usd": 0.06012,
"customer_charge_cents": 9,
"billing_currency": "CAD",
"usage": {
"voice_seconds": 18,
"reasoning": {
"model": "gpt-6-luna",
"response_count": 1,
"input_tokens": 50,
"cached_input_tokens": 0,
"cache_write_tokens": 0,
"output_tokens": 50
}
},
"created_at": "2026-10-08T12:00:00Z",
"updated_at": "2026-10-08T12:00:18Z",
"simulated": false
}
}Synthetic shape example: voice seconds and tokens are shown when the receipt is complete. `usage` is null when final usage is not verified; the CAD amount is this call’s rounded share of the organization’s cumulative total. Real calls remain disabled.
GPT-Live voice is measured by the second; GPT-6 Luna reasoning is measured by tokens. The intended charge adds the real provider costs of each component, multiplies the total by 4, and converts it to CAD using the rate recorded with the call. An incomplete receipt or a receipt above the reserve does not trigger a debit; funds stay reserved. Real calls are still disabled.
curl --request POST 'https://reismaker.ca/api/v1/telephone/contacts/do-not-call' \
--header "Authorization: Bearer <YOUR_LIVE_MANAGER_KEY>" \
--header 'Content-Type: application/json' \
--data '{"phone_number":"+14165550123","reason":"caller_request"}'Send an E.164 number with reason caller_request or business_policy. Only a keyed HMAC fingerprint is stored; the number is not returned. Do not log the request body. V1 has no endpoint to remove an entry. This list does not replace consent or regulatory checks, and real calls remain disabled.
curl --request POST 'https://reismaker.ca/api/v1/telephone/contacts/consents' \
--header "Authorization: Bearer <YOUR_LIVE_MANAGER_KEY>" \
--header 'Content-Type: application/json' \
--data-binary '{"source_event_id":"<SOURCE_EVENT_UUID>","phone_number":"+14165550123","jurisdiction":"CA-QC","contact_type":"consumer","phone_source":"customer_provided","phone_collected_at":"2026-09-30T15:30:00Z","phone_source_evidence_sha256":"cccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccccc","consent_method":"web_form","consented_at":"2026-10-01T12:00:00Z","consent_scope":"ai_sales_call","consent_evidence_sha256":"aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa","consent_text_sha256":"bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb","attestations":{"number_specific":true,"named_advertiser":true,"sales_calls":true,"ai_voice":true,"phone_source_documented":true}}'The live key must belong to an organization and an owner/manager. The customer keeps its evidence in its own systems and sends only SHA-256 hashes, the declared phone source, collection date, consent method and date. All five attestations confirm consent for this number and documented number provenance. This record is a customer assertion, not legal validation or call authorization. The do-not-call list always overrides it; real calls remain disabled.
curl --request POST 'https://reismaker.ca/api/v1/telephone/contacts/preflight' \
--header "Authorization: Bearer <YOUR_LIVE_MANAGER_KEY>" \
--header 'Content-Type: application/json' \
--data '{"phone_number":"+14165550123","jurisdiction":"CA-QC","contact_type":"consumer","recipient_timezone":"America/Toronto","campaign_sector":"business_services","call_reason":"unsolicited_outbound"}'The response checks consent and suppression for the same number, market and contact type. campaign_sector and call_reason add a narrow signal: unsolicited funeral solicitation to a Quebec consumer is blocked, while a declared customer request remains unverified. sector_specific_rules can return blocked_no_customer_request or customer_request_unverified; other sector rules, regional hours, the National DNCL, advertiser identity and original evidence remain unchecked. The route always returns call_authorized: false. Do not place a call based on it and do not log the request body.
curl --request PUT 'https://reismaker.ca/api/v1/telephone/transport/sip-trunk' \
--header "Authorization: Bearer <YOUR_LIVE_MANAGER_KEY>" \
--header 'Content-Type: application/json' \
--data-binary @sip-trunk.jsonThe sip-trunk.json file must contain provider_url (sips:), username, password and caller_number in E.164 format. Send credentials only from a trusted server; never use browser JavaScript. This prepares the transport but does not enable real calls.
curl --request POST 'https://reismaker.ca/api/v1/telephone/calls/<CALL_ID>/reconcile' --header "Authorization: Bearer <YOUR_LIVE_MANAGER_KEY>" \ --header 'Content-Type: application/json' --data '{"maximum_customer_charge_cents":22}'This endpoint requires a live key belonging to an organization and an owner or manager. The amount is a ceiling in CAD cents; the debit remains limited to the exact cost in the stored receipt. Replaying the request does not create a second debit.
03 · Integrations
Register an HTTPS domain and specific operations. Each operation fixes its path, method, input and output schemas, and required scopes. The agent cannot choose an arbitrary URL or path at runtime.
{
"name": "CRM test",
"base_url": "https://api.example.com",
"auth_mode": "bearer",
"credential": "",
"operations": [{
"operation_id": "find_contact",
"endpoint": "/v1/contacts/search",
"method": "POST",
"input_schema": {
"type": "object",
"properties": {"email": {"type": "string", "maxLength": 254}},
"required": ["email"],
"additionalProperties": false
},
"output_schema": {
"type": "object",
"properties": {"found": {"type": "boolean"}},
"required": ["found"],
"additionalProperties": false
},
"required_scopes": ["telephone:contacts:read"],
"voice_enabled": false
}]
}curl --request POST 'https://reismaker.ca/api/v1/telephone/integrations/<ID>/operations/find_contact' --header 'Authorization: Bearer <YOUR_TEST_KEY>' --header 'Idempotency-Key: crm-contact-lookup-001' --header 'Content-Type: application/json' --data '{"email":"demo@example.com"}'Tests validate configuration and scopes without connecting to the CRM. Live integrations stay disabled by default. If an external write has an uncertain result, do not replay it; check the execution status.
To allow a voice lookup, set voice_enabled to true only on a GET operation that uses read-only scopes. The field defaults to false; test operations and all writes stay excluded. Responses list opted-in operations in voice_enabled_operation_ids. This voice feature remains disabled in the service's current state.
04 · Controls
401 — missing, invalid, expired or revoked key403 — scope is missing404 — resource missing or owned by another account409 — idempotency conflict or uncertain write result422 — request does not match its schema429 — rate limit; respect Retry-After503 — live feature is disabledError responses do not contain the API key secret or raw CRM response bodies.