PTA-Approved SMS Aggregator · Serving Pakistan since 2014

Voice Call API

Voice Call API

Call a mobile number and play your approved voice recording. Optionally, let the person press keys — and get the answer back.

Use it for order confirmations, payment reminders, appointment reminders, alerts and simple “press 1 to confirm, press 2 to cancel” questions. You choose the recording and the number. SENDPK places the call, plays the recording, and tells you the result: answered or not, how long, which keys were pressed, and what it cost.

3endpoints for you
30 spulse billing, by recording length
DTMFup to 10 key presses
Refundwhen nobody answers (default)

API Base URL

https://sendpk.com/apps/phone_calls/

Every reply is JSON, including errors. Times are Unix timestamps in seconds. Money is in PKR.

Quick Start

Six steps from nothing to your first call.

1

Ask SENDPK to switch on voice calls for your account.

2

Copy your API key from Profile in your dashboard.

3

Get your recording approved. Check it with templates.php

4

Send the number and template_id to call.php

5

SENDPK calls and plays the recording.

6

Get the result by webhook or status.php

Approved recordings only

A call plays one of your approved recordings. Approval is checked again on every call. Text-to-speech is never used.

Optional key presses

Collect 1–10 keys after the recording, choose which keys count, and get them back in the result.

Fair, simple price

Pulses of 30 seconds of the recording. Not answered? The money goes back (default).

Important

Keep your API key on your server

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

  • Never inside an Android or iOS app
  • Never in browser JavaScript
  • Never in a public Git repository
Getting Started

How It Works

One request from you. One result back. Everything in between is done by SENDPK.

  1. Before

    Recording approved

    Your recording is approved and has an id. GET templates.php

  2. Your server

    You ask for a call

    Number + template_id to POST call.php. The price is taken now.

  3. SENDPK

    The phone rings

    We call the number. You get call.initiated.

  4. The person

    Listens & presses keys

    The recording plays. If you asked for keys, they press them.

  5. Your server

    You get the result

    call.completed or call.failed, with keys and the final charge.

What happens to one call

There are no separate “ringing” or “answered” events. The times ringing_at, answered_at and ended_at arrive together with the final result.

Getting Started

Integration Guide

Follow these steps in order. Most people finish in under an hour.

1

Get voice calls switched on

Contact SENDPK support. We switch voice calls on for your account and set your price per pulse. Your account must be verified. Until then you get 403 SERVICE_DISABLED.

2

Get your API key and whitelist your server

Copy the key from Profile → API Key. If IP restriction is on, add your server IP there.

3

Check your key and your recordings

Call GET templates.php. It is free and places no call. Note the id of a recording with "status": "approved".

4

Build your webhook endpoint

An https URL on your server that checks the signature and answers 200 within 5 seconds. Skip this if you prefer to poll status.php.

5

Call your own phone

Send one POST call.php to your own number, with an Idempotency-Key. Use the Request Builder to make the code. Save data.id.

6

Handle the final result

Read status, outcome, dtmf.digits and charge from call.completed / call.failed (or from status.php).

7

Go live safely

Retry only with the same Idempotency-Key, respect Retry-After, skip repeated event_ids, and log meta.request_id. See Best Practices.

Getting Started

Authentication

One header. Your normal SENDPK API key (the same key as 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 — refused as an unknown field.
Username / passwordNO — not supported.

What we check, in this order

CheckIf it fails
HTTPS403 HTTPS_REQUIRED
Too many wrong keys from your IP429 RATE_LIMIT_EXCEEDED (details.scope = "auth", wait 30 minutes)
Key is right401 AUTH_MISSING / 401 AUTH_INVALID
Account not blocked403 ACCOUNT_DISABLED
Key not marked “must change”401 API_KEY_REVOKED
IP whitelist (if on)403 IP_NOT_ALLOWED — details.ip shows the IP we saw
Voice calls switched on403 SERVICE_DISABLED
Account verified403 ACCOUNT_NOT_VERIFIED

HTTPS is required

Always use https://. A plain http:// request is refused with 403 HTTPS_REQUIRED, because over http anyone on the way could read your key.

API Reference

Base URL & Endpoints

Three endpoints. That is the whole API.

https://sendpk.com/apps/phone_calls/
MethodEndpointWhat it doesCost
GET /templates.php List your voice recordings (or one with ?id=) and their approval status. FREE
POST /call.php Place one call that plays a recording, with optional key presses. pulses × price
GET /status.php?id=CALL_ID The current state of one call: result, keys, times, charge, webhook delivery. FREE

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-0a1b2c3d4e60" }
}

ERROR

JSON
{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Calls per minute limit exceeded.",
    "details": { "scope": "account", "limit": 30, "retry_after_seconds": 41 }
  },
  "meta": { "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e60" }
}
FieldMeaning
successtrue or false. Check this first.
dataThe result, when success is true.
errorThe problem: a code for your program, a message for humans, and details.
meta.request_idThe id of this one HTTP request (also in the X-Request-Id header). Not the call id.

Save request_id and give it to SENDPK support when something goes wrong — we can find your exact request with it. When details.retry_after_seconds is present, a Retry-After header is sent too.

API Reference

Voice Templates GET

https://sendpk.com/apps/phone_calls/templates.php

A voice template is one of your recordings. Only approved recordings can be played. This endpoint is read-only: it shows what you have and whether each one can be used.

RequestReturns
GET templates.phpAll your recordings, newest first: data.templates + data.count.
GET templates.php?id=3301118One recording: data.template. Not yours or not found = 404 TEMPLATE_NOT_FOUND.

Example request

cURL
curl -s https://sendpk.com/apps/phone_calls/templates.php \
  -H "Authorization: YOUR_API_KEY"
PHP
<?php
$ch = curl_init('https://sendpk.com/apps/phone_calls/templates.php');
curl_setopt_array($ch, [
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 15,
    CURLOPT_HTTPHEADER     => ['Authorization: ' . getenv('SENDPK_API_KEY')],
]);
$reply = json_decode((string) curl_exec($ch), true);

foreach ($reply['data']['templates'] ?? [] as $t) {
    if ($t['usable']) {
        echo $t['id'] . ' - ' . $t['title'] . ' (' . $t['duration'] . " s)\n";
    }
}
Node.js
// Node.js 18+ (server-side)
const res = await fetch('https://sendpk.com/apps/phone_calls/templates.php', {
  headers: { 'Authorization': process.env.SENDPK_API_KEY },
});
const reply = await res.json();
const usable = (reply.data?.templates ?? []).filter(t => t.usable);
console.log(usable.map(t => `${t.id} - ${t.title} (${t.duration} s)`));
Python
import os
import requests

res = requests.get(
    "https://sendpk.com/apps/phone_calls/templates.php",
    headers={"Authorization": os.environ["SENDPK_API_KEY"]},
    timeout=15,
)
for t in res.json().get("data", {}).get("templates", []):
    if t["usable"]:
        print(t["id"], "-", t["title"], f'({t["duration"]} s)')

