beatNow(); // '@523' * print_r($bt->timeToBeat('2026-06-15T14:30:00+02:00')); * print_r($bt->beatToTime(523.45)); * * // Proof of existence (only the SHA-256 is sent — your file stays local): * $digest = BeatTime::sha256File('contract.pdf'); * $bt->stamp($digest); * echo $bt->certUrl($digest); // downloadable PDF certificate * * No API key, no limits beyond fair-use rate limiting. Use freely. */ class BeatTime { private string $base; private int $timeout; public function __construct(string $base = 'https://beattime.live', int $timeout = 10) { $this->base = rtrim($base, '/'); $this->timeout = $timeout; } // --- low-level ------------------------------------------------------- private function get(string $path, array $params = []): array { $params = array_filter($params, fn ($v) => $v !== null); $url = $this->base . $path . ($params ? ('?' . http_build_query($params)) : ''); return $this->request('GET', $url, null); } private function post(string $path, array $data): array { return $this->request('POST', $this->base . $path, json_encode($data)); } private function request(string $method, string $url, ?string $body): array { $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_RETURNTRANSFER => true, CURLOPT_TIMEOUT => $this->timeout, CURLOPT_CUSTOMREQUEST => $method, ]); if ($body !== null) { curl_setopt($ch, CURLOPT_POSTFIELDS, $body); curl_setopt($ch, CURLOPT_HTTPHEADER, ['Content-Type: application/json']); } $res = curl_exec($ch); if ($res === false) { $err = curl_error($ch); curl_close($ch); throw new RuntimeException("BeatTime request failed: $err"); } curl_close($ch); return json_decode($res, true) ?? []; } // --- time ------------------------------------------------------------ /** Current time: ['beat'=>'@523','beat_centi'=>'@523.45','beats'=>..,'utc'=>..]. */ public function now(): array { return $this->get('/api/now/'); } /** Just the current '@NNN'. */ public function beatNow(): string { return $this->now()['beat']; } /** Convert an instant (ISO 8601; no timezone = UTC) to its @beat. */ public function timeToBeat(string $iso8601): array { return $this->get('/api/convert/', ['at' => $iso8601]); } /** Convert a beat [0,1000) to UTC time. Optional $date (YYYY-MM-DD) and * $tzOffset (minutes, e.g. 120 for UTC+2) to also get the local time. */ public function beatToTime(float $beat, ?string $date = null, ?int $tzOffset = null): array { return $this->get('/api/convert/', ['beat' => $beat, 'date' => $date, 'tz_offset' => $tzOffset]); } /** Server time for clock sync (SNTP-style). */ public function sync(): array { return $this->get('/api/sync/'); } public function health(): array { return $this->get('/api/health/'); } // --- proof of existence --------------------------------------------- /** Record a 64-char lowercase hex SHA-256 in the public proof log. * Idempotent: the first stamp wins. The file never leaves your machine. */ public function stamp(string $digest): array { return $this->post('/api/proof/stamp', ['digest' => $digest]); } /** Look up a digest: timestamp, inclusion proof, signature, anchors. */ public function verify(string $digest): array { return $this->get('/api/proof/verify', ['digest' => $digest]); } /** URL of the downloadable PDF certificate for a stamped digest. */ public function certUrl(string $digest): string { return $this->base . '/api/proof/cert/' . $digest; } /** URL of the public verification page for a digest. */ public function verifyUrl(string $digest): string { return $this->base . '/proof/?h=' . $digest; } /** SHA-256 of a file — feed it to stamp()/verify(). */ public static function sha256File(string $path): string { return hash_file('sha256', $path); } }