PTA-Approved SMS Aggregator · Serving Pakistan since 2014

Missed Call API

Missed Call Verification API

Verify mobile numbers with a simple missed call — no SMS OTP required.

Missed Call Verification lets you verify a user's mobile number by placing a short missed call. SENDPK calls the user's number for around 2–3 seconds. The last 4 digits of the incoming caller number become the verification PIN.

The user's app reads or asks for those last 4 digits and sends them back to SENDPK. If they match, the number is verified.

API Base URL

https://sendpk.com/apps/call/

All API responses are returned as JSON, including errors. All timestamps are Unix timestamps in seconds.

Quick Start

Six steps from nothing to a verified number.

1

Get your SENDPK API key from your dashboard.

2

Send the user's mobile number to missed-call-auth.php

3

SENDPK calls the number for 2–3 seconds.

4

Read the last 4 digits of the caller number.

5

Send those 4 digits to verify.php

6

You get "validated": true — the number is verified.

Worldwide Mobile Verification

Missed Call Verification is designed for worldwide mobile-number verification. Coverage depends on destination and call-provider availability — if a country is not available you simply get COUNTRY_NOT_SUPPORTED, and nothing is charged.

Almost Free FREE TO START

Starting a verification request, checking the number, and verifying the PIN are free. A charge is applied only when the call provider confirms that the call was chargeable — normally when the customer answers during the short call.

View Missed Call Verification Rates
Important

Keep your API key on your server

Call this API from your own server only — never straight from a mobile app or a browser. Anyone who gets your API key can spend your balance.

  • Never inside an Android APK
  • Never inside an iOS app
  • Never in browser JavaScript
  • Never in a public GitHub repository
Getting Started

How It Works

Five steps — and nothing is ever delivered to the phone.

No SMS. No OTP message. Nothing at all arrives on the phone. The code is never sent — it is the number that rings. Your user simply reads the last 4 digits from the missed call in their own call list.

  1. 1Your app

    You ask us to call a number

    Target number to verify+92 333 0189315
  2. 2We call

    We ring that phone for 2–3 seconds

    The call stops on its own. Your user does not need to answer — and should not.
  3. 3Their phone

    The number that called is the code

    Incoming → missed+1 513‑613‑7973last 4 digits = the PIN
  4. 4Your screen

    They type those 4 digits into your app

    7973On Android your app can read it automatically.
  5. 5We confirm

    We tell you if it matched

    Verifying…Code matched+92 333 0189315 verified

Because the PIN never travels anywhere, there is no message to intercept, no delivery to wait for and no SMS cost. It works the same on any network, in any country we can call.

Two small rules that save you trouble

Wait 30–40 seconds for the call to arrive before giving up. If nothing arrives, stop the request with "pin": "HANGUP" (or cancel.php) and offer another method, such as SMS OTP.

Call the API from your server, never from the mobile app or browser. Your API key must remain secret.

Getting Started

Authentication

One header. The same SENDPK API key your account already uses for the SMS API works here too.

Send your key in the Authorization header

Authorization: YOUR_API_KEY

This form also works:

Authorization: Bearer YOUR_API_KEY

Where to find your key

Log in to your dashboard and open Profile → API Key.

View My API Key

The key is not accepted anywhere else

WhereAccepted?
Authorization headerYES — the only way.
URL / query stringNO — ignored, you get AUTH_MISSING.
JSON bodyNO — ignored, you get AUTH_MISSING.
Username / password loginNO — not supported at all.

HTTPS is required

Always call the API with https://. A request over plain http:// is refused with 403 HTTPS_REQUIRED.

Why: HTTPS protects your API key and request data while they travel between your server and SENDPK. Over plain http anyone on the way could read your key.

If IP restriction is on for your account

  • Your server's IP address must be whitelisted in your SENDPK dashboard (Profile).
  • If your server also connects over IPv6, whitelist that address too.
  • Otherwise you get 403 IP_NOT_ALLOWED, and details.ip shows exactly the IP we saw — copy that value into your whitelist.

Missed Call Verification is switched on for every SENDPK account, so there is nothing to request before you start. If an account is ever turned off (for example after abuse), every start request answers 403 SERVICE_DISABLED — contact our support team to have it enabled again.

API Reference

Base URL & Endpoints

Four endpoints. That is the whole API.

https://sendpk.com/apps/call/
MethodEndpointWhat it does
POST /missed-call-auth.php Start a missed call verification.
POST /verify.php Verify the 4-digit PIN (or stop the call with HANGUP).
GET /status.php?id=REQUEST_ID Check the verification status.
POST /cancel.php Cancel / stop a running verification.

Response format

Every reply has the same shape, so your code can always read it the same way.

SUCCESS

JSON
{
  "success": true,
  "data": {},
  "meta": {
    "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f"
  }
}

ERROR

JSON
{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Calls per minute limit exceeded.",
    "details": { "scope": "account", "limit": 2, "retry_after_seconds": 41 }
  },
  "meta": {
    "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f"
  }
}
FieldMeaning
successTells you if the request worked: true or false. Check this first.
dataThe result, when success is true.
errorThe problem, when success is false: a code, a message and extra details.
data.idThe missed call request ID. Use it for Verify, Status and Cancel.
meta.request_idIdentifies this one HTTP request (also sent back as the X-Request-Id header). It is not the call ID.

Keep request_id when troubleshooting and provide it to SENDPK Support. Every request is written to our log with that ID, so support can find your exact request at once. When details.retry_after_seconds is present, a Retry-After header is sent too.

API Reference

Start Verification POST

https://sendpk.com/apps/call/missed-call-auth.php

Headers

HeaderValue
Authorization requiredYour API key.
Content-Type requiredapplication/json
Idempotency-Key optionalAny unique text, 1–100 visible ASCII characters. See Idempotency.

Body fields

FieldDescription
number required The mobile number. E.164 is recommended: +923330189315. Also accepted: 923330189315, 00923330189315, 03330189315.
platform optional One of ios, android, web, desktop.
notification_callback optional Your webhook URL for this request: a public https address on port 443 or 8443, max 500 characters. If you leave it out, the default URL saved on your account is used. See Webhooks.
end_user_ip optional The public IP address of your app user. Helps fraud protection. Private addresses (10.x, 192.168.x…) are refused.

