base = rtrim($base, '/'); } /** @return array{status:int,body:array} */ public function request(string $method, string $path, array $query = [], ?array $body = null, ?string $idempotencyKey = null): array { $method = strtoupper($method); $qs = http_build_query($query, '', '&', PHP_QUERY_RFC3986); $raw = $body === null ? '' : json_encode($body, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR); $url = $this->base . '/' . ltrim($path, '/') . ($qs !== '' ? '?' . $qs : ''); $ts = (string) time(); // Canonical string: METHOD \n PATH \n QUERY \n TIMESTAMP \n sha256(body) $signedPath = (string) parse_url($url, PHP_URL_PATH); $canonical = $method . "\n" . $signedPath . "\n" . $qs . "\n" . $ts . "\n" . hash('sha256', $raw); $headers = [ 'Authorization: Bearer ' . $this->token, 'Accept: application/json', 'X-Timestamp: ' . $ts, 'X-Signature: ' . hash_hmac('sha256', $canonical, $this->secret), ]; if ($raw !== '') { $headers[] = 'Content-Type: application/json'; } if ($idempotencyKey !== null) { $headers[] = 'Idempotency-Key: ' . $idempotencyKey; } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers, CURLOPT_POSTFIELDS => $raw !== '' ? $raw : null, CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => 60, ]); $resp = (string) curl_exec($ch); $status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); return ['status' => $status, 'body' => (array) json_decode($resp, true)]; } } /** Verify an incoming webhook (call before trusting the body). */ function intapi_webhook_valid(string $secret, string $timestamp, string $signature, string $rawBody, int $window = 300): bool { if (! ctype_digit($timestamp) || abs(time() - (int) $timestamp) > $window) { return false; } return hash_equals(hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret), $signature); } if (PHP_SAPI === 'cli' && realpath($_SERVER['SCRIPT_FILENAME'] ?? '') === __FILE__) { $api = new IntegrationApiClient((string) getenv('INTAPI_BASE'), (string) getenv('INTAPI_TOKEN'), (string) getenv('INTAPI_SECRET')); // 1. Token and signing check. print_r($api->request('POST', 'ping', [], ['hello' => 'world'])); // 2. Stock: always try with dry_run first. print_r($api->request('PUT', 'stock', [], [ 'dry_run' => true, 'items' => [['sku' => 'ABC-123', 'warehouse_id' => 1, 'quantity' => 5]], ], 'stock-' . date('YmdHis'))); }