$method, CURLOPT_HTTPHEADER => $headers, CURLOPT_RETURNTRANSFER => true, CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 15, CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2, ]); if ($payload !== null) { // API request bodies are JSON objects; [] would be rejected. curl_setopt($handle, CURLOPT_POSTFIELDS, json_encode((object) $payload, JSON_THROW_ON_ERROR)); } $body = curl_exec($handle); $status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE); $error = curl_error($handle); curl_close($handle); if ($body === false) { throw new RuntimeException('Network error: ' . $error); } $data = json_decode($body, true, 512, JSON_THROW_ON_ERROR); if (!is_array($data) || $status < 200 || $status >= 300) { throw new RuntimeException('Sayapay HTTP ' . $status . ': ' . $body); } return $data; } function assertOrderMatches(array $payment, string $orderId, int $amount): void { if ( ($payment['order_id'] ?? null) !== $orderId || ($payment['amount'] ?? null) !== $amount || ($payment['currency'] ?? null) !== 'IRR' ) { throw new RuntimeException('Payment does not match the local order.'); } } // Examples: // SAYAPAY_TEST_KEY=sp_test_... php sayapay-example.php create ORDER-1042 1250000 https://shop.example/return // SAYAPAY_TEST_KEY=sp_test_... php sayapay-example.php verify pay_xxx ORDER-1042 1250000 // Keep this file and the key on the server. Never call the API with the key from a browser. if (PHP_SAPI !== 'cli') { throw new RuntimeException('Run this example from the command line only.'); } $key = getenv('SAYAPAY_TEST_KEY'); if (!is_string($key) || !str_starts_with($key, 'sp_test_')) { throw new RuntimeException('Set SAYAPAY_TEST_KEY in the server environment.'); } $mode = $argv[1] ?? ''; if ($mode === 'create') { $orderId = $argv[2] ?? ''; $amount = filter_var($argv[3] ?? null, FILTER_VALIDATE_INT); if ($orderId === '' || $amount === false || $amount < 1000 || $amount > 1000000000000) { throw new RuntimeException('Pass an order ID and an integer IRR amount.'); } $returnUrl = $argv[4] ?? ''; if (!str_starts_with($returnUrl, 'https://')) { throw new RuntimeException('Pass an HTTPS return URL.'); } $payment = sayapayRequest( 'POST', '/payments', $key, [ 'order_id' => $orderId, 'title' => 'Order ' . $orderId, 'amount' => $amount, 'currency' => 'IRR', 'return_url' => $returnUrl, ], 'order-' . hash('sha256', $orderId) ); assertOrderMatches($payment, $orderId, $amount); // Persist payment.id against the order before redirecting the buyer. echo json_encode($payment, JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . PHP_EOL; } elseif ($mode === 'verify') { $paymentId = $argv[2] ?? ''; $orderId = $argv[3] ?? ''; $amount = filter_var($argv[4] ?? null, FILTER_VALIDATE_INT); if ( !preg_match('/^pay_[a-f0-9]+$/', $paymentId) || $orderId === '' || $amount === false || $amount < 1000 || $amount > 1000000000000 ) { throw new RuntimeException('Usage: verify PAYMENT_ID ORDER_ID AMOUNT'); } $payment = sayapayRequest( 'POST', '/payments/' . rawurlencode($paymentId) . '/verify', $key, [] ); assertOrderMatches($payment, $orderId, $amount); if (($payment['id'] ?? null) !== $paymentId) { throw new RuntimeException('Unexpected payment ID.'); } // Fulfil a NEW local order only when status is paid and refund_amount is 0. // Make local fulfilment atomic/idempotent; handle refunds separately. echo json_encode([ 'status' => $payment['status'], 'refund_amount' => $payment['refund_amount'], 'eligible_for_initial_fulfilment' => $payment['status'] === 'paid' && $payment['refund_amount'] === 0, ], JSON_PRETTY_PRINT | JSON_THROW_ON_ERROR) . PHP_EOL; } else { throw new RuntimeException('Usage: create ORDER_ID AMOUNT RETURN_URL | verify PAYMENT_ID ORDER_ID AMOUNT'); }