Example request

cURL
curl https://sendpk.com/apps/call/missed-call-auth.php \
    -H "Content-Type: application/json" \
    -H "Authorization: API_KEY" \
    -H "Idempotency-Key: signup-7f3a9c2e" \
    -d '{
        "number": "+923330189315",
        "platform": "android",
        "notification_callback": "https://example.com/webhooks/missed-call"
    }'

Example response HTTP 200

JSON
{
  "success": true,
  "data": {
    "id": "01a0b338-57cf-766b-9a04-3a88decd1978",
    "status": "pending",
    "pin_hash": "b6e2efd8ffb991dd3ea069b7b9286d5e31f97b36",
    "cli_prefix": "44125",
    "validation_info": {
      "country_code": 92,
      "country_iso_code": "PK",
      "carrier": "Jazz",
      "is_mobile": true,
      "e164_format": "+923001654321",
      "formatting": "+92 300 1654321"
    },
    "expires_at": 1789716809,
    "daily_limit": 100,
    "daily_verified": 1,
    "reused": false
  },
  "meta": { "request_id": "01a0b338-56fa-712e-8d98-1035ab66bb54" }
}
FieldMeaning
idRequest ID. Use it in Verify, Status and Cancel.
statuspending right after the start.
pin_hashFrom the call provider: SHA-1 of the last 3 digits of the calling number. Server-side use only — see the warning below.
cli_prefixThe start of the number that will call. Only present if the provider sends it. Use it to spot the right incoming call in your app.
validation_infoWhat we know about the number: country code, ISO code, carrier, mobile or not, and two tidy formats.
validation_info.carrierThe network the number range was originally given to. A ported number may be on another network now.
expires_atThe PIN can be verified until this time (60 minutes).
daily_limit / daily_verifiedYour daily limit, and how many numbers you verified today (Pakistan time).
reusedtrue when the same number was requested again within 60 seconds: the running request is returned and no new call is made.

Never send pin_hash to the app or browser

pin_hash is the SHA-1 of the last 3 digits of the calling number. Anyone can turn that back into those 3 digits, and then only 10 guesses remain for the PIN. Keep it on your server. To spot the right incoming call inside your app, use cli_prefix instead.

No money is taken when a call starts — see Billing.

API Reference

Verify PIN POST

https://sendpk.com/apps/call/verify.php

What is the PIN?

The PIN is the last 4 digits of the number that called the user.

Caller number +9230001230711
PIN "0711"

Send the PIN as a string: "0711", never the number 711. Leading zeros matter and JSON drops them from plain numbers.

Body fields

FieldDescription
id requiredThe id from the start reply.
pin requiredThe last 4 digits of the calling number, as a string. Or "HANGUP" to stop the call.
use_server_hangup optionalDefault true: the call is ended from our side. Recommended, because some carriers charge when the phone rejects the call.

Example request

cURL
curl https://sendpk.com/apps/call/verify.php \
    -H "Content-Type: application/json" \
    -H "Authorization: API_KEY" \
    -d '{
        "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
        "pin": "9711",
        "use_server_hangup": true
    }'

Correct PIN HTTP 200

JSON
{
  "success": true,
  "data": {
    "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
    "number": "+923330189315",
    "status": "verified",
    "validated": true,
    "validation_date": 1770000042,
    "charged_amount": 0.0,
    "currency": "PKR",
    "billing_status": "pending"
  },
  "meta": { "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e61" }
}

"validated": true means the mobile number has been successfully verified. That is the only field you need to decide "this user owns this number".

Wrong PIN HTTP 422

JSON
{
  "success": false,
  "error": {
    "code": "PIN_INVALID",
    "message": "The verification PIN is invalid.",
    "details": { "attempts_remaining": 2 }
  },
  "meta": { "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e62" }
}

The PIN is wrong. details.attempts_remaining tells you how many tries are left — show that number to your user.

RuleWhat happens
PIN tries3 per request. After the third wrong PIN the request is closed with 429 PIN_ATTEMPTS_EXCEEDED.
Verify again after successYou get the same reply back. It is not counted or charged again.
"pin": "HANGUP"Stops the call and closes the request (status: "cancelled"). It does not use a PIN try.

Stop the call instead of verifying

JSON
{
  "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
  "pin": "HANGUP"
}
API Reference

Check Status GET

https://sendpk.com/apps/call/status.php?id=REQUEST_ID

Example request

cURL
curl "https://sendpk.com/apps/call/status.php?id=0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f" \
    -H "Authorization: API_KEY"

Example response HTTP 200

JSON
{
  "success": true,
  "data": {
    "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
    "number": "+923330189315",
    "status": "verified",
    "validated": true,
    "validation_date": 1770000042,
    "created_at": 1770000000,
    "expires_at": 1770003600,
    "attempts_used": 1,
    "attempts_remaining": 0,
    "charged_amount": 67.28,
    "currency": "PKR",
    "billing_status": "charged",
    "billed_seconds": 60,
    "error_code": null,
    "validation_info": {
      "country_code": 92,
      "country_iso_code": "PK",
      "carrier": "Ufone",
      "is_mobile": true,
      "e164_format": "+923330189315",
      "formatting": "+92 333 0189315"
    },
    "webhook": { "status": "sent", "last_event": "billing.charged", "delivered": 2, "last_http_code": 200 }
  },
  "meta": { "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e63" }
}

charged_amount above is only an example figure. Your real amount depends on your account and the destination — see current rates.

status values

statusMeaning
pendingCall placed, waiting for verification.
verifiedPIN is correct. Final.
failedVerification failed — too many wrong PINs, or the call could not be placed. Final. See error_code.
expiredThe PIN was not verified within 60 minutes. Final.
cancelledVerification was cancelled / stopped. Final.

Important fields

FieldMeaning
statusWhere the request is now (table above).
validatedtrue once the PIN was correct.
created_atWhen the request started (Unix seconds).
expires_atLast moment the PIN can still be verified (60 minutes after the start).
attempts_used / attempts_remainingPIN tries used, and how many are left out of 3.
charged_amountHow much was taken from your balance. Stays 0 until the provider confirms a charge.
currencyAlways PKR.
billing_statuspending, charged, not_charged or short — see Billing.
billed_secondsSeconds the provider billed, when it tells us.
error_codeWhy a failed request failed. null when there is no problem.
validation_infoCountry, carrier and tidy formats of the number.
API Reference

