PHP SDK
Reference for constaia/constaia-php on PHP 8.1+: Composer install, inputs, options, methods, pagination, exceptions, retries and webhooks.
constaia/constaia-php is the official PHP SDK. It requires PHP ≥ 8.1 with ext-curl and ext-json; no Guzzle
or other dependencies. It includes a Laravel integration (auto-discovered). Current version: 0.2.0.
composer require constaia/constaia-phpThe key lives on the server
Never print the key in a page or pass it to JavaScript. In the browser, use the widget pointing at a route of your backend.
Configuration
<?php
require __DIR__ . '/vendor/autoload.php';
$constaia = new \Constaia\Client(null, [ // null → reads CONSTAIA_API_KEY
'base_url' => 'https://api.constaia.com', // or CONSTAIA_BASE_URL
'timeout' => 60, // seconds per attempt
'max_retries' => 2, // on 408, 409, 429, 5xx and network errors
'max_concurrency' => 4, // parallel requests in analyzeMany()
'headers' => [], // extra headers on every request
]);Without a key (argument or CONSTAIA_API_KEY), the constructor throws AuthenticationException with code
missing_api_key.
Accepted files
| Input | Sent as |
|---|---|
Local path '/tmp/id.jpg' | multipart (file + options JSON) |
Open stream (fopen(…), php://memory) | multipart |
SplFileInfo, including Laravel or Symfony UploadedFile (uses the original name and type) | multipart |
URL 'https://…' | JSON file_url |
['base64' => '…', 'filename' => 'id.jpg'] | JSON file_base64 + filename |
['file_url' => 'https://…'] | JSON file_url |
Use the filename option to force the name (useful with streams and, in test mode, to pick
the scenario).
Options
Options use the same snake_case names as the API. Every method also accepts request options that are not sent to
the API: filename, idempotency_key, timeout, max_retries and headers.
$analysis = $constaia->analyze($path, [
'expect' => ['es_dni', 'es_nie', 'passport'], // without expect, verdict is null
'extract' => true, // or your own JSON Schema
'checks' => [
'not_expired' => true,
'min_age_years' => 18,
'holder' => ['full_name' => 'María García López', 'document_number' => '12345678Z'],
],
'storage' => 'none', // none | temporary | persistent
'ttl_hours' => 24, // with storage=temporary
'keep_results' => true,
'async' => false, // true → 202 and webhook
'export' => ['xlsx'], // signed URLs in $analysis->exports
'metadata' => ['registration_id' => '123'],
'language' => 'en', // es | en | pt | fr
// Request options (not sent to the API)
'filename' => 'dni_valid.jpg',
'idempotency_key' => 'registration-123-id',
'timeout' => 90,
]);Every option is explained in POST /v1/analyze and Checks.
Reading the result
analyze() returns a Constaia\Analysis. Objects can be read as properties or as arrays; a missing key returns
null without warnings.
$analysis->verdict->status; // "valid" | "invalid" | "review"
$analysis['verdict']['status']; // same, array style
$analysis->fields->document_number->value;
$analysis->verdictStatus(); // null if you sent no expect
$analysis->isValid();
$analysis->isInvalid();
$analysis->needsReview();
$analysis->isCompleted(); // false if the API answered 202
$analysis->field('birth_date'); // shortcut for fields.birth_date.value
foreach ($analysis->verdict->reasons as $reason) {
echo $reason->code, ' (', $reason->severity, '): ', $reason->message, PHP_EOL;
}
$analysis->toArray();
json_encode($analysis); // JsonSerializable
$constaia->lastResponse->status; // HTTP status of the last call
$constaia->lastResponse->requestId; // X-Request-Id: quote it when contacting supportMethods
// Classify (0.2 credits)
$c = $constaia->classify($input, ['expect' => 'invoice']);
$c->document->type;
$c->candidates[0]->confidence;
// Stored analyses
$constaia->analyses->get('an_01J…');
$page = $constaia->analyses->list([
'limit' => 50,
'status' => 'completed',
'type' => 'es_dni',
'metadata' => ['registration_id' => '123'],
]);
$constaia->analyses->delete('an_01J…'); // { id, object, deleted: true }
$bytes = $constaia->analyses->export('an_01J…', 'xlsx'); // bytes
$constaia->analyses->export('an_01J…', 'csv', '/tmp/an.csv'); // saves to disk and returns the path
// Batches (up to 100 documents, always asynchronous)
$batch = $constaia->batches->create([
'items' => [
['file_url' => 'https://example.com/a.pdf'],
['file_url' => 'https://example.com/b.pdf', 'options' => ['checks' => ['expected_amount' => 45]]],
],
'options' => ['expect' => 'payment_receipt', 'export' => ['xlsx']],
]);
$batch = $constaia->batches->create([
'files' => ['/tmp/a.pdf', '/tmp/b.pdf'], // multipart files[]
'options' => ['expect' => 'invoice'],
]);
$constaia->batches->get($batch->id);
// Catalogue
$constaia->documentTypes->list(['language' => 'en']);
$constaia->documentTypes->get('es_dni');
// Webhook endpoints (the secret is only returned on creation)
$endpoint = $constaia->webhookEndpoints->create([
'url' => 'https://example.com/webhooks/constaia',
'events' => ['analysis.completed', 'analysis.review_required', 'batch.completed'],
]);
$endpoint->secret; // whsec_…
$constaia->webhookEndpoints->list();
$constaia->webhookEndpoints->get('we_01J…');
$constaia->webhookEndpoints->delete('we_01J…');
// Balance and usage
$constaia->balance();
$constaia->usage(['from' => '2026-09-01', 'to' => '2026-09-30']);Several documents in parallel
analyzeMany() analyzes several documents at once with curl_multi, with at most max_concurrency requests in
flight (default 4). It preserves the array keys and, instead of throwing, returns the exception at the position of
the document that failed.
use Constaia\Exception\ConstaiaException;
$results = $constaia->analyzeMany(
['ana' => '/tmp/id_ana.jpg', 'luis' => '/tmp/id_luis.jpg'],
['expect' => 'es_dni', 'checks' => ['not_expired' => true]],
);
foreach ($results as $who => $result) {
if ($result instanceof ConstaiaException) {
echo "$who: error {$result->getErrorCode()}", PHP_EOL;
continue;
}
echo "$who: ", $result->verdictStatus(), PHP_EOL;
}For more than a handful of documents, or when you don't need the answer right away, use batches.
Pagination
analyses->list() returns a Constaia\Collection with the first page. foreach walks that page only;
autoPagingIterator() (or all()) walks every page, fetching the next ones with starting_after.
$page = $constaia->analyses->list(['limit' => 100, 'type' => 'es_dni']);
$page->data(); // array of the current page
$page->hasMore();
$next = $page->nextPage(); // null when there are no more
foreach ($page->autoPagingIterator() as $analysis) {
echo $analysis->id, ' ', $analysis->verdictStatus(), PHP_EOL;
}More in Pagination.
Webhooks
Constaia\Webhook::verify() checks the Standard Webhooks signature against the raw body and returns the event
as an associative array. It accepts headers from getallheaders() or the $_SERVER array (HTTP_WEBHOOK_ID…).
Default tolerance: 300 seconds (fourth argument).
<?php
require __DIR__ . '/vendor/autoload.php';
use Constaia\Exception\SignatureVerificationException;
use Constaia\Webhook;
$payload = file_get_contents('php://input');
try {
$event = Webhook::verify($payload, $_SERVER, getenv('CONSTAIA_WEBHOOK_SECRET'));
} catch (SignatureVerificationException $e) {
http_response_code(400);
exit;
}
switch ($event['type']) {
case 'analysis.completed':
case 'analysis.review_required':
// $event['data'] is the analysis
break;
case 'analysis.failed':
case 'batch.completed':
case 'credits.low':
break;
}
http_response_code(204);For your tests, Webhook::headers($payload, $secret) builds signed headers like Constaia's and
Webhook::sign($id, $timestamp, $payload, $secret) just the signature. More in Webhooks.
Exceptions
All extend Constaia\Exception\ConstaiaException and expose getType(), getErrorCode(), getParam(),
getRequestId() and getHttpStatus() (also as properties type, errorCode, param, requestId, httpStatus).
| Exception | When |
|---|---|
InvalidRequestException | 400, 409, 413, 415 and 422 (invalid file or option, size, type, idempotency). Also locally when a file can't be read. |
AuthenticationException | 401, or no key configured. |
InsufficientCreditsException | 402. |
PermissionException | 403. |
NotFoundException | 404. |
RateLimitException | 429 after retries; getRetryAfter() returns the Retry-After seconds. |
ApiException | 5xx after retries (for example 503 live_mode_unavailable). |
ConnectionException | Network error or timeout. |
SignatureVerificationException | Invalid webhook signature. |
use Constaia\Exception;
try {
$analysis = $constaia->analyze($path, ['expect' => 'es_dni']);
} catch (Exception\InsufficientCreditsException $e) {
// Alert whoever manages the account and retry later
} catch (Exception\RateLimitException $e) {
sleep($e->getRetryAfter() ?? 1);
} catch (Exception\InvalidRequestException $e) {
echo $e->getErrorCode(), ' ', $e->getParam(), ': ', $e->getMessage();
} catch (Exception\ConstaiaException $e) {
error_log(sprintf('Constaia %s %s (%s)', $e->getHttpStatus(), $e->getErrorCode(), $e->getRequestId()));
}Every code is listed in Errors.
Retries and idempotency
- Retries 408, 409
idempotency_in_progress, 429, 5xx (never 501) and network errors up tomax_retriestimes (0disables them), with exponential backoff and jitter, honouringRetry-After. $constaia->lastRateLimit(Constaia\RateLimit:limit,remaining,reset,policy,retryAfter) keeps the limit headers of the last response.- Every
POSTcarries anIdempotency-Key(UUID v4) kept across retries: a retry never charges twice. Pass your own withidempotency_keyto dedupe across processes. See Idempotency.
Laravel and WordPress
- Laravel: the package registers
Constaia\Clientas a singleton and theConstaiafacade. SetCONSTAIA_API_KEYandCONSTAIA_WEBHOOK_SECRETin.envand, optionally, publish the config withphp artisan vendor:publish --tag=constaia-config. Full guide in Laravel. - WordPress: the SDK repository includes an example plugin with a shortcode and a REST route. See WordPress.
Next steps
JavaScript and TypeScript SDK
Reference for @constaia/sdk on Node.js, Bun, Deno and edge runtimes. Installation, options, methods, errors, retries and webhook verification.
Python SDK
Reference for the official constaia SDK for Python 3.9+: sync and async clients, inputs, options, pagination, errors, concurrency and webhooks.