Example response HTTP 200

JSON
{
  "success": true,
  "data": {
    "templates": [
      {
        "id": 3301120,
        "title": "Eid offer",
        "duration": 35,
        "status": "rejected",
        "usable": false,
        "rejection_reason": "Background music too loud",
        "created_on": "2026-10-05"
      },
      {
        "id": 3301118,
        "title": "Order confirmation",
        "duration": 50,
        "status": "approved",
        "usable": true,
        "rejection_reason": null,
        "created_on": "2026-10-01"
      }
    ],
    "count": 2
  },
  "meta": { "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e61" }
}
FieldMeaning
idUse this as template_id in call.php.
titleThe name of the recording.
durationLength of the recording in seconds. This decides the price (see Billing).
statusapproved (can be played), pending (waiting for SENDPK), rejected.
usabletrue only for approved.
rejection_reasonWhy SENDPK rejected it (only for rejected), else null.
created_onDate the recording was added (YYYY-MM-DD), or null.

Good to know

Recordings are reviewed by SENDPK. To add a new recording, contact SENDPK support. It shows here as pending until it is reviewed.

Approval is checked on every call. If a recording stops being approved, the next call with it is refused (TEMPLATE_PENDING, TEMPLATE_REJECTED or TEMPLATE_NOT_APPROVED) and nothing is charged.

Maximum length: 240 seconds (8 pulses). You only ever see your own recordings.

API Reference

Place a Call POST

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

Headers

HeaderValue
Authorization requiredYour API key.
Content-Type requiredapplication/json
Idempotency-Key optionalAny unique text, 1–100 visible ASCII characters, no spaces. Strongly recommended — see Safe Retries.

Body fields (JSON object, max 8 KB)

FieldTypeDescription
number required string The Pakistani mobile number to call. Best: +923001234567. Also accepted: 923001234567, 00923001234567, 03001234567, 3001234567. Spaces, dashes and brackets are ignored.
template_id required* integer The id of one of your approved recordings (from templates.php).
audio_url free form only string Your own https .wav / .mp3 link, instead of template_id. Only for accounts SENDPK switched to free form. See Own Audio Link.
dtmf optional object | boolean Collect key presses. true = defaults, or an object. Leave out (or false) for no keys. See Key Presses.
notification_callback optional string Your webhook URL for this call: a public https address on port 443 or 8443, max 500 characters. See Webhooks.

* Send one of template_id or audio_url. Both = 422 CONFLICTING_FIELDS. Any other field = 422 UNKNOWN_FIELD. Text-to-speech and audio fields (text, tts, url, recording…) = 422 AUDIO_NOT_ALLOWED.

Example request

cURL
curl -s -X POST https://sendpk.com/apps/phone_calls/call.php \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-58213-call-1" \
  -d '{
        "number": "+923001234567",
        "template_id": 3301118,
        "dtmf": {"max_digits": 1, "timeout": 10, "valid_digits": "12"},
        "notification_callback": "https://example.com/webhooks/voice-call"
      }'
PHP
<?php
$ch = curl_init('https://sendpk.com/apps/phone_calls/call.php');
curl_setopt_array($ch, [
    CURLOPT_POST           => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT        => 30,
    CURLOPT_HTTPHEADER     => [
        'Authorization: ' . getenv('SENDPK_API_KEY'),
        'Content-Type: application/json',
        'Idempotency-Key: order-58213-call-1',
    ],
    CURLOPT_POSTFIELDS     => json_encode([
        'number'                => '+923001234567',
        'template_id'           => 3301118,
        'dtmf'                  => ['max_digits' => 1, 'timeout' => 10, 'valid_digits' => '12'],
        'notification_callback' => 'https://example.com/webhooks/voice-call',
    ]),
]);
$reply = json_decode((string) curl_exec($ch), true);
$callId = $reply['data']['id'] ?? null;   // save it
Node.js
// Node.js 18+ (server-side)
const res = await fetch('https://sendpk.com/apps/phone_calls/call.php', {
  method: 'POST',
  headers: {
    'Authorization': process.env.SENDPK_API_KEY,
    'Content-Type': 'application/json',
    'Idempotency-Key': 'order-58213-call-1',
  },
  body: JSON.stringify({
    number: '+923001234567',
    template_id: 3301118,
    dtmf: { max_digits: 1, timeout: 10, valid_digits: '12' },
    notification_callback: 'https://example.com/webhooks/voice-call',
  }),
});
const reply = await res.json();
const callId = reply.success ? reply.data.id : null; // save it
Python
import os
import requests

res = requests.post(
    "https://sendpk.com/apps/phone_calls/call.php",
    headers={
        "Authorization": os.environ["SENDPK_API_KEY"],
        "Idempotency-Key": "order-58213-call-1",
    },
    json={
        "number": "+923001234567",
        "template_id": 3301118,
        "dtmf": {"max_digits": 1, "timeout": 10, "valid_digits": "12"},
        "notification_callback": "https://example.com/webhooks/voice-call",
    },
    timeout=30,
)
reply = res.json()
call_id = reply["data"]["id"] if reply["success"] else None  # save it

Example response HTTP 200

JSON
{
  "success": true,
  "data": {
    "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
    "status": "initiated",
    "outcome": null,
    "number": "+923001234567",
    "template": { "id": 3301118, "title": "Order confirmation", "duration": 50 },
    "audio_url": null,
    "recording_seconds": 50,
    "dtmf": { "enabled": true, "max_digits": 1, "timeout": 10, "valid_digits": "12", "digits": null, "valid": null },
    "created_at": 1770000000,
    "ringing_at": null,
    "answered_at": null,
    "ended_at": null,
    "duration": null,
    "charge": { "status": "charged", "final": false, "price": 6.0, "pulses": 2, "price_per_pulse": 3.0, "paid": 6.0, "refunded": 0.0, "currency": "PKR" },
    "error_code": null,
    "reused": false
  },
  "meta": { "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e62" }
}

PKR 3.00 per pulse is only an example. Your real price per pulse is in charge.price_per_pulse.

ReplyMeaningWhat you do
200 initiatedThe call is accepted and will ring now. The full price is taken (charge.final = false).Save data.id. Wait for the webhook.
202 pendingWe sent the call but did not get a confirmation in time.Do not send it again (unless with the same Idempotency-Key). We check it ourselves within a few minutes.
200 reused: trueAn earlier call came back: same Idempotency-Key, or the same request to a number that is still being called.Nothing. No second call, no second charge.
4xxRefused. No call was made and nothing was charged.Fix the request (see Error Codes).
5xx / no replySomething failed on our side or on the network.Retry with the same Idempotency-Key.

What we check before the phone rings