Cancel Verification POST

https://sendpk.com/apps/call/cancel.php

Cancel is simply used to stop a running verification — for example when no call reached the user, or the user closed your app.

Body

JSON
{
  "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f"
}

Example request

cURL
curl https://sendpk.com/apps/call/cancel.php \
    -H "Content-Type: application/json" \
    -H "Authorization: API_KEY" \
    -d '{"id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f"}'

verify.php with "pin": "HANGUP" does exactly the same thing. Use whichever fits your code better. A request that is already verified answers 409 REQUEST_ALREADY_VERIFIED; one that is already closed answers 409 REQUEST_ALREADY_TERMINATED.

API Reference

Billing

You are not charged just because you start a verification.

Starting a call FREE

Placing the missed call costs you nothing.

Checking the number FREE

Status checks cost nothing, as often as you need them.

Verifying the PIN FREE

Checking the 4 digits costs nothing.

So when am I charged?

You are charged only if the call provider confirms that the call was chargeable. Normally this happens when the person answers the short 2–3 second call.

Your actual rate depends on your SENDPK account and the destination, so an expensive country costs more than a call inside Pakistan.

A call is charged at most once. The amount really taken is in charged_amount.

Two things that can trigger a charge you did not expect

The phone rejects the call instead of letting it ring out. Some carriers treat a rejected call as answered in the background and play a message such as "the number you are calling is busy" — and then charge the caller for it. Keep use_server_hangup: true (the default on Verify) so SENDPK ends the call from its own side, and make sure your app never rejects or hangs up the call itself.

The same number is tested too often. Mobile operators run anti-spam and fraud protection that can react to many calls that never properly connect to one number, and may then treat a later attempt as answered even though it was not. While you are integrating, avoid calling the same test number repeatedly in a short time — space your tests out, or use a few different numbers.

billing_status values

billing_statusMeaningcharged_amount
pendingThe provider has not returned the final billing result yet (up to about 10 minutes after the call).0
chargedThe call was charged. The amount was taken from your balance.the amount taken
not_chargedNo charge was made.0
shortThe call was charged, but your balance was lower than the full amount, so the remaining balance was used.less than the amount

Do not treat charged_amount as final while billing_status is pending. Wait for charged, not_charged or short — or let the billing.charged webhook tell you.

API Reference

Safe Retries with Idempotency-Key

Sometimes the internet is slow and you may not know whether your request reached SENDPK. Use the same Idempotency-Key when retrying.

Idempotency-Key: signup-7f3a9c2e
What you sendWhat happens
Same key + same bodyYou get the same request back. No second call. No second charge.
Same key + different body409 IDEMPOTENCY_CONFLICT. Use a new key for a new request.
The first request failedThe retry returns the same error, and no new call is made.

A key is remembered for 24 hours. Make it unique per verification attempt — for example signup- plus your own order or user ID.

API Reference

Limits

These are the default limits per account (all your requests together). SENDPK can change them for you.

LimitDefaultError when you pass it
New calls per minute (any 60 seconds)2429 RATE_LIMIT_EXCEEDED (details.scope = "account")
Calls running at the same time2429 CONCURRENT_LIMIT_REACHED
Verified numbers per day10 – 200, set by your package
read yours from daily_limit
429 DAILY_LIMIT_REACHED
New calls per IP address per minute30429 RATE_LIMIT_EXCEEDED (details.scope = "ip")
Calls to the same number per day10429 NUMBER_DAILY_LIMIT_REACHED
Unverified calls per daydaily verified limit × 3 (at least 30)429 DAILY_ATTEMPTS_LIMIT_REACHED
Failed API key attempts5 per 30 min per IP + key,
20 per 10 min per IP
429 RATE_LIMIT_EXCEEDED (details.scope = "auth")

Good to know

Only verified numbers count Failed, expired and cancelled requests do not use up your daily limit.
Resets at 00:00 Pakistan time The daily verified count starts again at midnight, Pakistan time.
Running calls still work Once the daily limit is reached, new requests are refused, but calls already running can still be verified.
"Running" means Until the request is verified, cancelled or failed — or for 60 seconds after it started.
Same number within 60 seconds You get the running request back (reused: true) and no new call is made.
Every 429 tells you when to retry The reply has details.retry_after_seconds and a Retry-After header. Wait that long, then try again.
Webhooks

Webhooks

Webhooks let SENDPK tell your server when something happens, so you do not always have to keep checking Status.

Set notification_callback in the start request, or ask SENDPK to save a default webhook URL on your account. SENDPK then sends a POST with a JSON body when something happens.

Events

EventWhen it is sent
validation.completedThe number was verified.
validation.failedThe request was closed — for example too many wrong PINs.
validation.expiredThe PIN was not verified within 60 minutes.
validation.cancelledThe call was stopped with HANGUP / cancel.
billing.chargedThe call was charged (billing_status is charged or short).

Your webhook URL must be

RuleDetail
Public httpsPlain http is refused.
Port 443 or 8443No other port is allowed.
Max 500 charactersLonger URLs are refused.
No localhostWe must be able to reach it from the internet.
No private / internal addresses10.x, 192.168.x, 127.x and similar are refused.
No SENDPK domainsA webhook may not point back at SENDPK itself.
Must pass URL validationNo user:password in the URL, and no \ or @ characters. The address is checked again right before every send.

A URL that breaks any of these rules is refused with 422 INVALID_CALLBACK_URL.

Headers we send

Headers
Content-Type: application/json
User-Agent: SENDPK-Webhook/1.0
X-SENDPK-Event: validation.completed
X-SENDPK-Event-Id: 5b1d0e8a-2f3c-5a4b-9c8d-7e6f5a4b3c2d
X-SENDPK-Signature: sha256=<hex HMAC-SHA256 of the raw body>

Body we send

