Kod örnekleri: PHP
Gereksinimler: PHP 8.1 ya da sonrası, curl eklentisiyle; paket gerekmez.
Küçük bir istemci
Taban adresi ve anahtarı ortamdan okur; ağ hatalarını, 5xx yanıtları ve yeniden denenebilir hataları aynı Idempotency-Key ile yeniden dener. Öteki örnekler onu kullanır.
Dosya: client.php
<?php
/*
* UPPU API v1: a minimal client (PHP 8.1+, curl extension, no packages).
*
* Configuration comes from the environment, never from the source code:
* UPPU_API_BASE https://messaging-staging.uppu.net/v1 (production: https://messaging.uppu.net/v1)
* UPPU_API_KEY uppu_test_... on staging, uppu_live_... in production. Never log it.
*/
/**
* Calls the API and returns [HTTP status, decoded body, response headers].
* Network errors, 5xx answers and errors marked retryable are retried with
* the same Idempotency-Key, so a retry never sends a message twice. The
* status is 0 when the network failed on every attempt.
*
* @param array<string, mixed>|object|null $body
* @return array{0: int, 1: array<string, mixed>|null, 2: array<string, string>}
*/
function uppu_request(string $method, string $path, array|object|null $body = null, ?string $idempotencyKey = null, int $attempts = 3): array
{
$base = rtrim((string) getenv('UPPU_API_BASE') ?: 'https://messaging-staging.uppu.net/v1', '/');
$headers = ['Authorization: Bearer '.getenv('UPPU_API_KEY'), 'Accept: application/json'];
$payload = null;
if ($body !== null) {
$headers[] = 'Content-Type: application/json';
$payload = json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
}
if ($idempotencyKey !== null) {
$headers[] = 'Idempotency-Key: '.$idempotencyKey;
}
$attempt = 0;
while (true) {
$attempt++;
$responseHeaders = [];
$curl = curl_init($base.$path);
curl_setopt_array($curl, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_HTTPHEADER => $headers,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5,
CURLOPT_TIMEOUT => 20,
CURLOPT_HEADERFUNCTION => function ($curl, string $line) use (&$responseHeaders): int {
$parts = explode(':', $line, 2);
if (count($parts) === 2) {
$responseHeaders[strtolower(trim($parts[0]))] = trim($parts[1]);
}
return strlen($line);
},
]);
if ($payload !== null) {
curl_setopt($curl, CURLOPT_POSTFIELDS, $payload);
}
$raw = curl_exec($curl);
$status = $raw === false ? 0 : (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
$decoded = is_string($raw) && $raw !== '' ? json_decode($raw, true) : null;
$decoded = is_array($decoded) ? $decoded : null;
$retryable = $status === 0 || $status >= 500 || ($decoded['error']['retryable'] ?? false) === true;
if (! $retryable || $attempt >= $attempts) {
return [$status, $decoded, $responseHeaders];
}
// Retry-After when UPPU sends it, otherwise 1, 2, ... seconds; at most 10
sleep(max(1, min((int) ($responseHeaders['retry-after'] ?? $attempt), 10)));
}
}
/**
* The error code of an answer, for logs and decisions (never the message).
*
* @param array<string, mixed>|null $body
*/
function uppu_error_code(int $status, ?array $body): string
{
return (string) ($body['error']['code'] ?? ($status === 0 ? 'network_error' : 'http_'.$status));
}Metin mesajı gönderin
messages:send kapsamıyla POST /v1/messages. Her mantıksal gönderime bir Idempotency-Key; her yeniden denemede aynısı.
Dosya: send-text.php
<?php
// Send a text message: POST /v1/messages, scope messages:send.
require __DIR__.'/client.php';
$channelId = '01j9z0ch5k2m4n6p8q0r2s4t6v'; // from GET /v1/channels
$contactId = '01j8x7ct5k2m4n6p8q0r2s4t6v'; // from message.received: data.contact.id
[$status, $message] = uppu_request('POST', '/messages', [
'channel_id' => $channelId,
'to' => ['contact_id' => $contactId],
'content' => ['type' => 'text', 'text' => ['body' => 'Your order has shipped.']],
'client_reference' => 'order-1001',
], idempotencyKey: 'order-1001-shipped'); // the same key for every retry of this send
if ($status === 202 || $status === 200) {
// 202: queued; 200: a replay of the same send (Idempotent-Replayed: true)
echo 'Message ', $message['id'], ' is ', $message['status'], PHP_EOL;
} else {
// For example window_closed: the 24-hour window is closed, send a template instead
echo 'Not sent: ', uppu_error_code($status, $message), PHP_EOL;
}Şablon mesajı gönderin
Onaylı bir WhatsApp şablonu; 24 saatlik pencerenin dışında da gider. Örnek, sahne simülatörünün bir şablonunu kullanır.
Dosya: send-template.php
<?php
// Send an approved WhatsApp template, also outside the 24-hour window:
// POST /v1/messages, scope messages:send. GET /v1/templates shows each
// template's parameters and a ready send_example.
require __DIR__.'/client.php';
[$status, $message] = uppu_request('POST', '/messages', [
'channel_id' => '01j9z0ch5k2m4n6p8q0r2s4t6v',
'to' => ['contact_id' => '01j8x7ct5k2m4n6p8q0r2s4t6v'],
'content' => [
'type' => 'template',
'template' => [
'name' => 'uppu_sim_order_update', // a template of the staging simulator
'language' => 'tr',
'body' => ['parameters' => ['Deniz', 'A-1001']], // {{1}} and {{2}}
],
],
'client_reference' => 'order-A-1001',
], idempotencyKey: 'order-A-1001-shipped');
if ($status === 202 || $status === 200) {
echo 'Template message ', $message['id'], ' is ', $message['status'], PHP_EOL;
} else {
// For example consent_required: a MARKETING template needs a recorded consent
echo 'Not sent: ', uppu_error_code($status, $message), PHP_EOL;
}Mesajları listeleyin
messages:read kapsamıyla GET /v1/messages, imleçle sayfa sayfa; örneğin bir webhook kesintisinden sonra uzlaşmak için.
Dosya: list-messages.php
<?php
// List messages page by page: GET /v1/messages, scope messages:read. Use it
// to reconcile after a webhook outage.
require __DIR__.'/client.php';
$query = [
'channel_id' => '01j9z0ch5k2m4n6p8q0r2s4t6v',
'created_after' => '2026-10-20T00:00:00Z',
'order' => 'asc',
'limit' => 100,
];
do {
[$status, $page] = uppu_request('GET', '/messages?'.http_build_query($query));
if ($status !== 200) {
throw new RuntimeException('Listing failed: '.uppu_error_code($status, $page));
}
foreach ($page['data'] as $message) {
echo $message['id'], ' ', $message['direction'], ' ', $message['status'], PHP_EOL;
}
// The cursor carries the order; repeat the filters on every page
$query['cursor'] = $page['next_cursor'];
} while ($query['cursor'] !== null);Mesajı okundu yapın
messages:send kapsamıyla POST /v1/messages/{id}/read; yalnızca WhatsApp. Idempotency-Key gerekmez: tekrarı 200 döner.
Dosya: mark-read.php
<?php
// Mark a message of the person as read (WhatsApp only):
// POST /v1/messages/{id}/read, scope messages:send. No Idempotency-Key is
// needed: a repeat answers 200.
require __DIR__.'/client.php';
$messageId = '01jab3t6ms2m4n6p8q0r2s4t6v'; // from message.received: data.message.id
[$status, $receipt] = uppu_request('POST', '/messages/'.rawurlencode($messageId).'/read', new stdClass);
if ($status === 202 || $status === 200) {
echo 'Read receipt ', $receipt['state'], PHP_EOL;
} elseif (uppu_error_code($status, $receipt) === 'not_inbound') {
// Nothing to mark (your own message, a reaction, a notice): final, do not retry
echo 'Nothing to mark: ', $receipt['error']['details']['reason'] ?? 'unknown', PHP_EOL;
} else {
echo 'Not marked: ', uppu_error_code($status, $receipt), PHP_EOL;
}Webhook imzasını doğrulayın
İmzayı ham gövde üzerinden denetleyin, tekrarları webhook-id ile ayıklayın, hemen 2xx dönün ve olayı sonra işleyin.
Dosya: verify-webhook.php
<?php
/*
* Receive UPPU webhooks: check the signature over the raw body, drop
* repeats by webhook-id, answer 2xx at once and process the event later.
* The signing secret comes from the environment: UPPU_WEBHOOK_SECRET, and
* during a secret rotation the previous one in UPPU_WEBHOOK_SECRET_PREVIOUS.
*/
/**
* Standard Webhooks v1 signature check: base64(HMAC-SHA256(key, id.timestamp.body)),
* the key being the base64-decoded secret after "whsec_".
*
* @param list<string> $secrets the current secret and, during a rotation, the previous one
*/
function uppu_verify_signature(array $secrets, string $id, string $timestamp, string $body, string $header, ?int $now = null, int $tolerance = 300): bool
{
if ($id === '' || ! ctype_digit($timestamp) || abs(($now ?? time()) - (int) $timestamp) > $tolerance) {
return false;
}
foreach ($secrets as $secret) {
if (! str_starts_with($secret, 'whsec_')) {
continue;
}
$key = base64_decode(substr($secret, 6), true);
if ($key === false || $key === '') {
continue;
}
$expected = base64_encode(hash_hmac('sha256', $id.'.'.$timestamp.'.'.$body, $key, true));
foreach (explode(' ', $header) as $part) {
[$version, $signature] = array_pad(explode(',', $part, 2), 2, '');
if ($version === 'v1' && hash_equals($expected, $signature)) {
return true;
}
}
}
return false;
}
// The endpoint itself (PHP-FPM or Apache): the raw body, never a re-encoded one
$body = (string) file_get_contents('php://input');
$header = fn (string $name): string => trim((string) ($_SERVER['HTTP_'.strtoupper(str_replace('-', '_', $name))] ?? ''));
$secrets = array_values(array_filter([(string) getenv('UPPU_WEBHOOK_SECRET'), (string) getenv('UPPU_WEBHOOK_SECRET_PREVIOUS')]));
if (! uppu_verify_signature($secrets, $header('webhook-id'), $header('webhook-timestamp'), $body, $header('webhook-signature'))) {
http_response_code(401); // anything but 2xx: the wrong-signature probe expects it
exit;
}
$event = json_decode($body, true, 512, JSON_THROW_ON_ERROR);
// Test events of the panel: answer 2xx and do nothing else
if (($event['test'] ?? false) === true) {
http_response_code(204);
exit;
}
// Drop repeats: remember every webhook-id for at least 35 days, for example
// in a table with the id as its primary key, and store the body with it
if (! store_event_once($event['id'], $event['type'], $body)) {
http_response_code(200);
exit;
}
// Answer within 10 seconds; process the stored event afterwards
http_response_code(204);
if (function_exists('fastcgi_finish_request')) {
fastcgi_finish_request();
}
process_event($event);
/**
* Your storage: true when the id is new (INSERT ... ON DUPLICATE KEY or the like).
*/
function store_event_once(string $id, string $type, string $body): bool
{
return true;
}
/**
* @param array<string, mixed> $event
*/
function process_event(array $event): void
{
switch ($event['type']) {
case 'message.received':
// data.message, data.contact and data.service_window
break;
case 'message.status':
// data.message.id or data.message.client_reference, data.status
break;
case 'contact.erased':
// delete your copy of data.contact_id
break;
default:
// ignore event types you do not know
break;
}
}