Skip to navigation

Testing

Testing webhooks locally

Start with a handler from Code examples. Keep signature verification enabled, including during local development.

Using ngrok

Run your server on port 3000, then expose it through an HTTPS tunnel:

ngrok http 3000

Register the tunnel’s HTTPS URL with /webhook appended. Save the returned secret as AGENTPHONE_WEBHOOK_SECRET and restart the local handler with that value:

curl --fail-with-body https://api.agentphone.ai/v1/webhooks \
-H "Authorization: Bearer $AGENTPHONE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"url":"https://YOUR_TUNNEL_HOST/webhook","contextLimit":10}'

The secret is a credential. Keep it out of logs and source control. If the tunnel URL changes, register the new URL and save the secret returned by that update.

Using localtunnel

A different HTTPS tunnel can be used with the same workflow. Its URL must accept direct server-to-server POST requests without an interstitial, login page, or browser challenge.

Test webhook endpoint

curl --fail-with-body -X POST https://api.agentphone.ai/v1/webhooks/test \
-H "Authorization: Bearer $AGENTPHONE_API_KEY" \
-H 'Content-Type: application/json' \
-d '{"channel":"sms"}'

This sends a signed synthetic event, marked with top-level test: true. Supported channels are sms, mms, imessage, whatsapp, and rcs. A successful response confirms endpoint delivery; it does not prove carrier delivery, registration approval, number readiness, or live voice behavior.

Test a per-agent override with POST /v1/agents/{agent_id}/webhook/test. Test in the same sub-account scope as the webhook. Avoid real outbound sends in response to synthetic events.

Mock webhook payloads

SMS payload

Save this fixture as event.json:

{
"event": "agent.message",
"channel": "sms",
"test": true,
"timestamp": "2026-10-26T00:00:00Z",
"agentId": "agt_example",
"data": {
"messageId": "msg_example",
"numberId": "num_example",
"from": "+14155550123",
"to": "+12125550199",
"message": "Hello from a local test",
"direction": "inbound"
}
}

Voice payload

For a local voice fixture, set channel to voice and put the caller’s current text in data.transcript. A voice handler must return {"text":"..."} or NDJSON speech chunks, while a text handler just acknowledges. The API’s synthetic test endpoint does not test voice.

Testing with curl

Sign the exact bytes you send. This local script uses the same timestamp/body HMAC as AgentPhone and makes no carrier calls:

send_fixture.py
import hashlib
import hmac
import os
from pathlib import Path
import time
import urllib.request
import uuid
delivery_id = os.environ.get("WEBHOOK_DELIVERY_ID") or str(uuid.uuid4())
body = Path("event.json").read_bytes()
timestamp = str(int(time.time()))
secret = os.environ["AGENTPHONE_WEBHOOK_SECRET"].encode()
signature = hmac.new(secret, timestamp.encode() + b"." + body, hashlib.sha256).hexdigest()
request = urllib.request.Request(
"http://127.0.0.1:3000/webhook",
data=body,
headers={
"Content-Type": "application/json",
"X-Webhook-Timestamp": timestamp,
"X-Webhook-Signature": "sha256=" + signature,
"X-Webhook-ID": delivery_id,
},
method="POST",
)
print("Delivery ID:", delivery_id)
with urllib.request.urlopen(request, timeout=10) as response:
print(response.status, response.read().decode())

Each run generates a new delivery ID. To test a retry, set WEBHOOK_DELIVERY_ID to the printed ID and reuse the same payload. For a distinct message event, also change data.messageId in event.json if your handler deduplicates by message ID.

Example test handlers

Use the Express, Flask, or Next.js handlers. Verify these cases before adding business logic:

  1. A valid signature is accepted; a changed body, missing header, or stale timestamp is rejected.
  2. A repeated delivery ID does not repeat a queued side effect.
  3. A text acknowledgement does not accidentally attempt a voice response or send a paid message.
  4. A voice event uses data.transcript, including the current turn, and returns speech before the configured timeout.
  5. A group reply targets data.group.groupId, not the individual sender.

Checking webhook deliveries

curl --fail-with-body 'https://api.agentphone.ai/v1/webhooks/deliveries?limit=10' \
-H "Authorization: Bearer $AGENTPHONE_API_KEY"

Inspect httpStatus, errorMessage, attemptCount, and nextRetryAt. Master delivery history uses an items array; per-agent delivery history returns an array directly. See Webhooks.

Test a real channel

After synthetic checks pass, use a number and recipient you control. Receive an inbound text, verify the webhook, and send one reply with an explicit sender. For voice, use a browser web call or a real phone call and check the transcript and response audio. These tests exercise live service and can incur usage charges. Release temporary numbers separately when finished; deleting their agent does not stop number billing.