In this order. The first problem found is the error you get — and nothing is charged:

  1. The body: only known fields, a valid number, template_id / audio_url, dtmf, notification_callback, Idempotency-Key.
  2. Is this a repeat? (same Idempotency-Key, or the same number still being called).
  3. Your limits: per minute, at the same time, per day, per number.
  4. The recording: yours, approved, not longer than 240 seconds, file reachable.
  5. Recording + key-press wait fits in one call.
  6. Balance: one of your PKR packages can pay the full price.
API Reference

Key Presses (DTMF)

DTMF (the tones a phone makes when you press a key) lets the person answer your recording — for example “press 1 to confirm, press 2 to cancel”.

JSON (part of the body)
"dtmf": { "max_digits": 1, "timeout": 10, "valid_digits": "12" }
FieldDefaultAllowedMeaning
max_digits11–10How many keys to collect.
timeout101–30 secondsHow long to wait for keys after the recording ends.
valid_digitsany keyup to 11 of 0–9 and *The keys that count as a valid answer, e.g. "12".

Rules

"dtmf": true Uses the defaults (1 key, 10 seconds, any key).
No dtmf / false No keys are collected. The reply shows "dtmf": {"enabled": false}.
# ends the input So # cannot be one of the valid_digits.
Sorted for you valid_digits is saved without repeats, in order: "21" is shown back as "12".
Length limit Recording length + timeout must be 245 seconds or less, else 422 INVALID_DTMF. Example: a 240 s recording allows a timeout of up to 5 s.
Free The key-press wait does not change the price.

Reading the answer

dtmf.digitsdtmf.validMeaning
nullnullThe call is still running, or the keys could not be read.
""falseNothing was pressed.
"1"trueThe person pressed 1, which is in valid_digits.
"7"falseA key outside valid_digits was pressed.

valid is true only when at least one key was pressed and every key was in valid_digits (any key counts when you did not set valid_digits). A # at the end is ignored.

API Reference

Call Status GET

https://sendpk.com/apps/phone_calls/status.php?id=CALL_ID

Example request

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

The reply has the same fields as the call.php reply (without reused), plus webhook. Pick an example:

JSON
{
  "success": true,
  "data": {
    "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
    "status": "completed",
    "outcome": "answered",
    "number": "+923001234567",
    "template": { "id": 3301118, "title": "Order confirmation", "duration": 50 },
    "audio_url": null,
    "recording_seconds": 50,
    "dtmf": { "enabled": true, "max_digits": 1, "timeout": 10, "valid_digits": "12", "digits": "1", "valid": true },
    "created_at": 1770000000,
    "ringing_at": 1770000001,
    "answered_at": 1770000005,
    "ended_at": 1770000062,
    "duration": 57,
    "charge": { "status": "charged", "final": true, "price": 6.0, "pulses": 2, "price_per_pulse": 3.0, "paid": 6.0, "refunded": 0.0, "currency": "PKR" },
    "error_code": null,
    "webhook": { "status": "sent", "last_event": "call.completed", "delivered": 2, "last_http_code": 200 }
  },
  "meta": { "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e63" }
}
JSON
{
  "success": true,
  "data": {
    "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
    "status": "failed",
    "outcome": "no_answer",
    "number": "+923001234567",
    "template": { "id": 3301118, "title": "Order confirmation", "duration": 19 },
    "audio_url": null,
    "recording_seconds": 19,
    "dtmf": { "enabled": false },
    "created_at": 1770000000,
    "ringing_at": 1770000001,
    "answered_at": null,
    "ended_at": null,
    "duration": 0,
    "charge": { "status": "refunded", "final": true, "price": 3.0, "pulses": 1, "price_per_pulse": 3.0, "paid": 0.0, "refunded": 3.0, "currency": "PKR" },
    "error_code": null,
    "webhook": { "status": "sent", "last_event": "call.failed", "delivered": 2, "last_http_code": 200 }
  },
  "meta": { "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e64" }
}
JSON
{
  "success": true,
  "data": {
    "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
    "status": "initiated",
    "outcome": null,
    "number": "+923001234567",
    "template": { "id": 3301118, "title": "Order confirmation", "duration": 50 },
    "audio_url": null,
    "recording_seconds": 50,
    "dtmf": { "enabled": true, "max_digits": 1, "timeout": 10, "valid_digits": "12", "digits": null, "valid": null },
    "created_at": 1770000000,
    "ringing_at": null,
    "answered_at": null,
    "ended_at": null,
    "duration": null,
    "charge": { "status": "charged", "final": false, "price": 6.0, "pulses": 2, "price_per_pulse": 3.0, "paid": 6.0, "refunded": 0.0, "currency": "PKR" },
    "error_code": null,
    "webhook": { "status": "sent", "last_event": "call.initiated", "delivered": 1, "last_http_code": 200 }
  },
  "meta": { "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e65" }
}
JSON
{
  "success": true,
  "data": {
    "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
    "status": "failed",
    "outcome": "unknown",
    "number": "+923001234567",
    "template": { "id": 3301118, "title": "Order confirmation", "duration": 50 },
    "audio_url": null,
    "recording_seconds": 50,
    "dtmf": { "enabled": false },
    "created_at": 1770000000,
    "ringing_at": null,
    "answered_at": null,
    "ended_at": null,
    "duration": null,
    "charge": { "status": "refunded", "final": true, "price": 6.0, "pulses": 2, "price_per_pulse": 3.0, "paid": 0.0, "refunded": 6.0, "currency": "PKR" },
    "error_code": "PROVIDER_UNKNOWN",
    "webhook": { "status": "none", "last_event": "call.failed", "delivered": 0, "last_http_code": null }
  },
  "meta": { "request_id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e66" }
}
FieldMeaning
idThe call id.
statuspending, initiated, completed or failed. See Statuses & Outcomes.
outcomeWhat happened (answered, no_answer, busy…). null until the call is final.
numberThe number called, in +92… form.
templateThe recording played (id, title, duration), or null for an audio link.
audio_urlYour own link (free form), or null for an approved recording.
recording_secondsLength of the recording. This decides the pulses.
dtmfKey-press settings and the answer. See Key Presses.
created_atWhen you submitted the call.
ringing_at / answered_at / ended_atWhen it rang, was answered, ended. null until the result arrives (or if it did not happen).
durationSeconds the call was connected (final calls only). For information — it does not change the price.
chargeThe money. See Billing.
error_codePROVIDER_UNKNOWN (we could not confirm the call was placed) or REPORT_MISSING (no result in 48 hours). Both free. Otherwise null.
webhook.statusnone (no webhook URL), pending, sent or failed.
webhook.last_event / delivered / last_http_codeThe last event, how many events reached you, and your server's last HTTP answer.

You only see your own calls: another account's id gives 404 CALL_NOT_FOUND. Status is read from SENDPK's records, so polling is fine — but not more than once every few seconds. Webhooks are better.

