Developer guide
Canadian sanctions API in PHP: screening and error handling
Build Canadian sanctions screening in PHP with a complete HTTP example, explicit source selection, idempotency, safe failures, and review handoffs.
On this page
What this PHP integration does
Canada’s Consolidated Canadian Autonomous Sanctions List is an administrative consolidation associated with specified Canadian sanctions legislation. It is not a substitute for the regulations and is not synonymous with every UN-related Canadian measure.
The client submits a subject, validates the response, and prints only the screening ID, status, and candidate count. It does not approve a customer, resolve an identity, or make a legal decision. A nonzero exit leaves the local operation incomplete.
Prepare the runtime and a synthetic request
Use PHP 8.2 or later with the cURL extension enabled. Run the script in a backend or trusted CLI. This example does not require Composer or a provider SDK.
Save the following as request.json. Mira Calder is an invented subject and sandbox@1 uses synthetic fixtures, not a current production sanctions search. Set SANCTIONSKIT_API_KEY to your test key in the process environment. Set SCREENING_OPERATION_ID to a new persistent identifier for this operation, and reuse it only for an unchanged retry.
{
"subject": {
"name": "Mira Calder",
"entityType": "person",
"birthDate": "1984"
},
"package": "sandbox@1",
"retention": "standard"
}Send the request and validate the result
Save the client as screen.php. The endpoint is fixed to SanctionsKit’s HTTPS API. The example rejects redirects, requires explicit authorization before using a live key, and does not retry automatically. Production integrations should preserve the operation state before the first network call.
<?php
// PHP 8.2+, ext-curl. Run in a backend or trusted CLI, never in browser code.
declare(strict_types=1);
try {
$key = getenv('SANCTIONSKIT_API_KEY') ?: '';
$operation = getenv('SCREENING_OPERATION_ID') ?: '';
if ($key === '' || $operation === '') throw new RuntimeException('Set the API key and a persistent operation ID.');
if (!str_starts_with($key, 'sk_test_') && !(str_starts_with($key, 'sk_live_') && getenv('ALLOW_LIVE_SCREENING') === 'yes')) {
throw new RuntimeException('Use a test key, or explicitly authorize a live screening.');
}
$raw = file_get_contents($argv[1] ?? '../request.sandbox.json');
if ($raw === false || strlen($raw) > 65536) throw new RuntimeException('Request file is missing or too large.');
$body = json_decode($raw, true, 512, JSON_THROW_ON_ERROR);
if (!is_array($body) || !isset($body['subject']) || (array_key_exists('sources', $body) === array_key_exists('package', $body))) {
throw new RuntimeException('Supply a subject and exactly one of sources or package.');
}
$received = '';
$curl = curl_init('https://www.sanctionskit.com/api/v1/screenings');
if ($curl === false) throw new RuntimeException('Unable to initialize HTTP client.');
curl_setopt_array($curl, [
CURLOPT_POST => true, CURLOPT_POSTFIELDS => json_encode($body, JSON_THROW_ON_ERROR),
CURLOPT_HTTPHEADER => ['Authorization: Bearer '.$key, 'Content-Type: application/json', 'Idempotency-Key: '.$operation],
CURLOPT_FOLLOWLOCATION => false, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 25,
CURLOPT_SSL_VERIFYPEER => true, CURLOPT_SSL_VERIFYHOST => 2,
CURLOPT_WRITEFUNCTION => static function ($handle, string $chunk) use (&$received): int {
if (strlen($received) + strlen($chunk) > 8 * 1024 * 1024) return 0;
$received .= $chunk;
return strlen($chunk);
},
]);
$ok = curl_exec($curl);
$status = (int) curl_getinfo($curl, CURLINFO_RESPONSE_CODE);
curl_close($curl);
if ($ok === false) throw new RuntimeException('Transport failed or response exceeded size limit.');
$envelope = json_decode($received, false, 512, JSON_THROW_ON_ERROR);
if ($status < 200 || $status >= 300) throw new RuntimeException('Screening incomplete: HTTP '.$status.'.');
$data = is_object($envelope) ? ($envelope->data ?? null) : null;
if (!is_object($data) || !is_string($data->id ?? null) || $data->id === '' ||
!in_array($data->status ?? null, ['potential_match', 'no_match'], true) ||
!is_array($data->matches ?? null)) {
throw new RuntimeException('Incomplete response: unexpected screening shape.');
}
$count = count($data->matches);
if (($data->status === 'no_match' && $count !== 0) || ($data->status === 'potential_match' && $count === 0)) {
throw new RuntimeException('Incomplete response: inconsistent screening shape.');
}
echo json_encode(['id' => $data->id, 'status' => $data->status, 'candidateCount' => $count], JSON_THROW_ON_ERROR).PHP_EOL;
} catch (Throwable $error) {
// No raw response, submitted identity, or credential is logged.
fwrite(STDERR, 'Screening did not complete: '.get_class($error).'. Check the operation before retrying.'.PHP_EOL);
exit(1);
}Run the client
Run this from the directory containing request.json and the project files. Keep the API key out of committed files and shell tracing. The tutorial’s request-file limit is 64 KiB and its response parsing limit is 8 MiB; these are client safeguards, not claims about API service limits.
Decode the response as objects so an empty JSON object cannot be confused with an empty JSON array. Keep TLS certificate verification enabled, disable redirect following, and bound cURL response writes. JSON_THROW_ON_ERROR prevents a parse failure from being mistaken for null data.
php screen.php request.jsonChange to the required production coverage
The documented SanctionsKit identifier is ca-autonomous. Check its availability and supported subject types before selecting it. Preserve the underlying regulation or authority context when deciding what a candidate means.
For an authorized production operation, replace the package field with the sources array shown below, supply the actual lawfully held subject details, use a live key, and set ALLOW_LIVE_SCREENING=yes. Keep the complete request in request.json and create a new operation ID. Do not send both sources and package. The fragment below is a coverage change, not a complete request.
{
"sources": [
"ca-autonomous"
]
}Preserve the source details a reviewer needs
Do not collapse French and English naming variants, aliases, or differently formatted identifiers into one unexplained display string. Keep the original source wording and provenance alongside normalized fields so a reviewer can trace the comparison.
Retain the screening ID with your internal subject reference. Retrieve the complete evidence through the documented retained-result workflow when authorized; the console summary deliberately does not print raw identity details. A later analyst decision must not overwrite the original screening outcome.
Handle failures and test the source-specific edge cases
Treat HTTP errors, timeouts, malformed JSON, missing fields, unknown statuses, and unavailable required coverage as incomplete checks. A 401 or 403 needs credential or scope correction. A 409 needs idempotency-conflict handling. A 429 needs its specific rate-limit or usage-cap treatment; it is not a no-match result.
Test accented characters, multilingual organization names, and a candidate whose missing date cannot distinguish identity. Check that the application describes autonomous-list coverage rather than claiming every Canadian restriction was assessed.
Test a literal matches: {} response alongside matches: []. PHP associative decoding can otherwise make those look similar. Also test a missing cURL extension, transport failure, HTTP error, and a changed idempotency payload.
- Verify both valid result states and inconsistent status/matches combinations.
- Retry an accepted operation only with its original key and unchanged payload.
- Keep missing identity facts unknown instead of inventing contradictory values.
- Route potential matches into review before the separate business decision.
- Keep secrets, submitted names, and raw error bodies out of routine logs.