Developer guide

OFAC API in PHP: onboarding with cURL

Add an OFAC API check to a PHP onboarding form with cURL. Keep the key on the server and link SDN results to the customer’s review.

250 free live screenings. One lifetime allowance. No credit card required.
On this page

Screen OFAC subjects from a PHP application

Keep the cURL request in a backend service used by the authenticated controller or queue worker. Validate the legal subject and selected coverage before creating the operation. Store the provider key in server-side configuration and avoid passing it through a form, query string or browser response.

When adapting the example to a framework such as Laravel or Symfony, retain the same failure semantics. A framework HTTP helper may represent HTTP errors differently from cURL. Confirm the status and response shape explicitly rather than assuming successful decoding means the screening completed.

Prepare the synthetic exercise

Save the sandbox request beside the PHP script and call it from a trusted command line before wiring an onboarding form. The invented person lets you inspect the controller response without copying a customer into a preview. The production credential belongs in backend configuration, never in a hidden form field.

PHP setup and request files
RequirementValue or action
RuntimeUse 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.
Client fileSave the client as screen.php
Screening endpointPOST /api/v1/screenings
SANCTIONSKIT_API_KEYSandbox key; screenings:write and results:read scopes
SCREENING_OPERATION_IDPersist one operation ID; reuse only with the identical request body
request.jsonMira Calder: invented subject; sandbox@1: synthetic records
request.json: synthetic sandbox request
{
  "subject": {
    "name": "Mira Calder",
    "entityType": "person",
    "birthDate": "1984"
  },
  "package": "sandbox@1",
  "retention": "standard"
}

Implement the PHP request boundary

The cURL example checks HTTP status separately from JSON decoding. Preserve both checks when moving it into Laravel or Symfony: a decoded error body is still a failed screening. Keep the user-facing form error separate from the private provider diagnostics.

PHP: authenticated screening client
<?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.');
    $raw = file_get_contents($argv[1] ?? 'request.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 the sandbox request
php screen.php request.json

Confirm OFAC scope before production

A PHP form labeled OFAC should say whether it requests ofac-sdn alone or also requires non-SDN sources. The sample selects SDN. Validate source availability in the backend before enabling submission, and keep required coverage fixed when an upstream list is unavailable.

Production source-discovery contract
CheckRequired evidence
GET /api/v1/sourcesProduction key with sources:read; response data array
idofac-sdn
availability / freshavailable / true
capabilitiesContains the requested subject.entityType
currentVersionActive version; retrieved_at and last_checked_at are not publication dates
lastImportMay describe an import that is not the active version
Missing or ineligible sourceInspect disabledReason; required coverage stays incomplete
Displayed clientAdd these discovery checks before its production call
Read source discovery with your production key
curl --fail-with-body --max-time 25 --header "Authorization: Bearer $SANCTIONSKIT_API_KEY" "https://www.sanctionskit.com/api/v1/sources"

Select ofac-sdn in the production request

Explicit change from the sandbox exercise
SettingProduction request
KeySeparate production credential with screenings:write
Coveragesources: ["ofac-sdn"]; omit package
Operation IDNew ID for the changed body and environment
UsageProduction access and screening allowance apply
SubjectOnly lawfully held, known identifying facts; no invented date precision
request.json: explicit ofac-sdn selection
{
  "subject": {
    "name": "Mira Calder",
    "entityType": "person",
    "birthDate": "1984"
  },
  "sources": [
    "ofac-sdn"
  ],
  "retention": "standard"
}

Interpret OFAC evidence in this workflow

Use data.id as the link from the form submission to retained screening evidence. A controller can return a review-required state for data.matches without embedding raw names and identifiers in its error message. data.coverage records the actual source used by that operation.

OFAC candidate fields to retain
Fields in data.matches[]Review use
record.sourceId and record.idKeep the SDN source and stable record reference attached to the candidate.
record.designations[].programs and legalReferencesShow the listed program and legal references; do not turn every non-SDN restriction into an asset freeze.
record.names, identifiers and birthDatesCompare the supplied identity with aliases and available identifiers, preserving partial-date precision.
  • data.id; data.coverage; data.versions; supplied disclaimer
  • data.matches[]: record, assessment, evidence, conflicts
  • GET /api/v1/results/{id}: results:read scope
  • Retained evidence and case review

Verify the application behavior

Recovery rules for the same saved operation
ConditionNext action
401 / 403Correct credentials or scopes before retrying
coverage_unavailable / stale_coverageKeep required coverage incomplete; no silent source substitution
Timeout after submissionRecover with the original Idempotency-Key and identical body
409Investigate an operation-key conflict
429 / 503Follow the specific error and Retry-After; an allowance cap may require a plan or usage change
Contract and safe diagnostic references
Operational reference
ReferenceUse
error.code / error.requestIdSafe recovery diagnostics; never log the submitted subject, key or full candidate response
potential_match / no_matchCompleted comparison outcomes; neither is an automatic business approval
  • Return valid JSON with an HTTP 403 and confirm the controller rejects it.
  • Retry a timed-out form submission with the stored operation key.
  • Check the rendered form and browser response for leaked credentials or candidate evidence.

Sources and references

Frequently asked questions

Which PHP extension does the OFAC example require?

Use PHP 8.2 or later with cURL enabled. No Composer package is required for the displayed script. If a framework wraps cURL, retain explicit HTTP-status and response-contract checks in the service boundary.

Can a PHP form let the user skip an unavailable OFAC list?

Only a deliberate, authorized policy change should alter required sources. A submit handler must not silently delete the missing list and return success under the old coverage label.

Can I copy this OFAC request into Laravel or Symfony?

Yes, but check how your framework handles HTTP errors compared with cURL. Validate the status and response shape before treating a check as complete. Keep the key server-side and validate the submitted identity and permitted coverage.

Why can an OFAC response decode successfully but still fail in PHP?

Error responses are also JSON. Check the HTTP code and expected screening fields before reading the outcome. An error object or missing coverage must remain incomplete rather than becoming an empty list of matches.