send('0712345678', 'Habari! Malipo yamepokelewa.'); * if ($res['ok']) { ... $res['data']['messages'][0]['id'] ... } else { error_log($res['error']); } * * Methods never throw on API/network errors; they return * ['ok' => bool, 'status' => int, 'data' => array|null, 'error' => ?string, 'code' => ?string] */ class PoaSms { private string $apiKey; private string $baseUrl; private int $timeout; public function __construct(string $apiKey, string $baseUrl = 'https://poasms.com/api/v1', int $timeout = 20) { $this->apiKey = $apiKey; $this->baseUrl = rtrim($baseUrl, '/'); $this->timeout = $timeout; } /** * @param string|string[] $to * @param array $options from, type (transactional|promotional|otp), reference, schedule_at, app */ public function send($to, string $message, array $options = []): array { return $this->request('POST', '/sms/send', array_merge(['to' => $to, 'message' => $message], $options)); } public function otp(string $to, string $message, ?string $reference = null): array { return $this->send($to, $message, array_filter(['type' => 'otp', 'reference' => $reference])); } public function status(string $messageId): array { return $this->request('GET', '/sms/'.rawurlencode($messageId)); } public function messages(array $filters = []): array { return $this->request('GET', '/sms', $filters); } public function balance(): array { return $this->request('GET', '/balance'); } /** @param string $groupBy app|day|month|status|network|type|sender */ public function usage(?string $from = null, ?string $to = null, string $groupBy = 'app'): array { return $this->request('GET', '/usage', array_filter(['from' => $from, 'to' => $to, 'group_by' => $groupBy])); } // --- WhatsApp ------------------------------------------------------------- /** * Send an approved WhatsApp template. * * @param string|string[] $to * @param string[] $params values for {{1}}, {{2}}… * @param array $options header (type,url|id,filename), buttons, reference, app, schedule_at, language */ public function whatsapp($to, string $template, array $params = [], array $options = []): array { return $this->request('POST', '/whatsapp/send', array_merge(['to' => $to, 'template' => $template, 'params' => $params], $options)); } /** Free-text WhatsApp reply — only within 24h of the customer's last message. */ public function whatsappReply(string $to, string $message, ?string $reference = null): array { return $this->request('POST', '/whatsapp/send', array_filter(['to' => $to, 'message' => $message, 'reference' => $reference])); } public function whatsappTemplates(): array { return $this->request('GET', '/whatsapp/templates'); } public function createWhatsappTemplate(array $template): array { return $this->request('POST', '/whatsapp/templates', $template); } public function whatsappInbox(array $filters = []): array { return $this->request('GET', '/whatsapp/inbox', $filters); } public function senderIds(): array { return $this->request('GET', '/sender-ids'); } /** One sender ID's approval status: pending | approved | rejected. */ public function senderId(string $name): array { return $this->request('GET', '/sender-ids/'.rawurlencode($name)); } /** * Request a sender ID. $options: use_case, reference, document (path to a PDF/JPG/PNG ≤ 5 MB). * A name already requested returns ok=false with code 'sender_exists'. */ public function requestSenderId(string $name, string $sampleMessage, array $options = []): array { $data = array_filter([ 'name' => $name, 'sample_message' => $sampleMessage, 'use_case' => $options['use_case'] ?? 'transactional', 'reference' => $options['reference'] ?? null, ], fn ($v) => $v !== null); $doc = $options['document'] ?? null; if ($doc && is_readable($doc)) { $data['document'] = new CURLFile($doc); } return $this->request('POST', '/sender-ids', $data); } public function addContact(string $phone, ?string $name = null, array $groups = []): array { return $this->request('POST', '/contacts', array_filter(['phone' => $phone, 'name' => $name, 'groups' => $groups])); } public function optOut(string $phone): array { return $this->request('POST', '/opt-outs', ['phone' => $phone]); } /** Verify a webhook from POASMS. Pass the raw request body and the X-PoaSMS-Signature header. */ public static function verifyWebhook(string $rawBody, ?string $signatureHeader, string $secret): bool { if (! $signatureHeader) { return false; } return hash_equals('sha256='.hash_hmac('sha256', $rawBody, $secret), $signatureHeader); } private function request(string $method, string $path, array $data = []): array { $url = $this->baseUrl.$path; if ($method === 'GET' && $data) { $url .= '?'.http_build_query($data); } $ch = curl_init($url); $headers = ['Authorization: Bearer '.$this->apiKey, 'Accept: application/json', 'User-Agent: poasms-php/1.0']; $multipart = (bool) array_filter($data, fn ($v) => $v instanceof CURLFile); if ($method !== 'GET' && $multipart) { curl_setopt($ch, CURLOPT_POSTFIELDS, $data); // curl sets multipart/form-data } elseif ($method !== 'GET') { $headers[] = 'Content-Type: application/json'; curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode($data)); } curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_CONNECTTIMEOUT => 8, ]); $raw = curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_HTTP_CODE); $err = curl_error($ch); if (PHP_VERSION_ID < 80000) { curl_close($ch); } if ($raw === false || $status === 0) { return ['ok' => false, 'status' => 0, 'data' => null, 'error' => 'Network error: '.$err, 'code' => 'network_error']; } $json = json_decode((string) $raw, true); $ok = $status >= 200 && $status < 300; return [ 'ok' => $ok, 'status' => $status, 'data' => $ok ? ($json['data'] ?? $json) : null, 'error' => $ok ? null : ($json['error']['message'] ?? $json['message'] ?? "HTTP $status"), 'code' => $ok ? null : ($json['error']['code'] ?? null), ]; } }