JSON
{
  "event": "validation.completed",
  "event_id": "5b1d0e8a-2f3c-5a4b-9c8d-7e6f5a4b3c2d",
  "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
  "number": "+923330189315",
  "status": "verified",
  "validated": true,
  "validation_date": 1770000042,
  "charged_amount": 0.0,
  "currency": "PKR",
  "billing_status": "pending",
  "timestamp": 1770000043
}

Delivery and retries

Speed Webhooks are sent by a background job, normally within a few seconds of the event.
Your answer Reply with any 2xx status within 5 seconds.
We retry after a timeout, a network error, 408, 429 or any 5xx — first after 1 minute, then after 5 minutes (3 tries in total).
We do not retry other 4xx replies and redirects.
Duplicates The same webhook can arrive more than once. Use event_id to ignore repeats — it stays the same for the same event.
Always current Every webhook carries the current state of the request (status and billing).
Charged calls get two webhooks: first validation.*, then billing.charged.
Webhooks

Webhook Security

The webhook signature proves that the webhook came from SENDPK and has not been changed.

We sign the exact raw body with HMAC-SHA256, using your API key as the secret, and send the result in the X-SENDPK-Signature header. Your server does the same calculation and compares. If the two do not match, the request is not from us — throw it away.

PHP

PHP
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, 'YOUR_API_KEY');