API Reference

Statuses & Outcomes

status tells you where the call is. outcome tells you what happened.

status

statusMeaningFinal?
pendingSent, waiting for confirmation (only after a 202 reply).no
initiatedAccepted. Ringing or playing. The result comes when the call ends.no
completedAnswered: the recording was played.yes
failedNot answered, or could not be placed. See outcome and error_code.yes

outcome and what happens to the money

outcomeMeaningNormal account“Keep unanswered” account
answeredA person answered; the recording was played.keptkept
answered_machineVoicemail / answering machine picked up; the recording was played.keptkept
no_answerNobody answered.given backkept
busyThe line was busy.given backkept
rejectedThe call was declined.given backkept
unreachablePhone off or out of coverage.given backkept
invalid_numberThe number does not exist.given backkept
not_answeredNot answered (no more detail available).given backkept
expiredCould not be placed within 10 minutes.given backkept
network_errorA problem on the phone network.given backkept
unknownThe network gave no clear result.given backkept
unknown + error_codePROVIDER_UNKNOWN: we could not confirm the call was placed. REPORT_MISSING: no result in 48 hours.always given backalways given back
recording_errorThe recording could not be played. For an approved recording SENDPK is alerted; for your own link, check the file.always given backalways given back
service_errorA problem on our side.always given backalways given back

Every account is a normal account unless SENDPK has set it to keep charges for unanswered calls. A call the network refuses at once is never a call: you get an error reply (e.g. 502 PROVIDER_REJECTED) and the money goes straight back.

A late result can still change a failed call

Rarely, the real result arrives after we closed a call as PROVIDER_UNKNOWN or REPORT_MISSING. The call is then updated with the real result. If it was answered, the price is taken again and you get a call.completed webhook after the earlier call.failed. Always trust the newest status.

API Reference

Billing

You pay for 30-second pulses of the recording. Every started pulse counts.

Pulses = recording ÷ 30 s

Rounded up. Price = pulses × your price per pulse.

Taken when you submit

The full price comes from one PKR package that can pay all of it.

Back if not answered DEFAULT

Answered (or voicemail) = kept. Otherwise given back.

Examples at PKR 3.00 per pulse

RecordingPulsesPrice
20 s 1 PKR 3.00 any length up to 30 s is one pulse
30 s 1 PKR 3.00 exactly one pulse
50 s 2 PKR 6.00 the 2nd pulse started
75 s 3 PKR 9.00 1 min 15 s

PKR 3.00 is only an example. Your price per pulse is set on your package by SENDPK. Try your own numbers in the Price Calculator.

What does not change the price

How long the person listens Hang up after 5 seconds or listen to the end: same price.
The key-press wait The dtmf.timeout seconds are free.
Ringing time Not charged.
Safety cap A call is never charged more than PKR 1,000.

The charge object

FieldMeaning
statusSee the table below.
finalfalse while the call runs (the money may still come back); true once it is final.
pricePulses × price per pulse, worked out at submission.
pulses / price_per_pulseThe two numbers behind price.
paidWhat you pay right now (the price, or 0 once given back).
refundedWhat was given back to your balance.
currencyAlways PKR.

charge.status

statusMeaning
chargedThe money is taken — while the call runs, and afterwards if it is kept.
refundedThe money was given back to your balance (to the package it came from).
shortRare: a late result needed the money again, but your balance was too low, so what was left was taken.
refund_pendingRare: we could not give it back automatically (no PKR package left). SENDPK support will do it.

One package must pay all of it

The full price must fit in one of your PKR voice packages. If none can pay it you get 402 INSUFFICIENT_BALANCE with details.required (the price) and details.available (your biggest single package), and no call is made. Do not treat paid as final while charge.final is false.

API Reference

Safe Retries with Idempotency-Key

If your request timed out, you do not know if the phone is already ringing. Send it again with the same Idempotency-Key and nobody is called twice.

Idempotency-Key: order-58213-call-1
What you sendWhat happens
Same key + same body (within 24 h)The same call comes back (reused: true). No second call, no second charge.
Same key + different body409 IDEMPOTENCY_CONFLICT. Use a new key for a new call.
The first try was refused by the call networkThe retry gets the same error (e.g. 503 PROVIDER_UNAVAILABLE) and no new call is made. Use a new key to try again.

The simple rule

No reply, or a 5xx Retry with the same key.
Any 4xx Fix the request if needed, then use a new key.
Make keys unique per call For example order-58213-call-1. Keys are per account.

Same number still being called

While a call to a number is pending / initiated (up to 2 minutes after it started): the same request again returns that call (reused: true); a different request to that number gets 409 NUMBER_BUSY with details.id = the running call.

API Reference

Limits

Per account, for all your requests together. Need more? Contact SENDPK support.

LimitValueError when you pass it
New calls per minute (any 60 seconds)30429 RATE_LIMIT_EXCEEDED (details.scope = "account")
Calls running at the same time20429 CONCURRENT_LIMIT_REACHED
Calls per day2,000429 DAILY_LIMIT_REACHED
Calls to the same number per day5429 NUMBER_DAILY_LIMIT_REACHED
New calls per IP address per minute (all accounts)60429 RATE_LIMIT_EXCEEDED (details.scope = "ip")
Failed API key attempts5 / 30 min per IP + key,
20 / 10 min per IP
429 RATE_LIMIT_EXCEEDED (details.scope = "auth")
Recording length240 s (8 pulses)422 TEMPLATE_INVALID (audio link: INVALID_AUDIO_URL / too_long)
Recording + key-press timeout245 s422 INVALID_DTMF
Request body8 KB413 PAYLOAD_TOO_LARGE

Good to know

Resets at 00:00 Pakistan time The daily counts start again at midnight, Pakistan time.
Refused calls do not count A call refused with an error does not use up your daily limit.
Every 429 tells you when to retry details.retry_after_seconds and a Retry-After header. Wait that long.
Webhooks

Webhooks

Send notification_callback with the call and SENDPK tells your server what happened — no need to keep checking Status.

Events

EventWhen it is sent
call.initiatedThe call was accepted and will ring.
call.completedThe call was answered and the recording played. The charge is final.
call.failedThe call ended without an answer, or could not be placed. The charge is final.

Order: call.initiated first, then one final event. If call.initiated could not be delivered and the result is already known, you get only the final event (it holds everything). A very short call may also send only the final event. The final event is sent after the money is settled, so its charge is final.

Headers we send

Headers
POST /webhooks/voice-call HTTP/1.1
Content-Type: application/json
User-Agent: SENDPK-Webhook/1.0
X-SENDPK-Event: call.completed
X-SENDPK-Event-Id: 3c2384b7-580f-52fc-b246-aea4f0a32eb5
X-SENDPK-Signature: sha256=<hex HMAC-SHA256 of the raw body>

