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 usually read the caller number itself, so verification can feel automatic.

What your app does

1. Listen Listen for the incoming missed call.
2. Read Read the last 4 digits of the caller number.
3. Send Send those digits to your own server, which calls verify.php.
4. Do not reject Do not reject the call from the phone if possible — some carriers charge for a rejected call.
5. Prefer server hangup Keep use_server_hangup: true so SENDPK ends the call from our side.
COMING SOON

Platform-specific integration instructions for Android will be added here.

Platform Guides

iOS Guide

On iOS the user types the 4 digits, because apps cannot read the caller number.

What your app does

1. Explain iOS apps generally cannot directly read the caller number.
2. Ask Ask the user to read the last 4 digits from the phone's call history.
3. Send Send those digits to your own server, which calls verify.php.
COMING SOON

Platform-specific integration instructions for iOS will be added here.

Platform Guides

Desktop Guide

For desktop and web apps, the rule is simple: the API call stays on your server.

What your app does

1. Server-side only The actual API request must remain server-side.
2. Never expose the key Browser and client-side apps must never expose the API key.
3. User types the digits Show a small 4-digit box, then send the digits from your page to your own server, which calls verify.php.
COMING SOON

Platform-specific integration instructions for desktop and web will be added here.

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_urlhttps://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.