if (!hash_equals($expected, $_SERVER['HTTP_X_SENDPK_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);

Node.js

JavaScript
const crypto = require('crypto');
// rawBody must be the exact bytes received (not re-encoded JSON)
const expected = 'sha256=' + crypto.createHmac('sha256', process.env.SENDPK_API_KEY).update(rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.get('X-SENDPK-Signature') || ''));

Use the raw body, always

Always calculate the signature using the exact raw request body. Do not JSON-decode and re-encode it before calculating the signature — even one changed space makes a completely different signature, and your check will always fail.

If you change your API key, use the new key to check signatures from that moment on.

Platform Guides

Android Guide

On Android your app can read the caller number itself, so verification is automatic: the phone rings, the app ends the call, fills in the PIN and verifies it. The user types nothing.

What your app does

1. Listen Listen for the incoming missed call.
2. Read Read the last 4 digits of the caller number.
3. End the call End the ringing call at once, on the phone. The demo app does this for you.
4. Send Send those digits to your own server, which calls verify.php.
5. Backup hang-up Keep use_server_hangup: true in the verify, so SENDPK also ends the call if the phone could not.

Try it: Android demo app

A small, ready-made app that shows the whole flow. You type a number and your phone starts to ring. The app ends the call at once, takes the last 4 digits of the caller number and verifies them — you do not type anything.

Android 8.0 or newer, with a SIM card. The source code is Kotlin + Jetpack Compose and opens in Android Studio. The ready-to-install APK is coming soon; until then, build it from the source in a few minutes.

Step by step: from zero to "Number verified"

Follow the steps in order. Each step is one small thing. The orange boxes and arrows show you where to look.

  • An Android phone with a SIM card
  • A website that opens with https://
  • Your SENDPK API key
  • A computer with Android Studio (free)
PART A On your server — do this once
1

Download the server file

Click Download Server File (PHP) and unzip it. Inside is one file: sendpk-missed-call-bridge.php. This file keeps your API key safe on your server, so it is never inside the app.

2

Put your API key in the file

Open the file in any text editor (Notepad is fine). Near the top, change the orange line to your own key. You find your key in the SENDPK Dashboard → Profile → API Key.

3

Upload it and check it

Upload the file to your website, like any other PHP file. Then open it in your browser with ?action=ping at the end. If you see "ready": true, your server is ready.

The file has built-in limits, so strangers cannot spend your balance: 5 calls per number per day and 10 new verifications per visitor per hour. Every country works; to allow only some, list their codes in allowed_country_codes. If IP restriction is on for your SENDPK account, whitelist your server's IP addresses (IPv4 and IPv6 — the ping shows the exact one SENDPK saw).

PART B On your phone
4

Put the app on your phone

Download the Source Code (ZIP) and unzip it. In Android Studio click File → Open and choose the android folder. Wait until it stops loading. Connect your phone with a USB cable and press the green Run ▶ button.

5

Tell the app where your server is

1 Type the address of the file you uploaded in step 3 (without ?action=ping). 2 Tap Test connection.

The demo app. Box 1 marks the Server URL field, box 2 marks the Test connection button.
6

Wait for the green words

First it says Checking.... After a second it turns green: Connected. Your server is ready. Green means the app can talk to your server.

The app shows Checking... under the Test connection button.
First: Checking...
The app shows the green text: Connected. Your server is ready.
Then: green = ready
7

Type your mobile number and tap the button

1 Type the number with its country code, like +923001234567. 2 Tap Verify with a missed call. The first time, Android asks for permissions: tap Allow each time. The app needs them to see the caller number and to end the call.

Box 1 marks the mobile number field, box 2 marks the Verify with a missed call button.
8

Your phone rings — do not answer

Just wait. 1 The countdown shows the app is waiting for the call. When the phone rings, the app ends the call at once. 2 Then it puts the last 4 digits into the PIN box by itself.

Box 1 marks the Waiting for the missed call countdown, box 2 marks the PIN box that the app fills in.
9

Done! Your number is verified

1 The green box says Number verified. Look at the list under it to see how it worked: 2 the call came from a number ending in 0237, and 3 those same 4 digits became the PIN. That is the whole trick.

Box 1 marks the green Number verified card. Box 2 marks the caller number ending in 0237, box 3 marks the line PIN 0237 filled in.

How the app keeps your API key safe

Anyone can open an APK file and read what is inside — even hidden or encrypted text. So the demo app has no API key at all. It talks only to one small PHP file on your server, and that file talks to SENDPK.

Android app Knows only your server URL. No API key inside.
Your server sendpk-missed-call-bridge.php keeps your API key and calls the SENDPK API.
SENDPK Places the missed call and checks the PIN.

The app only ever gets these small replies. The SENDPK request id travels inside a sealed ticket that the app cannot read or change, and pin_hash never leaves your server.

What the app receives
// start
{ "ok": true, "ticket": "Qm9x...sealed", "cli_prefix": "12679", "number": "+923001234567", "wait_seconds": 40 }

// verify
{ "ok": true, "verified": true, "number": "+923001234567" }

The important code

Three permissions: two let Android include the caller number, the third lets the app end the call:

AndroidManifest.xml
<uses-permission android:name="android.permission.READ_PHONE_STATE" />
<uses-permission android:name="android.permission.READ_CALL_LOG" />
<uses-permission android:name="android.permission.ANSWER_PHONE_CALLS" />

Read the number while the phone rings:

Kotlin
val receiver = object : BroadcastReceiver() {
    override fun onReceive(context: Context, intent: Intent) {
        if (intent.getStringExtra(TelephonyManager.EXTRA_STATE) != TelephonyManager.EXTRA_STATE_RINGING) return
        val caller = intent.getStringExtra(TelephonyManager.EXTRA_INCOMING_NUMBER) ?: return
        onIncomingNumber(caller)   // e.g. "+12679000711"
    }
}
ContextCompat.registerReceiver(context, receiver,
    IntentFilter(TelephonyManager.ACTION_PHONE_STATE_CHANGED), ContextCompat.RECEIVER_EXPORTED)

Use only the right call: end it at once, then verify its last 4 digits:

Kotlin
fun onIncomingNumber(caller: String) {
    val digits = caller.filter { it.isDigit() }.removePrefix("00")
    if (!digits.startsWith(cliPrefix)) return   // a friend's call: ignore it
    telecomManager.endCall()                     // 1. end the call now (Android 9+, ANSWER_PHONE_CALLS)
    val pin = digits.takeLast(4)                 // "+12679000711" -> "0711"
    verifyOnMyServer(ticket, pin)                // 2. your server calls SENDPK verify.php
}

The demo also reads the call log every 1.5 seconds as a backup, because some phones leave the number out while it rings. See CallWatcher.kt, CallerMatch.kt and VerifyViewModel.kt in the ZIP.

Good to know

If the user says no to the permissions, show a box to type the last 4 digits from the call history — exactly like on iOS. The demo app does this for you.

Ending the call on the phone. The demo ends the call the moment it rings, and only then verifies. On a few networks, mostly outside Pakistan, a call ended on the phone is sent to voicemail, and a voicemail answer can be billed. If you see charges, set HANG_UP_ON_PHONE = false in VerifyViewModel.kt: SENDPK then ends the call right after the verify (use_server_hangup: true).

Publishing on Google Play? Google allows the Call log permission only for some kinds of apps. Check Google Play's SMS and Call Log permissions policy before you publish. The typed-PIN option always works without it.

Platform Guides

Verification of Website / iOS

A website or an iPhone app cannot see who is calling. So the user reads the last 4 digits of the number that called and types them. It is still very simple.

What your page or app does

1. Ask Ask for the mobile number and start the call through your own server.
2. Show a box Show one box: “Type the last 4 digits of the number that called you.”
3. Verify Send the 4 digits to your server, which calls verify.php. Wrong digits? The user can try again (3 tries).

Try it: website demo

A ready-made page in two files: index.html (what your user sees) and sendpk-missed-call-bridge.php (your API key and all settings: rate limits, countries and more). It works on any website with PHP. iPhone apps use the same PHP file.

Step by step: from zero to "Number verified"

Follow the steps in order. The orange boxes and arrows show you where to look.

  • A website that opens with https:// and runs PHP
  • Your SENDPK API key
  • A phone to test with
PART A On your server — do this once
1

Download the website demo

Click Download Website Demo (ZIP) and unzip it. Inside are index.html, sendpk-missed-call-bridge.php and a short README.md.

2

Put your API key in the PHP file

Open sendpk-missed-call-bridge.php in any text editor. All settings are at the top. 1 Paste your key (from the SENDPK Dashboard → Profile → API Key). 2 The rate limits stop strangers from spending your balance — change them only if you need to.

3

Upload both files into one folder

Put the two files in the same folder on your website, for example verify. Then open https://your-domain.com/verify/sendpk-missed-call-bridge.php?action=ping once — "ready": true means the server side works. If not, the problems list says exactly what to fix.

IP restriction on? Add your server's IP address to the whitelist in your SENDPK profile. Many servers have an IPv4 and an IPv6 address — add both. The ping shows the exact address SENDPK saw. Five refused tries block new ones for 30 minutes, so fix this before you test.

Testing? Set 'debug' => true in the PHP file. Every error on the page then also shows SENDPK's exact reply — for example IP_NOT_ALLOWED and the IP it saw — so you know what to fix. Set it back to false before going live. Your API key is never shown.

PART B What your user does
4

Type the number and tap “Call me”

Open https://your-domain.com/verify/index.html. 1 Type the mobile number with its country code. 2 Tap Call me.

5

The phone rings — do not answer

A call comes in a few seconds. Do not answer: it stops by itself. Then look at the number that called — on an iPhone in Phone → Recents, on Android in the call history. 1 Its last 4 digits are the PIN.

6

Type the 4 digits and tap “Verify”

The page helps with an example. 1 Type the last 4 digits. 2 Tap Verify. Typed a wrong digit? The page says so, and the user can try again.

7

Done! The number is verified

1 The page shows Number verified. On your server, on_number_verified() in the PHP file runs at the same moment — put your own code there, for example to save the number to the user's account.

For iPhone apps

Use the same PHP file. Your app sends the same two requests as the web page, and shows one box for the 4 digits. No special permission is needed. Send the PIN as text ("0711"), so a leading 0 is kept.

Swift
let server = "https://your-domain.com/verify/sendpk-missed-call-bridge.php"

func post(_ action: String, _ body: [String: String]) async throws -> [String: Any] {
    var request = URLRequest(url: URL(string: server + "?action=" + action)!)
    request.httpMethod = "POST"
    request.setValue("application/json", forHTTPHeaderField: "Content-Type")
    request.httpBody = try JSONSerialization.data(withJSONObject: body)
    let (data, _) = try await URLSession.shared.data(for: request)
    return try JSONSerialization.jsonObject(with: data) as? [String: Any] ?? [:]
}

// 1. "Call me"
let start = try await post("start", ["number": "+923001234567", "platform": "ios"])
let ticket = start["ticket"] as? String ?? ""

// 2. The user types the last 4 digits of the caller, then taps "Verify"
let result = try await post("verify", ["ticket": ticket, "pin": "0711"])
let verified = result["verified"] as? Bool ?? false

Good to know

Never put your API key in a web page or in JavaScript. Anyone can see a page's code. The key stays in the PHP file on your server.

Only your own website can use the PHP file. Pages on other websites are refused (allowed_origins), so they cannot start calls through it. If bots start using your page, add a CAPTCHA in front of “Call me”.

No call after 40 seconds? The page offers “Start again”, which stops the old call and lets the user try once more. After 3 wrong PINs the user must start again too.

Developer Tools

Code Examples

Ready to paste. Replace YOUR_API_KEY with your own key — and keep it on your server.

All examples are server-side

The JavaScript examples are Node.js (server) code, not browser code. Never run these in a browser or inside a mobile app: your API key would be visible to everyone.

1. Start Verification

cURL
curl https://sendpk.com/apps/call/missed-call-auth.php \
    -H "Content-Type: application/json" \
    -H "Authorization: YOUR_API_KEY" \
    -H "Idempotency-Key: signup-7f3a9c2e" \
    -d '{
        "number": "+923330189315",
        "platform": "android"
    }'
PHP
<?php
// Server-side only. Keep YOUR_API_KEY out of your app and out of git.
$ch = curl_init('https://sendpk.com/apps/call/missed-call-auth.php');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'Authorization: YOUR_API_KEY',
        'Idempotency-Key: signup-7f3a9c2e',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'number'   => '+923330189315',
        'platform' => 'android',
    ]),
]);
$reply = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!empty($reply['success'])) {
    $requestId = $reply['data']['id'];   // save it: you need it to verify
    // Do NOT send $reply['data']['pin_hash'] to your app.
} else {
    error_log('Missed call start failed: ' . $reply['error']['code']);
}
JavaScript
// Node.js 18+ (server-side). Never run this in a browser.
const res = await fetch('https://sendpk.com/apps/call/missed-call-auth.php', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': process.env.SENDPK_API_KEY,
    'Idempotency-Key': 'signup-7f3a9c2e',
  },
  body: JSON.stringify({
    number: '+923330189315',
    platform: 'android',
  }),
});

