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'];
public function __construct(private readonly string $key = '')
{
$this->key = $key ?: 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
{
$headers = $idempotencyKey ? ['Idempotency-Key: ' . $idempotencyKey] : [];
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.