Developer guide

U.S. CSL API in Go: screening and error handling

Build U.S. CSL screening in Go with a complete HTTP example, explicit source selection, idempotency, safe failures, and review handoffs.

On this page

What this Go integration does

The U.S. Consolidated Screening List combines several export-related screening sources. Its component lists have different purposes and implications. A CSL result should not be presented as a uniform OFAC designation.

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 Go 1.22 or later. The example uses only the standard library and can run as a small command-line program or be adapted into a backend service.

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.go. 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.

// Go 1.22+. Standard library only. Backend or trusted CLI.
package main

import (
    "bytes"
    "encoding/json"
    "errors"
    "fmt"
    "io"
    "net/http"
    "os"
    "strings"
    "time"
)

func run() error {
    key, operation := os.Getenv("SANCTIONSKIT_API_KEY"), os.Getenv("SCREENING_OPERATION_ID")
    if key == "" || operation == "" { return errors.New("set the API key and a persistent operation ID") }
    if !strings.HasPrefix(key, "sk_test_") && !(strings.HasPrefix(key, "sk_live_") && os.Getenv("ALLOW_LIVE_SCREENING") == "yes") {
        return errors.New("use a test key, or explicitly authorize a live screening")
    }
    path := "../request.sandbox.json"
    if len(os.Args) > 1 { path = os.Args[1] }
    raw, err := os.ReadFile(path)
    if err != nil { return errors.New("cannot read request file") }
    if len(raw) > 64*1024 { return errors.New("request file exceeds 64 KiB") }
    var body map[string]json.RawMessage
    if json.Unmarshal(raw, &body) != nil { return errors.New("request is not a JSON object") }
    _, sources := body["sources"]; _, pack := body["package"]
    if len(body["subject"]) == 0 || sources == pack { return errors.New("supply a subject and exactly one of sources or package") }
    request, err := http.NewRequest(http.MethodPost, "https://www.sanctionskit.com/api/v1/screenings", bytes.NewReader(raw))
    if err != nil { return err }
    request.Header.Set("Authorization", "Bearer "+key)
    request.Header.Set("Content-Type", "application/json")
    request.Header.Set("Idempotency-Key", operation)
    client := &http.Client{
        Timeout: 25 * time.Second,
        CheckRedirect: func(req *http.Request, via []*http.Request) error { return http.ErrUseLastResponse },
    }
    response, err := client.Do(request)
    if err != nil { return errors.New("transport incomplete; check the operation before retrying") }
    defer response.Body.Close()
    const limit = 8 * 1024 * 1024
    payload, err := io.ReadAll(io.LimitReader(response.Body, limit+1))
    if err != nil || len(payload) > limit { return errors.New("incomplete or oversized response") }
    if response.StatusCode < 200 || response.StatusCode >= 300 { return fmt.Errorf("screening incomplete: HTTP %d", response.StatusCode) }
    var envelope struct {
        Data *struct {
            ID string `json:"id"`
            Status string `json:"status"`
            Matches []json.RawMessage `json:"matches"`
        } `json:"data"`
    }
    if json.Unmarshal(payload, &envelope) != nil || envelope.Data == nil { return errors.New("invalid response envelope") }
    data := envelope.Data
    if data.ID == "" || (data.Status != "potential_match" && data.Status != "no_match") || data.Matches == nil {
        return errors.New("incomplete response: unexpected screening shape")
    }
    count := len(data.Matches)
    if (data.Status == "no_match" && count != 0) || (data.Status == "potential_match" && count == 0) {
        return errors.New("incomplete response: inconsistent screening shape")
    }
    return json.NewEncoder(os.Stdout).Encode(map[string]any{"id":data.ID,"status":data.Status,"candidateCount":count})
}
func main() {
    if err := run(); err != nil { fmt.Fprintln(os.Stderr, err.Error()); os.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.

Use a client deadline, close the response body, and reject redirects. Decode matches into a slice and check for nil: a missing or null field must not be treated as a valid empty collection. Reuse a configured client in a long-running service rather than creating a transport for every request.

go run screen.go request.json

Change to the required production coverage

The documented SanctionsKit identifier is us-csl. Confirm the current coverage and preserve each returned record’s underlying list identity. Separate-source overlap can occur when a portfolio also selects other U.S. lists; retain provenance rather than silently merging the evidence.

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": [
    "us-csl"
  ]
}

Preserve the source details a reviewer needs

Keep the component authority, list identity, record reference, and restriction context available to the trade reviewer. Screening a party does not classify goods, determine end use, establish a license requirement, or replace a broader export-control assessment.

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 organization subjects and candidates from different component lists. Confirm that the application does not transform every CSL candidate into an SDN finding, and that it can retain separate records with similar names from different authorities.

Use httptest with a test-only endpoint substitution or an injected RoundTripper. Include absent matches, null matches, an empty JSON array, a timeout, and a response body that exceeds the configured limit.

  • 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.

Official references