1. Access & API keys
API access is enabled per account by our team. Contact us to request it. Once it’s on, open Settings → Developer API to create keys. A key is shown once; we store only a hash and show its prefix (vu_live_ab12cd…). Revoke a key at any time; it stops working immediately.
Send the key as a bearer token. Every request only sees numbers assigned to your account, and only messages and calls received while the number was yours.
curl https://verifyuniverse.com/api/v1/numbers \ -H "Authorization: Bearer vu_live_YOUR_KEY"
2. Errors & rate limits
Errors return a non-2xx status and {"error": {"code": "…", "message": "…"}}.
| Status | Code | Meaning |
|---|---|---|
| 401 | missing_api_key, invalid_api_key | No key, a malformed key, or a revoked key. |
| 403 | api_not_enabled, account_suspended | API access is off for this account. |
| 400 | invalid_number, invalid_parameter | Use E.164 numbers (+14155550123) and ISO 8601 or unix-second times. |
| 404 | call_not_found, not_found | Unknown id, or not yours. |
| 429 | rate_limited | Over 120 requests per minute per key. See the Retry-After header. |
3. List your numbers
GET/api/v1/numbers
curl https://verifyuniverse.com/api/v1/numbers -H "Authorization: Bearer $VU_KEY"
{
"data": [
{ "number": "+14155550123", "country": "US", "status": "assigned", "sms": true, "voice": true,
"since": "2026-10-09T18:00:00Z", "expires_at": null }
]
}
4. Calls to a number
GET/api/v1/numbers/{e164}/calls?since=&limit=. since defaults to 24 hours ago and limit to 50 (max 100). Results are newest first.
curl "https://verifyuniverse.com/api/v1/numbers/+14155550123/calls?since=2026-10-09T00:00:00Z" \
-H "Authorization: Bearer $VU_KEY"
{
"number": "+14155550123",
"data": [{
"id": "8c1d…", "number": "+14155550123", "from": "+18005550199", "status": "completed",
"started_at": "2026-10-09T20:14:03Z", "ended_at": "2026-10-09T20:14:41Z", "duration_sec": 38.2,
"otp": { "code": "482916", "confidence": 0.95, "status": "confirmed", "detected_at": "2026-10-09T20:14:19Z" },
"dtmf_sent": [{ "digit": "1", "at_sec": 6.1 }],
"transcript": "…your verification code is 4 8 2 9 1 6…",
"transcript_purged": false,
"recording": { "available": true, "expires_at": "2026-10-09T20:34:45Z",
"url": "https://verifyuniverse.com/api/v1/recordings/8c1d…?exp=…&sig=…",
"url_expires_at": "2026-10-09T20:19:45Z" }
}]
}
The call status is in_progress while the call is still live (the code can already be present), then completed, max_duration, no_audio or failed. The OTP status is confirmed (the code was repeated), likely or uncertain.
5. SMS to a number
GET/api/v1/numbers/{e164}/messages?since=&limit=
curl "https://verifyuniverse.com/api/v1/numbers/+14155550123/messages" -H "Authorization: Bearer $VU_KEY"
{ "number": "+14155550123",
"data": [{ "id": "b7e2…", "type": "sms", "number": "+14155550123", "from": "Google",
"text": "G-731942 is your Google verification code.", "otp": "731942",
"received_at": "2026-10-09T20:20:00Z" }] }
6. One call
GET/api/v1/calls/{id} returns {"data": { …call… }} in the same shape as above.
curl https://verifyuniverse.com/api/v1/calls/8c1d… -H "Authorization: Bearer $VU_KEY"
7. Latest code (long-poll)
GET/api/v1/otp/latest?number=&since=&wait= returns the newest code from an SMS or a call received after since (default: 10 minutes ago). With wait=1…30 the request waits up to that many seconds for a new code. Codes from calls arrive while the call is still in progress. A long-poll counts as one request against your rate limit.
# Start your sign-up flow, then:
SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
curl "https://verifyuniverse.com/api/v1/otp/latest?number=%2B14155550123&since=$SINCE&wait=30" \
-H "Authorization: Bearer $VU_KEY"
{ "data": { "code": "482916", "source": "call", "confidence": 0.95, "status": "confirmed",
"detected_at": "2026-10-09T20:14:19Z", "number": "+14155550123", "from": "+18005550199",
"call_id": "8c1d…", "message_id": null, "text": null, "call_status": "in_progress" } }
# nothing yet:
{ "data": null, "waited_sec": 30 }
8. Recordings
Call recordings are deleted automatically 20 minutes after the call ends, everywhere: on our servers and in the database. While a recording exists, call objects include a signed recording.url that works for 5 minutes and never past the deletion time. It needs no API key. After deletion the link returns 404 and recording.available is false. Transcripts, codes and SMS text are kept.
curl -o call.mp3 "https://verifyuniverse.com/api/v1/recordings/8c1d…?exp=1791634785&sig=…"
9. Webhooks
Set an https endpoint in Settings → Developer API. We POST JSON for these events:
| Event | When |
|---|---|
otp.detected | A code was heard on a call, usually seconds before the caller hangs up. Sent again if the reading changes. |
call.completed | The call ended: full transcript, keys pressed, recording info. |
sms.received | An SMS arrived (with otp when a code was found). |
ping | The “Send test” button. |
POST /hooks/verifyuniverse HTTP/1.1
Content-Type: application/json
VU-Event: otp.detected
VU-Delivery: 5f0c…
VU-Signature: t=1791634459,v1=6b1f…
{"id": "5f0c…", "data": {"id": "8c1d…", "otp": {"code": "482916", "status": "likely", …}, "status": "in_progress", …},
"type": "otp.detected", "created_at": "2026-10-09T20:14:19.4Z"}
Respond with any 2xx within 10 seconds. Failed deliveries are retried after 1, 5, 15, 60 and 240 minutes (6 attempts in total). Use VU-Delivery (equal to the body’s id) to ignore duplicates.
10. Verifying signatures
v1 is the hex HMAC-SHA256 of "{t}.{raw request body}" using your signing secret (whsec_…, shown once when created or rotated). Reject requests whose t is more than 5 minutes old.
// Node.js (Express): use the raw body, not re-serialized JSON
import crypto from "node:crypto";
app.post("/hooks/verifyuniverse", express.raw({ type: "application/json" }), (req, res) => {
const [t, v1] = req.get("VU-Signature").split(",").map((p) => p.split("=")[1]);
const expected = crypto.createHmac("sha256", process.env.VU_WEBHOOK_SECRET)
.update(`${t}.${req.body}`).digest("hex");
const ok = v1.length === expected.length && crypto.timingSafeEqual(Buffer.from(v1), Buffer.from(expected))
&& Math.abs(Date.now() / 1000 - Number(t)) < 300;
if (!ok) return res.status(400).end();
const event = JSON.parse(req.body);
if (event.type === "otp.detected") console.log("code", event.data.otp.code);
res.status(204).end();
});
# Python (Flask)
import hmac, hashlib, time
sig = dict(p.split("=", 1) for p in request.headers["VU-Signature"].split(","))
mac = hmac.new(SECRET.encode(), f"{sig['t']}.".encode() + request.get_data(), hashlib.sha256).hexdigest()
assert hmac.compare_digest(mac, sig["v1"]) and abs(time.time() - int(sig["t"])) < 300