const reply = await res.json();
if (reply.success) {
  const requestId = reply.data.id;   // save it: you need it to verify
  // Do NOT send reply.data.pin_hash to your app.
} else {
  console.error('Missed call start failed:', reply.error.code);
}
Python
# Server-side only (Python 3, requests).
import os, requests

reply = requests.post(
    'https://sendpk.com/apps/call/missed-call-auth.php',
    headers={
        'Content-Type': 'application/json',
        'Authorization': os.environ['SENDPK_API_KEY'],
        'Idempotency-Key': 'signup-7f3a9c2e',
    },
    json={'number': '+923330189315', 'platform': 'android'},
    timeout=20,
).json()

if reply.get('success'):
    request_id = reply['data']['id']   # save it: you need it to verify
    # Do NOT send reply['data']['pin_hash'] to your app.
else:
    print('Missed call start failed:', reply['error']['code'])

2. Verify PIN

cURL
curl https://sendpk.com/apps/call/verify.php \
    -H "Content-Type: application/json" \
    -H "Authorization: YOUR_API_KEY" \
    -d '{
        "id": "REQUEST_ID",
        "pin": "0711",
        "use_server_hangup": true
    }'
PHP
<?php
// $pin comes from your app as a STRING, e.g. "0711" — keep the leading zero.
$ch = curl_init('https://sendpk.com/apps/call/verify.php');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'Authorization: YOUR_API_KEY',
    ],
    CURLOPT_POSTFIELDS => json_encode([
        'id'                => 'REQUEST_ID',
        'pin'               => $pin,
        'use_server_hangup' => true,
    ]),
]);
$reply = json_decode(curl_exec($ch), true);
curl_close($ch);

if (!empty($reply['data']['validated'])) {
    // The number is verified.
} elseif (($reply['error']['code'] ?? '') === 'PIN_INVALID') {
    $left = $reply['error']['details']['attempts_remaining'];   // show this to the user
}
JavaScript
// Node.js 18+ (server-side). pin is a STRING: '0711', not 711.
const res = await fetch('https://sendpk.com/apps/call/verify.php', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': process.env.SENDPK_API_KEY,
  },
  body: JSON.stringify({
    id: requestId,
    pin: pin,
    use_server_hangup: true,
  }),
});

const reply = await res.json();
if (reply.data && reply.data.validated) {
  // The number is verified.
} else if (reply.error && reply.error.code === 'PIN_INVALID') {
  const left = reply.error.details.attempts_remaining;   // show this to the user
}
Python
# pin is a STRING: '0711', not 711.
reply = requests.post(
    'https://sendpk.com/apps/call/verify.php',
    headers={
        'Content-Type': 'application/json',
        'Authorization': os.environ['SENDPK_API_KEY'],
    },
    json={'id': request_id, 'pin': pin, 'use_server_hangup': True},
    timeout=20,
).json()

if reply.get('data', {}).get('validated'):
    pass   # the number is verified
elif reply.get('error', {}).get('code') == 'PIN_INVALID':
    left = reply['error']['details']['attempts_remaining']   # show this to the user

3. Check Status

cURL
curl "https://sendpk.com/apps/call/status.php?id=REQUEST_ID" \
    -H "Authorization: YOUR_API_KEY"
PHP
<?php
$ch = curl_init('https://sendpk.com/apps/call/status.php?id=' . urlencode($requestId));
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_HTTPHEADER     => ['Authorization: YOUR_API_KEY'],
]);
$reply = json_decode(curl_exec($ch), true);
curl_close($ch);

$status  = $reply['data']['status'] ?? null;            // pending | verified | failed | expired | cancelled
$billing = $reply['data']['billing_status'] ?? null;    // pending | charged | not_charged | short
JavaScript
// Node.js 18+ (server-side).
const res = await fetch('https://sendpk.com/apps/call/status.php?id=' + encodeURIComponent(requestId), {
  headers: { 'Authorization': process.env.SENDPK_API_KEY },
});

