Developer guide

OFAC API in C# / .NET: ASP.NET onboarding

Connect an ASP.NET service to the OFAC API with C# and HttpClient. Run a sandbox check, preserve retry keys, and link SDN evidence to review.

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

Screen OFAC subjects in an ASP.NET application

Place the provider call behind an application service with a managed HttpClient lifecycle. Persist the subject, coverage and operation key before the request. Keep the key in backend configuration and return only the fields the authenticated caller is permitted to access.

A CancellationToken can stop local waiting, but it does not establish that the remote operation never completed. Recover an uncertain operation using its original key and unchanged payload under the API contract. A corrected subject needs a new operation and a traceable link to the earlier one.

Prepare the synthetic exercise

Save Program.cs, Screen.csproj and the synthetic request together, then run the .NET 8 example before adding it to an ASP.NET controller. Exercise a CancellationToken cancellation and retain the uncertain operation so a later request can recover it rather than silently start over.

C# / .NET setup and request files
RequirementValue or action
RuntimeUse .NET 8 or later. Save the program as Program.cs and the project file shown below as Screen.csproj. HttpClient and System.Text.Json are included; no provider SDK or third-party JSON library is required.
Client fileSave the client as Program.cs
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"
}

Add the project configuration

Screen.csproj
<Project Sdk="Microsoft.NET.Sdk"><PropertyGroup><OutputType>Exe</OutputType><TargetFramework>net8.0</TargetFramework><ImplicitUsings>enable</ImplicitUsings><Nullable>enable</Nullable></PropertyGroup></Project>

Implement the C# / .NET request boundary

Manage HttpClient through your application’s normal service lifecycle. The displayed client checks the response contract after the HTTP call. Keep cancellation and connection errors distinct from no_match, and do not return the provider credential or full evidence in a controller exception.

C# / .NET: authenticated screening client
// .NET 8+. Backend or trusted CLI. No third-party packages.
using System.Net.Http.Headers;
using System.Text;
using System.Text.Json;
try
{
    var key = Environment.GetEnvironmentVariable("SANCTIONSKIT_API_KEY") ?? "";
    var operation = Environment.GetEnvironmentVariable("SCREENING_OPERATION_ID") ?? "";
    if (key.Length == 0 || operation.Length == 0) throw new InvalidOperationException("Missing configuration");
    var raw = await File.ReadAllBytesAsync(args.Length > 0 ? args[0] : "request.json");
    if (raw.Length > 65536) throw new InvalidDataException("Oversized request");
    using var input = JsonDocument.Parse(raw);
    var body = input.RootElement;
    if (body.ValueKind != JsonValueKind.Object || !body.TryGetProperty("subject", out _) ||
        body.TryGetProperty("sources", out _) == body.TryGetProperty("package", out _))
        throw new InvalidDataException("Invalid request object");
    using var handler = new HttpClientHandler { AllowAutoRedirect = false };
    using var client = new HttpClient(handler) { Timeout = TimeSpan.FromSeconds(25) };
    using var deadline = new CancellationTokenSource(TimeSpan.FromSeconds(30));
    using var request = new HttpRequestMessage(HttpMethod.Post, "https://www.sanctionskit.com/api/v1/screenings");
    request.Headers.Authorization = new AuthenticationHeaderValue("Bearer", key);
    request.Headers.Add("Idempotency-Key", operation);
    request.Content = new StringContent(Encoding.UTF8.GetString(raw), Encoding.UTF8, "application/json");
    using var response = await client.SendAsync(request, HttpCompletionOption.ResponseHeadersRead, deadline.Token);
    if (!response.IsSuccessStatusCode) throw new HttpRequestException("Incomplete HTTP response");
    await using var stream = await response.Content.ReadAsStreamAsync(deadline.Token);
    using var received = new MemoryStream();
    var buffer = new byte[8192]; int read;
    while ((read = await stream.ReadAsync(buffer.AsMemory(), deadline.Token)) != 0)
    {
        if (received.Length + read > 8 * 1024 * 1024) throw new InvalidDataException("Oversized response");
        received.Write(buffer, 0, read);
    }
    using var envelope = JsonDocument.Parse(received.ToArray());
    var root = envelope.RootElement;
    if (root.ValueKind != JsonValueKind.Object || !root.TryGetProperty("data", out var data) || data.ValueKind != JsonValueKind.Object ||
        !data.TryGetProperty("id", out var id) || id.ValueKind != JsonValueKind.String || string.IsNullOrEmpty(id.GetString()) ||
        !data.TryGetProperty("status", out var state) || state.ValueKind != JsonValueKind.String ||
        !data.TryGetProperty("matches", out var matches) || matches.ValueKind != JsonValueKind.Array)
        throw new InvalidDataException("Unexpected screening response");
    var status = state.GetString(); var count = matches.GetArrayLength();
    if ((status != "potential_match" && status != "no_match") || (status == "no_match" && count != 0) || (status == "potential_match" && count == 0))
        throw new InvalidDataException("Inconsistent screening response");
    Console.WriteLine(JsonSerializer.Serialize(new { id = id.GetString(), status, candidateCount = count }));
}
catch (Exception)
{
    Console.Error.WriteLine("Screening incomplete. Check the operation before retrying.");
    Environment.ExitCode = 1;
}

Run the client

Run the sandbox request
dotnet run --project Screen.csproj -- request.json

Confirm OFAC scope before production

The production C# request names ofac-sdn. Required non-SDN lists must be added through an approved coverage change, with their own source availability checked. Keep the backend source set independent of optional fields submitted by the browser.

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

Save data.id with the onboarding operation, then use data.coverage to describe the completed check. A potential_match should lead to a review state in the controller response. HTTP 201 alone says nothing about whether the customer may proceed under the organization’s policy.

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 HTTP 201 with a potential match and keep onboarding pending.
  • Cancel local waiting after submission and recover the original request key.
  • Check that changing the subject produces a new operation rather than a key conflict.

Sources and references

Frequently asked questions

Does the C# OFAC client need a third-party JSON package?

The .NET 8 example uses built-in HttpClient and System.Text.Json. Save the supplied project file with Program.cs, then keep the provider adapter behind your application’s authorized backend boundary.

What should an ASP.NET OFAC endpoint return after cancellation?

Use an application state that preserves uncertainty about the accepted operation. A cancelled local wait does not justify returning no_match, and a blind new request key could create another screening.

Should an ASP.NET controller approve a customer on HTTP 201?

No. HTTP success means the screening completed. Read its status, route candidates to review, and check the business workflow requirements. A no-match applies to the selected sources and matching rules; it is not a customer-approval decision.

How should an ASP.NET form handle corrected identity details?

Validate the correction and create a new operation linked to the prior result. Reusing an earlier idempotency key with changed subject content is a conflict, not a way to amend retained evidence.