Body we send

The call fields (same as Status, without webhook), plus event, event_id and timestamp. Shown here with line breaks; the real body is one compact line.

JSON
{
  "event": "call.completed",
  "event_id": "3c2384b7-580f-52fc-b246-aea4f0a32eb5",
  "id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f",
  "status": "completed",
  "outcome": "answered",
  "number": "+923001234567",
  "template": { "id": 3301118, "title": "Order confirmation", "duration": 50 },
  "audio_url": null,
  "recording_seconds": 50,
  "dtmf": { "enabled": true, "max_digits": 1, "timeout": 10, "valid_digits": "12", "digits": "1", "valid": true },
  "created_at": 1770000000,
  "ringing_at": 1770000001,
  "answered_at": 1770000005,
  "ended_at": 1770000062,
  "duration": 57,
  "charge": { "status": "charged", "final": true, "price": 6.0, "pulses": 2, "price_per_pulse": 3.0, "paid": 6.0, "refunded": 0.0, "currency": "PKR" },
  "error_code": null,
  "timestamp": 1770000063
}

Your webhook URL must be

RuleDetail
Public httpsPlain http is refused.
Port 443 or 8443No other port.
Max 500 charactersLonger URLs are refused.
Reachable from the internetNo localhost, no private / internal addresses (10.x, 192.168.x, 127.x…).
Not SENDPKA webhook may not point back at SENDPK's own domains.
Clean URLNo user:password, no \ or @. The address is checked again right before every send.

A URL that breaks a rule is refused with 422 INVALID_CALLBACK_URL, before any call is made.

Delivery and retries

Speed Sent by a background job, normally within a few seconds.
Your answer Any 2xx status within 5 seconds. Do slow work after you answer.
We retry after a timeout, a network error, 408, 429 or 5xx: after 1 min, 5 min, 15 min and 1 hour (5 tries in total).
We do not retry other 4xx replies and redirects.
Duplicates The same event can arrive more than once. event_id stays the same for the same event — use it to skip repeats.
Always current Each webhook carries the call as it is when it is sent, so a late call.initiated can already show the final status. Trust status, not only event.
Delivery state status.php shows webhook.status: none, pending, sent, failed.
Webhooks

Webhook Security

The signature proves the webhook came from SENDPK and was not changed on the way.

We sign the exact raw body with HMAC-SHA256, using your API key as the secret, and send it in X-SENDPK-Signature. Your server does the same sum and compares. No match = not from us: throw it away.

PHP
<?php
$raw = file_get_contents('php://input');
$expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('SENDPK_API_KEY'));

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

$event = json_decode($raw, true);
// Skip repeats: have you already handled $event['event_id']?
http_response_code(200);
Node.js
const crypto = require('crypto');

// rawBody = the exact bytes received (e.g. express.raw({ type: 'application/json' }))
function isFromSendpk(rawBody, header) {
  const expected = 'sha256=' + crypto
    .createHmac('sha256', process.env.SENDPK_API_KEY)
    .update(rawBody)
    .digest('hex');
  const got = header || '';
  return got.length === expected.length
    && crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got));
}
Python
import hashlib
import hmac
import os


def is_from_sendpk(raw_body: bytes, header: str) -> bool:
    expected = "sha256=" + hmac.new(
        os.environ["SENDPK_API_KEY"].encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, header or "")

Test your code with these fixed values

Run your check with this test key and this exact body (one line, no spaces added). It must produce the signature below. The values are made up — they are not a real key or a real call.

Test key testuser-3f9a1c2b7d8e4f60a1b2c3d4e5f60718
Signature sha256=4f992f59ff30849ec3b97e119f40a3bc47e1283c92dc1d2530e15032469bb2e7
Raw body (one line)
{"event":"call.completed","event_id":"3c2384b7-580f-52fc-b246-aea4f0a32eb5","id":"0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f","status":"completed","outcome":"answered","number":"+923001234567","template":{"id":3301118,"title":"Order confirmation","duration":50},"audio_url":null,"recording_seconds":50,"dtmf":{"enabled":true,"max_digits":1,"timeout":10,"valid_digits":"12","digits":"1","valid":true},"created_at":1770000000,"ringing_at":1770000001,"answered_at":1770000005,"ended_at":1770000062,"duration":57,"charge":{"status":"charged","final":true,"price":6.0,"pulses":2,"price_per_pulse":3.0,"paid":6.0,"refunded":0.0,"currency":"PKR"},"error_code":null,"timestamp":1770000063}

Use the raw body, always

Never JSON-decode and re-encode the body before checking. One changed space gives a completely different signature.

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

Interactive Tools

Request Builder

Fill in the form. The code below updates as you type — ready to copy.

POST call.php

