Backend
PHP
cURL from the standard library, and Guzzle where a framework already ships it.
For the browser half of a PHP app, see the Laravel guide. This page is the server half: calls your PHP makes itself.
Plain PHP
<?php
final class Pulse
{
private const BASE = 'https://api.thepulseapi.com/api/v1';
public function __construct(
private readonly string $apiKey,
private readonly string $organizationId,
) {}
public function track(string $sessionId, array $signals): array
{
return $this->post('/track', ['sessionId' => $sessionId, 'signals' => $signals]);
}
public function rankedCatalog(string $sessionId, int $limit = 24): array
{
return $this->post('/catalog/ranked', ['sessionId' => $sessionId, 'limit' => $limit]);
}
private function post(string $path, array $body): array
{
// organizationId is read from the body rather than the key, and is required.
$body['organizationId'] = $this->organizationId;
$ch = curl_init(self::BASE . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_TIMEOUT => 5,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'x-api-key: ' . $this->apiKey,
],
// JSON_THROW_ON_ERROR, so a body that failed to encode is an exception
// rather than the string "false" posted to the API.
CURLOPT_POSTFIELDS => json_encode($body, JSON_THROW_ON_ERROR),
]);
$response = curl_exec($ch);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);
if ($response === false || $status >= 300) {
throw new RuntimeException("PULSE {$path}: {$status}");
}
return json_decode($response, true, 512, JSON_THROW_ON_ERROR);
}
}Laravel
Guzzle is already there, wrapped as Http.
<?php
namespace App\Services;
use Illuminate\Support\Facades\Http;
class Pulse
{
public function rankedCatalog(string $sessionId, int $limit = 24): array
{
return Http::withHeaders(['x-api-key' => config('services.pulse.key')])
->timeout(5)
->post('https://api.thepulseapi.com/api/v1/catalog/ranked', [
'sessionId' => $sessionId,
'organizationId' => config('services.pulse.org'),
'limit' => $limit,
])
->throw()
->json();
}
}'pulse' => [
'key' => env('PULSE_API_KEY'),
'org' => env('PULSE_ORG_ID'),
],Keep it out of the view
A ranking call inside a Blade template runs on every render with no timeout you control and no way to fall back. Resolve it in the controller, pass the result in, and render catalogue order when it fails.

