# Code examples > Complete, working examples in curl, PHP, Node.js and Python. > Source: https://onpad.in/docs/api/examples 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 ```bash #!/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 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`. ```javascript 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`. ```python 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. ```python 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](https://onpad.in/docs/openapi.json) describes the whole API. Point a generator at it, or hand it to a coding agent: ```text Use the OpenAPI spec at https://onpad.in/docs/openapi.json to write a client that sends a template message and polls for delivery. ```