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.

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.
| Requirement | Value or action |
|---|---|
| Runtime | Use .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 file | Save the client as Program.cs |
| Screening endpoint | POST /api/v1/screenings |
| SANCTIONSKIT_API_KEY | Sandbox key; screenings:write and results:read scopes |
| SCREENING_OPERATION_ID | Persist one operation ID; reuse only with the identical request body |
| request.json | Mira Calder: invented subject; sandbox@1: synthetic records |
{
"subject": {
"name": "Mira Calder",
"entityType": "person",
"birthDate": "1984"
},
"package": "sandbox@1",
"retention": "standard"
}Add the project configuration
<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.
// .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
dotnet run --project Screen.csproj -- request.jsonConfirm 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.
| Check | Required evidence |
|---|---|
| GET /api/v1/sources | Production key with sources:read; response data array |
| id | ofac-sdn |
| availability / fresh | available / true |
| capabilities | Contains the requested subject.entityType |
| currentVersion | Active version; retrieved_at and last_checked_at are not publication dates |
| lastImport | May describe an import that is not the active version |
| Missing or ineligible source | Inspect disabledReason; required coverage stays incomplete |
| Displayed client | Add these discovery checks before its production call |
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
| Setting | Production request |
|---|---|
| Key | Separate production credential with screenings:write |
| Coverage | sources: ["ofac-sdn"]; omit package |
| Operation ID | New ID for the changed body and environment |
| Usage | Production access and screening allowance apply |
| Subject | Only lawfully held, known identifying facts; no invented date precision |
{
"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.
| Fields in data.matches[] | Review use |
|---|---|
| record.sourceId and record.id | Keep the SDN source and stable record reference attached to the candidate. |
| record.designations[].programs and legalReferences | Show the listed program and legal references; do not turn every non-SDN restriction into an asset freeze. |
| record.names, identifiers and birthDates | Compare 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
| Condition | Next action |
|---|---|
| 401 / 403 | Correct credentials or scopes before retrying |
| coverage_unavailable / stale_coverage | Keep required coverage incomplete; no silent source substitution |
| Timeout after submission | Recover with the original Idempotency-Key and identical body |
| 409 | Investigate an operation-key conflict |
| 429 / 503 | Follow the specific error and Retry-After; an allowance cap may require a plan or usage change |
Contract and safe diagnostic references
| Reference | Use |
|---|---|
| error.code / error.requestId | Safe recovery diagnostics; never log the submitted subject, key or full candidate response |
| potential_match / no_match | Completed 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
Authority and technical sources
SanctionsKit documentation
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.