const reply = await res.json();
const status = reply.data && reply.data.status;            // pending | verified | failed | expired | cancelled
const billing = reply.data && reply.data.billing_status;   // pending | charged | not_charged | short
Python
reply = requests.get(
    'https://sendpk.com/apps/call/status.php',
    params={'id': request_id},
    headers={'Authorization': os.environ['SENDPK_API_KEY']},
    timeout=20,
).json()

status = reply.get('data', {}).get('status')            # pending | verified | failed | expired | cancelled
billing = reply.get('data', {}).get('billing_status')   # pending | charged | not_charged | short

4. Cancel

cURL
curl https://sendpk.com/apps/call/cancel.php \
    -H "Content-Type: application/json" \
    -H "Authorization: YOUR_API_KEY" \
    -d '{"id": "REQUEST_ID"}'
PHP
<?php
// Call this when no missed call arrived within 30-40 seconds.
$ch = curl_init('https://sendpk.com/apps/call/cancel.php');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_POST           => true,
    CURLOPT_HTTPHEADER     => [
        'Content-Type: application/json',
        'Authorization: YOUR_API_KEY',
    ],
    CURLOPT_POSTFIELDS => json_encode(['id' => $requestId]),
]);
$reply = json_decode(curl_exec($ch), true);
curl_close($ch);
JavaScript
// Node.js 18+ (server-side). Call this when no missed call arrived in 30-40 s.
const res = await fetch('https://sendpk.com/apps/call/cancel.php', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Authorization': process.env.SENDPK_API_KEY,
  },
  body: JSON.stringify({ id: requestId }),
});

const reply = await res.json();
Python
# Call this when no missed call arrived within 30-40 seconds.
reply = requests.post(
    'https://sendpk.com/apps/call/cancel.php',
    headers={
        'Content-Type': 'application/json',
        'Authorization': os.environ['SENDPK_API_KEY'],
    },
    json={'id': request_id},
    timeout=20,
).json()

5. Webhook verification

PHP
<?php
// Your webhook endpoint, e.g. https://example.com/webhooks/missed-call
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, 'YOUR_API_KEY');

if (!hash_equals($expected, $_SERVER['HTTP_X_SENDPK_SIGNATURE'] ?? '')) {
    http_response_code(401);
    exit;
}

$event = json_decode($raw, true);

// The same event can arrive more than once: ignore repeats by event_id.
if (already_handled($event['event_id'])) {
    http_response_code(200);
    exit;
}

if ($event['event'] === 'validation.completed') {
    mark_number_verified($event['number']);
}

http_response_code(200);   // answer 2xx within 5 seconds
JavaScript
// Node.js + Express (server-side).
const crypto = require('crypto');
const express = require('express');
const app = express();

// IMPORTANT: keep the RAW body — do not let a JSON parser re-encode it.
app.post('/webhooks/missed-call', express.raw({ type: 'application/json' }), (req, res) => {
  const rawBody = req.body;   // Buffer with the exact bytes we sent
  const expected = 'sha256=' + crypto.createHmac('sha256', process.env.SENDPK_API_KEY).update(rawBody).digest('hex');
  const given = req.get('X-SENDPK-Signature') || '';

  if (expected.length !== given.length ||
      !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(given))) {
    return res.sendStatus(401);
  }

  const event = JSON.parse(rawBody.toString('utf8'));
  // Use event.event_id to ignore repeats.
  res.sendStatus(200);   // answer 2xx within 5 seconds
});
Python
# Flask (server-side).
import hashlib, hmac, os
from flask import Flask, request

app = Flask(__name__)

@app.post('/webhooks/missed-call')
def missed_call_webhook():
    raw = request.get_data()   # the exact bytes we sent
    expected = 'sha256=' + hmac.new(
        os.environ['SENDPK_API_KEY'].encode(), raw, hashlib.sha256
    ).hexdigest()

    if not hmac.compare_digest(expected, request.headers.get('X-SENDPK-Signature', '')):
        return '', 401

    event = request.get_json()
    # Use event['event_id'] to ignore repeats.
    return '', 200   # answer 2xx within 5 seconds
Developer Tools

Postman Collection

Test the whole API without writing a single line of code.

SENDPK Missed Call Verification

Import this collection into Postman, enter your API key, and start testing. Every endpoint is pre-built, and "Start Verification" automatically saves the returned id into the request_id variable, so Verify, Status and Cancel work straight away.

Download Postman Collection

What is inside

  • 1. Authentication / Setup — check your API key works
  • 2. Start Verification
  • 3. Verify PIN (plus a HANGUP request)
  • 4. Check Status
  • 5. Cancel Verification
  • 6. Webhooks / Examples — send a signed test webhook to your own server

Collection variables

  • base_url — https://sendpk.com/apps/call
  • api_key — your own API key
  • request_id — filled in for you after a start
  • phone_number — the number to verify
  • pin — the last 4 digits you received
  • webhook_url — your own webhook endpoint

How to import

In Postman click Import, choose the downloaded .json file, open the collection's Variables tab, paste your api_key, then run 1. Authentication / Setup first. Your API key is secret — do not share the collection after you fill it in.

Reference

Error Codes

Every error reply has a code you can switch on in your program. The message is for humans; the code is for your code.

