PHP
Submit and poll with the curl extension, for a Laravel job, a WordPress plugin or a plain script.
<?php
final class Silvertext
{
private const BASE = 'https://api.silvertext.com';
private const FINAL = ['done', 'rejected', 'failed', 'cancelled'];
private readonly string $key;
public function __construct(string $key = '')
{
$this->key = $key !== '' ? $key : (string) getenv('SILVERTEXT_API_KEY');
}
/** @return array<string,mixed>|null */
public function request(string $method, string $path, ?array $json = null, array $headers = [], int $attempts = 4): ?array
{
$delay = 1.0;
for ($attempt = 1; ; $attempt++) {
$ch = curl_init(self::BASE . $path);
$hdrs = array_merge(['Authorization: Bearer ' . $this->key, 'Content-Type: application/json'], $headers);
curl_setopt_array($ch, [
CURLOPT_CUSTOMREQUEST => $method,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HEADER => true,
CURLOPT_HTTPHEADER => $hdrs,
CURLOPT_POSTFIELDS => $json === null ? null : json_encode($json),
CURLOPT_TIMEOUT => 60,
]);
$raw = curl_exec($ch);
$status = (int) curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
$headerSize = (int) curl_getinfo($ch, CURLINFO_HEADER_SIZE);
curl_close($ch);
if ($raw !== false && $status < 400) {
$body = substr($raw, $headerSize);
return $body === '' ? null : json_decode($body, true);
}
$retryable = $raw === false || $status === 429 || $status >= 500;
if (!$retryable || $attempt >= $attempts) {
$error = $raw === false ? null : (json_decode(substr($raw, $headerSize), true)['error'] ?? null);
throw new RuntimeException(($error['code'] ?? 'network') . ': ' . ($error['message'] ?? 'request failed'), $status);
}
$retryAfter = 0;
if ($raw !== false && preg_match('/^Retry-After:\s*(\d+)/mi', substr($raw, 0, $headerSize), $m)) {
$retryAfter = (int) $m[1];
}
usleep((int) (($retryAfter > 0 ? $retryAfter : $delay + mt_rand(0, 500) / 1000) * 1_000_000));
$delay = min($delay * 2, 30);
}
}
public function createCheck(array $body, ?string $idempotencyKey = null): string
{
// Always idempotent: request() retries, and a retry must never start a second check.
$headers = ['Idempotency-Key: ' . ($idempotencyKey ?? bin2hex(random_bytes(16)))];
return $this->request('POST', '/v1/checks', $body, $headers)['id'];
}
public function getCheck(string $id, ?string $view = null, ?array $include = null): array
{
$q = $include ? '?include=' . implode(',', $include) : ($view ? '?view=' . $view : '');
return $this->request('GET', '/v1/checks/' . $id . $q);
}
public function cancelCheck(string $id): void
{
$this->request('POST', '/v1/checks/' . $id . '/cancel');
}
public function waitForCheck(string $id, int $deadlineSeconds = 600): array
{
$started = time();
$delay = 3;
while (true) {
$check = $this->getCheck($id, 'summary');
if (in_array($check['status'], self::FINAL, true)) {
return $check;
}
if (time() - $started > $deadlineSeconds) {
$this->cancelCheck($id);
throw new RuntimeException("check $id did not finish in time");
}
sleep($delay);
if (time() - $started > 60) {
$delay = min($delay + 2, 10);
}
}
}
}Using it
$st = new Silvertext();
$id = $st->createCheck(
['title' => 'Q3 launch post', 'text' => $draft, 'options' => ['citations' => false]],
"post:$postId:v$version",
);
$summary = $st->waitForCheck($id);
if ($summary['status'] !== 'done') {
throw new RuntimeException($summary['rejection']['message'] ?? $summary['status']);
}
$report = $st->getCheck($id, include: ['issues', 'sources'])['report'];In a queue-backed framework (Laravel, Symfony Messenger), split submit and read into two jobs and store the check id between them rather than sleeping inside one job. Pass an idempotency key on the submit job so a redelivery cannot create a second check.