Live

    Quick checks in your browser only — nothing is sent anywhere. SENDPK checks everything again when you call the API.

    Your code

    cURL
    Loading...
    PHP
    Loading...
    Node.js
    Loading...
    Python
    Loading...
    JSON body
    Loading...

    The code reads your key from the SENDPK_API_KEY environment variable, so the key never sits in your code.

    Interactive Tools

    Price Calculator

    See the pulses and the price of one call before you send it.

    One call

    Live
    Pulses-
    Taken when you submit-
    Given back if not answered*-

    * On a normal account. If SENDPK set your account to keep charges for unanswered calls, it is kept. Answered calls (also voicemail) are always charged.

    Developer Tools

    Code Examples

    A complete, small integration: place a call, receive the result, and fall back to Status if a webhook is late.

    All examples run on your server

    The JavaScript examples are Node.js, not browser code. Put your key in the SENDPK_API_KEY environment variable.

    1. Place a call (with safe retry)

    PHP
    <?php
    /**
     * Place one call. Retries on a timeout or a 5xx with the SAME Idempotency-Key,
     * so the person is never called twice.
     */
    function sendpk_voice_call(string $number, int $templateId, string $idempotencyKey, array $extra = []): array
    {
        $body = json_encode(['number' => $number, 'template_id' => $templateId] + $extra);
    
        for ($try = 1; $try <= 3; $try++) {
            $ch = curl_init('https://sendpk.com/apps/phone_calls/call.php');
            curl_setopt_array($ch, [
                CURLOPT_POST           => true,
                CURLOPT_RETURNTRANSFER => true,
                CURLOPT_TIMEOUT        => 30,
                CURLOPT_HTTPHEADER     => [
                    'Authorization: ' . getenv('SENDPK_API_KEY'),
                    'Content-Type: application/json',
                    'Idempotency-Key: ' . $idempotencyKey,
                ],
                CURLOPT_POSTFIELDS     => $body,
            ]);
            $raw  = curl_exec($ch);
            $http = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
            curl_close($ch);
    
            $reply = is_string($raw) ? json_decode($raw, true) : null;
            if (is_array($reply) && $http < 500) {
                return $reply;                    // success, or a 4xx you must fix
            }
            sleep($try * 2);                      // no reply or 5xx: same key, try again
        }
    
        return ['success' => false, 'error' => ['code' => 'NO_REPLY', 'message' => 'No reply from SENDPK']];
    }
    
    $reply = sendpk_voice_call('+923001234567', 3301118, 'order-58213-call-1', [
        'dtmf'                  => ['max_digits' => 1, 'timeout' => 10, 'valid_digits' => '12'],
        'notification_callback' => 'https://example.com/webhooks/voice-call',
    ]);
    
    if ($reply['success']) {
        // Save $reply['data']['id'] with your order.
        echo 'Call id: ' . $reply['data']['id'] . ' (' . $reply['data']['status'] . ")\n";
    } else {
        echo 'Not placed: ' . $reply['error']['code'] . ' - ' . $reply['error']['message'] . "\n";
    }
    Node.js
    // Node.js 18+. Retries on a timeout or a 5xx with the SAME Idempotency-Key.
    async function sendpkVoiceCall(number, templateId, idempotencyKey, extra = {}) {
      const body = JSON.stringify({ number, template_id: templateId, ...extra });
    
      for (let attempt = 1; attempt <= 3; attempt++) {
        try {
          const res = await fetch('https://sendpk.com/apps/phone_calls/call.php', {
            method: 'POST',
            headers: {
              'Authorization': process.env.SENDPK_API_KEY,
              'Content-Type': 'application/json',
              'Idempotency-Key': idempotencyKey,
            },
            body,
            signal: AbortSignal.timeout(30000),
          });
          if (res.status < 500) {
            return await res.json(); // success, or a 4xx you must fix
          }
        } catch (err) {
          // timeout / network error: retry with the same key
        }
        await new Promise(r => setTimeout(r, attempt * 2000));
      }
      return { success: false, error: { code: 'NO_REPLY', message: 'No reply from SENDPK' } };
    }
    
    const reply = await sendpkVoiceCall('+923001234567', 3301118, 'order-58213-call-1', {
      dtmf: { max_digits: 1, timeout: 10, valid_digits: '12' },
      notification_callback: 'https://example.com/webhooks/voice-call',
    });
    console.log(reply.success ? `Call id: ${reply.data.id}` : `Not placed: ${reply.error.code}`);
    Python
    import os
    import time
    
    import requests
    
    
    def sendpk_voice_call(number, template_id, idempotency_key, **extra):
        """Retries on a timeout or a 5xx with the SAME Idempotency-Key."""
        body = {"number": number, "template_id": template_id, **extra}
        for attempt in range(1, 4):
            try:
                res = requests.post(
                    "https://sendpk.com/apps/phone_calls/call.php",
                    headers={
                        "Authorization": os.environ["SENDPK_API_KEY"],
                        "Idempotency-Key": idempotency_key,
                    },
                    json=body,
                    timeout=30,
                )
                if res.status_code < 500:
                    return res.json()  # success, or a 4xx you must fix
            except requests.RequestException:
                pass  # timeout / network error: retry with the same key
            time.sleep(attempt * 2)
        return {"success": False, "error": {"code": "NO_REPLY", "message": "No reply from SENDPK"}}
    
    
    reply = sendpk_voice_call(
        "+923001234567", 3301118, "order-58213-call-1",
        dtmf={"max_digits": 1, "timeout": 10, "valid_digits": "12"},
        notification_callback="https://example.com/webhooks/voice-call",
    )
    print(reply["data"]["id"] if reply["success"] else reply["error"]["code"])

    2. Receive the webhook

    PHP
    <?php
    // https://example.com/webhooks/voice-call
    $raw = file_get_contents('php://input');
    $expected = 'sha256=' . hash_hmac('sha256', $raw, getenv('SENDPK_API_KEY'));
    if (!hash_equals($expected, $_SERVER['HTTP_X_SENDPK_SIGNATURE'] ?? '')) {
        http_response_code(401);
        exit;
    }
    
    $call = json_decode($raw, true);
    
    // 1. Skip repeats (store handled event ids in your database).
    // if (already_handled($call['event_id'])) { http_response_code(200); exit; }
    
    // 2. Act on the final result only. Trust "status", not only "event".
    if ($call['status'] === 'completed') {
        $key = $call['dtmf']['digits'] ?? null;          // "1", "2", "" or null
        // e.g. $key === '1' -> order confirmed, '2' -> cancelled
    } elseif ($call['status'] === 'failed') {
        // $call['outcome']: no_answer, busy, unreachable, ... -> try SMS instead?
    }
    // $call['charge']['paid'] is what you paid for this call.
    
    http_response_code(200);   // answer fast (within 5 seconds)
    Node.js
    // Node.js + Express
    const express = require('express');
    const crypto = require('crypto');
    const app = express();
    
    app.post('/webhooks/voice-call', express.raw({ type: 'application/json' }), (req, res) => {
      const expected = 'sha256=' + crypto
        .createHmac('sha256', process.env.SENDPK_API_KEY)
        .update(req.body)              // raw bytes, NOT re-encoded JSON
        .digest('hex');
      const got = req.get('X-SENDPK-Signature') || '';
      if (got.length !== expected.length || !crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(got))) {
        return res.sendStatus(401);
      }
    
      const call = JSON.parse(req.body.toString('utf8'));
      // Skip repeats with call.event_id, then act on the final result:
      if (call.status === 'completed') {
        console.log('Answered. Key pressed:', call.dtmf.digits);
      } else if (call.status === 'failed') {
        console.log('Not answered:', call.outcome, 'refunded:', call.charge.refunded);
      }
      res.sendStatus(200);             // answer fast (within 5 seconds)
    });
    
    app.listen(3000);
    Python
    # Python + Flask
    import hashlib
    import hmac
    import json
    import os
    
    from flask import Flask, abort, request
    
    app = Flask(__name__)
    
    
    @app.post("/webhooks/voice-call")
    def voice_call_webhook():
        raw = request.get_data()  # raw bytes, NOT re-encoded JSON
        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", "")):
            abort(401)
    
        call = json.loads(raw)
        # Skip repeats with call["event_id"], then act on the final result:
        if call["status"] == "completed":
            print("Answered. Key pressed:", call["dtmf"].get("digits"))
        elif call["status"] == "failed":
            print("Not answered:", call["outcome"])
        return "", 200  # answer fast (within 5 seconds)

    3. Check the status (fallback when a webhook is late)

    cURL
    curl -s "https://sendpk.com/apps/phone_calls/status.php?id=0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f" \
      -H "Authorization: YOUR_API_KEY"
    PHP
    <?php
    $id = '0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f';
    $ch = curl_init('https://sendpk.com/apps/phone_calls/status.php?id=' . rawurlencode($id));
    curl_setopt_array($ch, [
        CURLOPT_RETURNTRANSFER => true,
        CURLOPT_TIMEOUT        => 15,
        CURLOPT_HTTPHEADER     => ['Authorization: ' . getenv('SENDPK_API_KEY')],
    ]);
    $call = json_decode((string) curl_exec($ch), true)['data'] ?? null;
    
    if ($call && in_array($call['status'], ['completed', 'failed'], true)) {
        echo $call['outcome'] . ', paid PKR ' . $call['charge']['paid'] . "\n";
    }
    Node.js
    const id = '0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f';
    const res = await fetch(`https://sendpk.com/apps/phone_calls/status.php?id=${encodeURIComponent(id)}`, {
      headers: { 'Authorization': process.env.SENDPK_API_KEY },
    });
    const { data: call } = await res.json();
    if (call && ['completed', 'failed'].includes(call.status)) {
      console.log(call.outcome, 'paid PKR', call.charge.paid);
    }
    Python
    import os
    import requests
    
    res = requests.get(
        "https://sendpk.com/apps/phone_calls/status.php",
        params={"id": "0192f4c3-7a1b-7c2d-9e3f-0a1b2c3d4e5f"},
        headers={"Authorization": os.environ["SENDPK_API_KEY"]},
        timeout=15,
    )
    call = res.json().get("data")
    if call and call["status"] in ("completed", "failed"):
        print(call["outcome"], "paid PKR", call["charge"]["paid"])
    Developer Tools

    Postman Collection

    Try every endpoint in Postman without writing code.

    What is inside

    1. Check my key GET templates.php — free, places no call.
    2. One recording GET templates.php?id=
    3. Place a call POST call.php — costs money; saves the call id for you.
    4. Place a call with key presses POST call.php with dtmf.
    5. Call status GET status.php?id={{call_id}}

    Put your key in the collection's api_key variable. Never share the collection after you filled it in.

    Reference

    Error Codes

    Switch on error.code in your program. The message is for humans.

    HTTPCodeMeaningWhat to do
    Request format
    400 BAD_REQUEST The request could not be understood. Check the URL, method and body.
    400 INVALID_JSON The body is not a valid JSON object. Send one JSON object, e.g. {"number": "..."}.
    400 INVALID_IDEMPOTENCY_KEY Idempotency-Key has spaces or non-ASCII characters, or is longer than 100. Use 1-100 visible ASCII characters.
    403 HTTPS_REQUIRED The request used plain http. Use https://.
    404 NOT_FOUND Wrong URL. Use call.php, status.php or templates.php.
    405 METHOD_NOT_ALLOWED Wrong HTTP method (details.allowed shows the right one). POST for call.php, GET for status.php and templates.php.
    413 PAYLOAD_TOO_LARGE The body is larger than 8 KB. Send only the fields this API accepts.
    415 INVALID_CONTENT_TYPE Content-Type is not application/json. Add the header Content-Type: application/json.
    API key and account
    401 AUTH_MISSING No API key in the Authorization header. Send Authorization: YOUR_API_KEY.
    401 AUTH_INVALID Wrong API key. Copy the key again from Profile. Too many wrong keys = 429 for 30 minutes.
    401 API_KEY_REVOKED Your API key must be changed. Make a new key in the dashboard.
    403 ACCOUNT_DISABLED The account is blocked. Contact SENDPK support.
    403 ACCOUNT_NOT_VERIFIED The account is not verified yet. Verify your account on sendpk.com.
    403 IP_NOT_ALLOWED Your server IP is not whitelisted (details.ip = the IP we saw). Add that IP in Profile (IPv6 too, if your server uses it).
    403 SERVICE_DISABLED Voice calls are not switched on for your account. Contact SENDPK support.
    403 FORBIDDEN Access not allowed. Contact SENDPK support with the request_id.
    Fields
    422 UNKNOWN_FIELD A field this API does not accept (details.field, details.allowed). Remove it. Allowed: number, template_id, dtmf, notification_callback (+ audio_url for free form).
    422 AUDIO_NOT_ALLOWED Text-to-speech or audio fields are never accepted; audio_url only on free-form accounts. Use template_id of an approved recording.
    422 INVALID_AUDIO_URL audio_url is not an allowed link, or the file is not reachable / not audio / too big / too long (details.reason). See "Own Audio Link" for the rules.
    422 CONFLICTING_FIELDS Both template_id and audio_url were sent. Send one of them.
    422 MISSING_NUMBER number is missing. Add number.
    422 INVALID_NUMBER Not a valid phone number (or the network refused it). Use the form +923001234567.
    422 NUMBER_NOT_MOBILE Not a mobile number (details.number_type). Only mobile numbers can be called.
    422 COUNTRY_NOT_SUPPORTED Calls to this country are not available (details.country_iso_code). Only Pakistani mobile numbers.
    422 MISSING_TEMPLATE_ID template_id (or, for free form, audio_url) is missing. Add template_id.
    422 INVALID_TEMPLATE_ID template_id is not a positive whole number. Send the id from templates.php, e.g. 3301118.
    422 INVALID_DTMF Wrong dtmf settings (details.field), or recording + timeout is longer than 245 s. See "Key Presses (DTMF)".
    422 INVALID_CALLBACK_URL notification_callback is not allowed. Public https URL, port 443 or 8443, max 500 characters.
    422 MISSING_ID status.php was called without id. Add ?id=CALL_ID.
    422 INVALID_ID id is not a valid call id. Use data.id from the call.php reply.
    Recordings
    404 TEMPLATE_NOT_FOUND No recording with this id on your account. Check the id with templates.php.
    422 TEMPLATE_PENDING The recording is still waiting for approval. Wait for approval.
    422 TEMPLATE_REJECTED The recording was rejected (details.reason). Fix it and have a new one approved.
    422 TEMPLATE_NOT_APPROVED The recording is not approved. Contact SENDPK support.
    422 TEMPLATE_INVALID The recording cannot be played (longer than 240 s, or a file problem). Contact SENDPK support.
    503 RECORDING_UNAVAILABLE The recording file cannot be reached right now. SENDPK is alerted. Retry later.
    Money and limits
    402 INSUFFICIENT_BALANCE No single PKR package can pay the full price (details.required, details.available = your biggest package). Top up, or use a shorter recording.
    402 VOICE_PRICE_NOT_SET Your package has no voice price yet. Contact SENDPK support.
    409 NUMBER_BUSY A different call to this number is still running (details.id). Wait up to 2 minutes (details.retry_after_seconds).
    409 IDEMPOTENCY_CONFLICT The same Idempotency-Key was used with a different body. Use a new key for a new call.
    429 RATE_LIMIT_EXCEEDED Too many calls per minute, or too many wrong API keys (details.scope = account, ip or auth). Wait details.retry_after_seconds.
    429 CONCURRENT_LIMIT_REACHED Too many calls running at the same time. Wait for running calls to end.
    429 DAILY_LIMIT_REACHED Daily call limit reached. Try again after 00:00 Pakistan time.
    429 NUMBER_DAILY_LIMIT_REACHED Too many calls to this number today. Try again after 00:00 Pakistan time.
    Our side / the call network
    500 INTERNAL_ERROR Our error. Retry with the SAME Idempotency-Key.
    502 PROVIDER_REJECTED The call network refused the call. The money is given back. Check the number; retry later.
    502 PROVIDER_INVALID_RESPONSE The call network sent a bad reply. Retry with the SAME Idempotency-Key.
    503 PROVIDER_UNAVAILABLE The call service is temporarily unavailable. Retry later.
    503 PROVIDER_BUSY The call service is busy. Retry after details.retry_after_seconds.
    503 SERVICE_BUSY Busy for a moment. Retry after details.retry_after_seconds.
    503 SERVICE_UNAVAILABLE Down for a moment (or voice calls are paused). Retry later.
    504 PROVIDER_TIMEOUT The call network did not answer in time. Retry with the SAME Idempotency-Key.
    No error code matches that search.

    error_code on a call (in Status and webhooks, not an error reply)

    CodeMeaning
    PROVIDER_UNKNOWNWe could not confirm the call was placed (checked for 15 minutes). Free. If the network reports it later, the call is updated.
    REPORT_MISSINGNo result arrived within 48 hours. Free.

    4xx or 5xx?

    After a 4xx no call was made and nothing was charged. After a 5xx (or no reply), send the request again with the same Idempotency-Key: if a call went out you get it back, never a second one.

    Reference

    Troubleshooting

    The problems people meet most, and the fix.

    ProblemWhyFix
    403 SERVICE_DISABLEDVoice calls are not switched on for your account.Contact SENDPK support.
    402 INSUFFICIENT_BALANCE but I have balanceThe full price must fit in one PKR package.Compare details.required with details.available; top up one package.
    402 VOICE_PRICE_NOT_SETYour package has no voice price yet.Contact SENDPK support.
    422 TEMPLATE_PENDINGThe recording is not reviewed yet.Wait; check templates.php.
    422 INVALID_DTMF with a valid timeoutRecording + timeout is over 245 seconds.Use a shorter timeout (e.g. 240 s recording = max 5 s).
    A 202 reply with pendingThe call network was slow to confirm.Do not send again. Wait for the webhook or check Status.
    409 NUMBER_BUSYA different call to that number started less than 2 minutes ago and is still running.Wait, or check details.id.
    No webhook arrivesThe URL breaks a rule, or your server did not answer 2xx within 5 s.Check webhook.status and last_http_code in Status.
    Signature never matchesThe body was re-encoded, or an old API key is used.Use the raw body and your current key. Test with the fixed values.
    dtmf.digits is null on a final callThe keys could not be read for this call.Treat it as “no answer given”.
    403 IP_NOT_ALLOWEDIP restriction is on and this IP is not on your list.Add details.ip in Profile (IPv6 too).
    422 AUDIO_NOT_ALLOWEDA text / audio field was sent, or audio_url without free form.Use template_id.
    Reference

    Frequently Asked Questions

    What does the Voice Call API do?
    It calls a Pakistani mobile number and plays one of your approved voice recordings. If you want, the person can press keys on the phone (for example 1 = yes, 2 = no) and you get the keys back.
    How do I switch it on for my account?
    Voice calls are switched on per account by SENDPK, and SENDPK sets your price per pulse. Contact SENDPK support. Until then every request answers 403 SERVICE_DISABLED.
    Can SENDPK read out my text (text-to-speech)?
    No. Only recordings are played. Fields such as text or tts are refused with 422 AUDIO_NOT_ALLOWED.
    How do I get a recording approved?
    Your recordings are reviewed by SENDPK before they can be played. Contact SENDPK support to add a new recording. GET templates.php shows every recording on your account and its status: approved, pending or rejected (with the reason).
    Which numbers can I call?
    Pakistani mobile numbers only. Other countries give COUNTRY_NOT_SUPPORTED and landlines give NUMBER_NOT_MOBILE. Nothing is charged for these errors.
    How is a call priced?
    By 30-second pulses of the RECORDING length, rounded up: 20 s = 1 pulse, 50 s = 2 pulses, 75 s = 3 pulses. Price = pulses x your price per pulse. How long the person listens, and the key-press wait, do not change the price.
    When is the money taken?
    The full price is taken when you submit the call. When the call ends it is kept if the call was answered. If nobody answered it is given back, unless SENDPK set your account to keep charges for unanswered calls.
    Is a call answered by voicemail charged?
    Yes. The outcome is answered_machine and the recording was played, so it counts as answered.
    Can I play my own audio file without approval?
    Only if SENDPK has switched on free form for your account. Then you may send audio_url (a public https .wav or .mp3 link) instead of template_id. We download it once, check it is real audio, measure it and price it by that length.
    Do I get an event when the phone starts ringing?
    No. You get call.initiated when the call is accepted, then one final event (call.completed or call.failed). The ringing, answer and end times arrive inside the final result.
    My request timed out. Will a retry call the person twice?
    Not if you send the same Idempotency-Key with the same body. Within 24 hours you get the first call back (reused: true) and nothing is charged again.
    What does a 202 reply mean?
    We sent the call but did not get a confirmation in time (status pending). Do not send it again. We check it ourselves within a few minutes and you then get the webhook, or see the new status in status.php.
    How long until I get the final result?
    Normally a few seconds after the call ends. If the result is late we look it up ourselves. If no result arrives in 48 hours the call is closed as failed with REPORT_MISSING and the money is given back.
    How do I test my API key without making a call?
    Call GET templates.php. It is free and places no call. 200 means your key, IP whitelist and account are all fine.
    Can I call the API from a mobile app or a web page?
    No. Call it from your own server only. Anyone who sees your API key can spend your balance.
    Reference

    Best Practices

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

    • Keep your API key on your server, in an environment variable.
    • Send an Idempotency-Key with every call, and retry only with the same key.
    • Never re-send a 202 pending call with a new key.
    • Check the webhook signature on every webhook, and skip repeated event_ids.
    • Act on status (completed / failed), not only on event.
    • Keep recordings short: every started 30 seconds is a pulse.
    • Say the choices clearly in the recording (“press 1 to confirm”) and set valid_digits.
    • Respect Retry-After on every 429 and 503.
    • Save meta.request_id for support.
    • Call people at sensible hours, and only people who agreed to hear from you.
    Reference

    Support

    Need help integrating?

    We can switch voice calls on, set your price, add and review your recordings, raise your limits, or find out why a call did not go through. Keep your request_id or call id ready.

    Looking for SMS instead? See the SMS API documentation. To verify numbers with a missed call, see Missed Call Verification.