Code examples
Complete, working examples in curl, PHP, Node.js and Python.
Each example sends a template message, handles the errors that matter, and polls for the result. Copy one and change the key, template name and recipient.
curl
#!/usr/bin/env bash
set -euo pipefail
API_KEY="wh_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
BASE="https://app.onpad.in/api/v1"
# Replace this. 919876543210 is a placeholder, not a test number โ
# whoever owns it will receive whatever you send, and Meta will charge you.
RECIPIENT="919876543210"
response=$(curl -sS -X POST "$BASE/messages" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-10482-shipped" \
-d '{
"to": "'"$RECIPIENT"'",
"type": "template",
"template": {
"name": "order_shipped",
"language": "en_US",
"parameters": ["Priya", "#10482", "Friday"]
}
}')
echo "$response" | jq .
id=$(echo "$response" | jq -r '.message.id')
sleep 3
curl -sS "$BASE/messages?id=$id" -H "Authorization: Bearer $API_KEY" | jq .PHP
<?php
declare(strict_types=1);
final class OnpadClient
{
public function __construct(
private string $apiKey,
private string $baseUrl = 'https://app.onpad.in/api/v1',
) {}
/** @throws RuntimeException on anything other than a queued or duplicate message */
public function sendTemplate(
string $to,
string $template,
array $parameters,
string $idempotencyKey,
string $language = 'en_US',
): array {
return $this->request('POST', '/messages', [
'to' => $to,
'type' => 'template',
'template' => [
'name' => $template,
'language' => $language,
'parameters' => $parameters,
],
], $idempotencyKey);
}
public function getMessage(string $id): array
{
return $this->request('GET', '/messages?id=' . urlencode($id));
}
private function request(string $method, string $path, ?array $body = null, ?string $idempotencyKey = null): array
{
$headers = ['Authorization: Bearer ' . $this->apiKey];
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
}
if ($idempotencyKey !== null) {
$headers[] = 'Idempotency-Key: ' . $idempotencyKey;
}
$curl = curl_init($this->baseUrl . $path);
curl_setopt_array($curl, [
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_TIMEOUT => 30,
]);
if ($body !== null) {
curl_setopt($curl, CURLOPT_POSTFIELDS, json_encode($body, JSON_UNESCAPED_UNICODE));
}
$raw = curl_exec($curl);
$status = (int) curl_getinfo($curl, CURLINFO_HTTP_CODE);
$error = curl_error($curl);
curl_close($curl);
if ($raw === false) {
throw new RuntimeException('Could not reach ONPAD: ' . $error);
}
$decoded = json_decode((string) $raw, true);
if (!is_array($decoded)) {
throw new RuntimeException('Unreadable response from ONPAD.');
}
if (($decoded['ok'] ?? false) !== true) {
throw new RuntimeException(sprintf(
'[%d %s] %s',
$status,
$decoded['error'] ?? 'error',
$decoded['message'] ?? 'Unknown error',
));
}
return $decoded;
}
}
$onpad = new OnpadClient('wh_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx');
// Replace this. 919876543210 is a placeholder, not a test number.
$recipient = '919876543210';
$result = $onpad->sendTemplate(
to: $recipient,
template: 'order_shipped',
parameters: ['Priya', '#10482', 'Friday'],
idempotencyKey: 'order-10482-shipped',
);
echo $result['message']['id'], PHP_EOL;
sleep(3);
echo $onpad->getMessage($result['message']['id'])['message']['status'], PHP_EOL;Node.js
Needs Node 18 or newer for the built-in fetch.
const API_KEY = "wh_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
const BASE = "https://app.onpad.in/api/v1";
// Replace this. 919876543210 is a placeholder, not a test number.
const RECIPIENT = "919876543210";
class OnpadError extends Error {
constructor(status, code, message) {
super(`[${status} ${code}] ${message}`);
this.status = status;
this.code = code;
}
}
async function call(path, { method = "GET", body, idempotencyKey } = {}) {
const headers = { Authorization: `Bearer ${API_KEY}` };
if (body) headers["Content-Type"] = "application/json";
if (idempotencyKey) headers["Idempotency-Key"] = idempotencyKey;
const response = await fetch(BASE + path, {
method,
headers,
body: body ? JSON.stringify(body) : undefined,
});
const data = await response.json();
if (!data.ok) throw new OnpadError(response.status, data.error, data.message);
return data;
}
async function sendTemplate(to, name, parameters, idempotencyKey, language = "en_US") {
return call("/messages", {
method: "POST",
idempotencyKey,
body: { to, type: "template", template: { name, language, parameters } },
});
}
async function waitForDelivery(id, attempts = 6) {
const delays = [2000, 5000, 15000, 30000, 30000, 30000];
for (let i = 0; i < attempts; i++) {
await new Promise(resolve => setTimeout(resolve, delays[i] ?? 30000));
const { message } = await call(`/messages?id=${encodeURIComponent(id)}`);
if (["delivered", "read", "failed"].includes(message.status)) return message;
}
return null;
}
const { message } = await sendTemplate(
RECIPIENT,
"order_shipped",
["Priya", "#10482", "Friday"],
"order-10482-shipped",
);
console.log(message.id, message.status);
console.log(await waitForDelivery(message.id));Python
Needs requests.
import time
import requests
API_KEY = "wh_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
BASE = "https://app.onpad.in/api/v1"
# Replace this. 919876543210 is a placeholder, not a test number.
RECIPIENT = "919876543210"
class OnpadError(Exception):
def __init__(self, status, code, message):
super().__init__(f"[{status} {code}] {message}")
self.status = status
self.code = code
def call(path, method="GET", body=None, idempotency_key=None):
headers = {"Authorization": f"Bearer {API_KEY}"}
if idempotency_key:
headers["Idempotency-Key"] = idempotency_key
response = requests.request(method, BASE + path, json=body, headers=headers, timeout=30)
data = response.json()
if not data.get("ok"):
raise OnpadError(response.status_code, data.get("error"), data.get("message"))
return data
def send_template(to, name, parameters, idempotency_key, language="en_US"):
return call(
"/messages",
method="POST",
idempotency_key=idempotency_key,
body={
"to": to,
"type": "template",
"template": {"name": name, "language": language, "parameters": parameters},
},
)
def wait_for_delivery(message_id, delays=(2, 5, 15, 30, 30, 30)):
for delay in delays:
time.sleep(delay)
message = call(f"/messages?id={message_id}")["message"]
if message["status"] in ("delivered", "read", "failed"):
return message
return None
result = send_template(
to=RECIPIENT,
name="order_shipped",
parameters=["Priya", "#10482", "Friday"],
idempotency_key="order-10482-shipped",
)
message_id = result["message"]["id"]
print(message_id, result["message"]["status"])
print(wait_for_delivery(message_id))Retrying safely
All four examples would be improved by retrying 500 responses. The rule is the same in every language: build the idempotency key once, outside the retry loop, so every attempt carries the same key and at most one message is ever sent.
key = f"order-{order_id}-shipped" # once
for attempt in range(3):
try:
return send_template(to, name, params, key)
except OnpadError as error:
if error.status != 500:
raise # 4xx will not fix itself
time.sleep(2 ** attempt)Retry 500 only. A 422 returns the same answer forever, and retrying it just fills your logs.
Generated clients
The OpenAPI specification describes the whole API. Point a generator at it, or hand it to a coding agent:
Use the OpenAPI spec at https://onpad.in/docs/openapi.json
to write a client that sends a template message and polls for delivery.