HTTPCodeMeaning
400INVALID_JSONBody is not a valid JSON object.
400INVALID_IDEMPOTENCY_KEYIdempotency-Key is empty, too long or has spaces.
400BAD_REQUESTThe request could not be understood.
401AUTH_MISSINGNo API key in the Authorization header.
401AUTH_INVALIDWrong API key.
401API_KEY_REVOKEDYour API key must be changed in the dashboard.
402INSUFFICIENT_BALANCEYour PKR balance is below the minimum needed to start a call (details.minimum_balance).
403ACCOUNT_DISABLEDAccount blocked.
403IP_NOT_ALLOWEDYour server IP is not whitelisted. details.ip shows the IP we saw.
403SERVICE_DISABLEDMissed call service is not switched on for your account.
403HTTPS_REQUIREDThe request used plain http. Use https://.
403FORBIDDENAccess not allowed.
404NOT_FOUNDWrong URL.
404REQUEST_NOT_FOUNDNo request with this id on your account.
405METHOD_NOT_ALLOWEDWrong HTTP method (for example GET instead of POST).
409IDEMPOTENCY_CONFLICTSame Idempotency-Key used with a different body.
409REQUEST_IN_PROGRESSThe request is still starting. Retry in a few seconds.
409REQUEST_EXPIREDThe 60 minutes are over. Start a new request.
409REQUEST_ALREADY_VERIFIEDCancel was called on a request that is already verified.
409REQUEST_ALREADY_TERMINATEDThe request is already closed.
413PAYLOAD_TOO_LARGEBody larger than 8 KB.
415INVALID_CONTENT_TYPEContent-Type must be application/json.
422MISSING_NUMBERnumber is missing.
422INVALID_NUMBERNot a valid phone number.
422NUMBER_NOT_MOBILENot a mobile number.
422COUNTRY_NOT_SUPPORTEDMissed call is not available for this country.
422NUMBER_BLOCKEDThis number cannot receive missed call verification.
422INVALID_PLATFORMplatform is not ios / android / web / desktop.
422INVALID_CALLBACK_URLWebhook URL is not allowed — see the rules in Webhooks.
422INVALID_END_USER_IPend_user_ip is not a public IP address.
422INVALID_FIELDA field has a wrong type (see details.field).
422MISSING_ID / INVALID_IDid missing, or not a valid request id.
422PIN_REQUIRED / INVALID_PIN_FORMATpin missing, or not 4 digits / HANGUP.
422PIN_INVALIDWrong PIN. details.attempts_remaining shows the tries left.
429PIN_ATTEMPTS_EXCEEDED3 wrong PINs. The request is closed.
429RATE_LIMIT_EXCEEDEDToo many calls per minute (see details.scope).
429CONCURRENT_LIMIT_REACHEDToo many calls running at the same time.
429DAILY_LIMIT_REACHEDDaily verified limit reached.
429NUMBER_DAILY_LIMIT_REACHEDToo many calls to this number today.
429DAILY_ATTEMPTS_LIMIT_REACHEDToo many unverified calls today.
429NUMBER_TEMPORARILY_BLOCKEDToo many attempts for this number. Try later.
500INTERNAL_ERROR / BILLING_ERROROur error. Retry later.
502PROVIDER_REJECTEDThe call provider refused the request.
502PROVIDER_INVALID_RESPONSEThe call provider sent a bad reply.
503PROVIDER_UNAVAILABLECall service temporarily unavailable.
503SERVICE_BUSY / SERVICE_UNAVAILABLEBusy or down for a moment. Retry after retry_after_seconds.
504PROVIDER_TIMEOUTThe provider did not answer in time. The call may still ring: check Status, or retry with the same Idempotency-Key.
504PROVIDER_UNKNOWNThe call could not be confirmed. Start a new request.
Reference

Frequently Asked Questions

What is Missed Call Verification?
It proves that the person really owns a mobile number. SENDPK rings the number for 2-3 seconds. The last 4 digits of the number that called become the PIN. If the user sends those 4 digits back, the number is verified.
Do I need an SMS?
No. Nothing is sent to the phone. The phone only receives a short call, and the calling number itself is the code.
What is the verification PIN?
The last 4 digits of the number that called the user. If the call came from +9230001230711, the PIN is 0711.
Who generates the PIN?
SENDPK and the call provider. You never create it. You only send the 4 digits back for checking.
Can I use this worldwide?
It is built for worldwide mobile verification, but coverage depends on the destination and the call provider. If a country is not available you get COUNTRY_NOT_SUPPORTED, and nothing is charged.
When am I charged?
Only when the call provider confirms the call was chargeable. That normally happens when the person answers the short call.
Do I get charged when the call starts?
No. Starting a call, checking the number and verifying the PIN are free.
What happens if the user does not answer?
That is the normal case, and it is usually not chargeable. The user still gets the PIN from the missed call on the screen.
What happens if no call arrives?
After 30 to 40 seconds send "pin": "HANGUP" (or call cancel.php) to stop the request, then offer another method such as SMS OTP.
How many PIN attempts are allowed?
Three per request. After the third wrong PIN the request is closed with PIN_ATTEMPTS_EXCEEDED. HANGUP does not use a try.
Can I retry a request safely?
Yes. Send the same request again with the same Idempotency-Key. Within 24 hours you get the first request back instead of a second call, so nobody is called twice.
Can I use it from a mobile app?
The app can read or ask for the 4 digits, but the API call itself must come from your own server. Your API key must never be inside an app.
Where should I keep my API key?
On your server only: in an environment variable or a config file outside your public folder. Never in an APK, an iOS app, browser JavaScript or a public GitHub repository.
Can I use webhooks?
Yes. Set notification_callback in the start request, or ask SENDPK to save a default webhook URL on your account. We then tell your server when a request is verified, failed, expired, cancelled or charged.
Why did I receive COUNTRY_NOT_SUPPORTED?
Missed call verification is not available for that country right now. Use another verification method for those users.
Why did I receive IP_NOT_ALLOWED?
IP restriction is on for your account and the IP we saw is not on your list. The reply shows that IP in details.ip. Add it in your dashboard Profile page. If your server also uses IPv6, add that address too.
Reference

Best Practices

A short checklist. Follow it and most problems never happen.

  • Always use HTTPS.
  • Keep your API key on your server.
  • Never put the API key inside Android, iOS or browser code.
  • Use E.164 numbers where possible: +923330189315.
  • Treat the PIN as a string: "0711", not 711.
  • Use an Idempotency-Key for safe retries.
  • Use use_server_hangup: true where appropriate.
  • Do not expose pin_hash to the client.
  • Verify webhook signatures on every webhook.
  • Use event_id to prevent duplicate processing.
  • Save request_id for troubleshooting.
  • Respect the rate limits and the Retry-After header.
  • Check Status when the provider response is delayed.
Reference

Support

Need help integrating?

Our team can help you switch the service on for your account, whitelist your server IP, or work out why a verification did not go through. Keep your request_id ready — it lets us find your exact request straight away.

Looking for our SMS & WhatsApp API instead? See the SMS API documentation.