Skip to content
ONPADDocs

Docs / API

Code examples

Complete, working examples in curl, PHP, Node.js and Python.

View as Markdown

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.

Still stuck?

Ask ONPAD, the assistant inside your workspace, can answer questions about your own data โ€” how a specific campaign performed, why one message failed, what your limits are right now.

Ask ONPAD in your workspaceEmail support@onpad.in