Ironbark API Reference

Ironbark is Australia's compliance intelligence layer — machine-to-machine first. Agents and applications call Ironbark to screen entities against PEP and sanctions lists, run AML risk assessments, manage compliance cases, and export regulator-ready PDF reports.

Base URL

https://api.ironbarkaml.com.au

All endpoints are versioned under /v1. ACP endpoints use the /acp prefix.

Why Ironbark vs NameScan

FeatureNameScanIronbark
Scan + risk in one call2 API calls1 call — POST /v1/scans/full
GET scan by IDAdded in v3.1 onlyDay 1, all entity types
Credit expiry12 months (top complaint)Never expire
AU-native dataNoneASIC, AUSTRAC, ABN (Phase 2+)
AuthStatic API keyOAuth 2.0 client credentials
Rate limit transparencyUndocumentedExplicit headers on every response
PDF reportsBundled, no APIGET /v1/cases/{id}/report

Authentication

Ironbark uses OAuth 2.0 client credentials (RFC 6749 §4.4). Exchange your client_id and client_secret for a Bearer token, then pass that token in the Authorization header on every API call.

Token TTL Access tokens expire after 1 hour. Request a new token before expiry. The X-Api-Key-Expires header on every response tells you when your client registration expires.
POST /v1/oauth/token

Request must be application/x-www-form-urlencoded.

FieldTypeRequiredDescription
grant_typestringRequiredMust be client_credentials
client_idstringRequiredUUID issued at registration
client_secretstringRequiredPlaintext secret issued at registration (shown once)

Response

{
  "access_token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...",
  "token_type": "bearer",
  "expires_in": 3600,
  "scope": "scans:read scans:write cases:read cases:write reports:read"
}

Available Scopes

ScopeGrants access to
scans:readGET /v1/scans/{id}, GET /v1/scans/batch/{id}
scans:writePOST /v1/scans/full, POST /v1/scans/batch
cases:readGET /v1/cases/{id}
cases:writePOST /v1/cases, PATCH /v1/cases/{id}/decision
reports:readGET /v1/cases/{id}/report
monitoring:writeOngoing monitoring enrolment (Phase 3)

Code Examples

import requests

response = requests.post(
    "https://api.ironbarkaml.com.au/v1/oauth/token",
    data={
        "grant_type": "client_credentials",
        "client_id": "your_client_id",
        "client_secret": "your_client_secret",
    },
)
response.raise_for_status()
token = response.json()["access_token"]

# Use token in subsequent requests
headers = {"Authorization": f"Bearer {token}"}
const res = await fetch(
  "https://api.ironbarkaml.com.au/v1/oauth/token",
  {
    method: "POST",
    headers: { "Content-Type": "application/x-www-form-urlencoded" },
    body: new URLSearchParams({
      grant_type: "client_credentials",
      client_id: "your_client_id",
      client_secret: "your_client_secret",
    }),
  }
);
const { access_token } = await res.json();

// Use token in subsequent requests
const headers = { Authorization: `Bearer ${access_token}` };
require 'net/http'
require 'json'

uri = URI('https://api.ironbarkaml.com.au/v1/oauth/token')
res = Net::HTTP.post_form(uri, {
  'grant_type'  => 'client_credentials',
  'client_id'    => 'your_client_id',
  'client_secret' => 'your_client_secret'
})
token = JSON.parse(res.body)['access_token']

# Use token in subsequent requests
headers = { 'Authorization' => "Bearer #{token}" }
import java.net.URI;
import java.net.http.*;

HttpClient client = HttpClient.newHttpClient();
String body = "grant_type=client_credentials"
    + "&client_id=your_client_id"
    + "&client_secret=your_client_secret";

HttpRequest request = HttpRequest.newBuilder()
    .uri(URI.create("https://api.ironbarkaml.com.au/v1/oauth/token"))
    .header("Content-Type", "application/x-www-form-urlencoded")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response = client.send(request,
    HttpResponse.BodyHandlers.ofString());
// Parse access_token from response.body() with a JSON library
curl -X POST https://api.ironbarkaml.com.au/v1/oauth/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials&client_id=YOUR_ID&client_secret=YOUR_SECRET"
Try it — Get Token
Response

Rate Limits

Rate limits are enforced per OAuth client on a per-minute sliding window, tracked in Redis. If Redis is unavailable, rate limiting degrades gracefully — requests are allowed through rather than blocked. Limits reset at the start of each UTC minute.

PlanRequests / minuteBatch entities / request
Free1010
Starter ($99/mo)6050
Builder ($299/mo)200100
Scale ($799/mo)1,000100
EnterpriseCustom100

When a rate limit is exceeded, the API returns 429 Too Many Requests with a Retry-After header indicating the seconds until the window resets.

Rate limit response (429)

{
  "detail": "Rate limit exceeded"
}
Headers:
  Retry-After: 47

Response Headers

Every authenticated response (including error responses from authenticated routes) includes the following headers. These are injected by the API middleware, not the route handler.

HeaderExampleDescription
X-Api-Key-Expires 2026-06-30T00:00:00+00:00 ISO 8601 timestamp when your client registration expires. Value is never for non-expiring clients.
X-RateLimit-Limit 60 Maximum requests permitted in the current 1-minute window for your client.
X-RateLimit-Remaining 54 Requests remaining in the current window. Never negative.
X-RateLimit-Reset 1743123660 Unix timestamp (UTC) of when the current window resets.

Full Scan

Screen a person or organisation against PEP lists, sanctions databases, and adverse media. Returns a combined scan result + AML risk assessment in a single call. The scan_id in the response is permanent — retrieve it any time with GET /v1/scans/{scan_id}.

POST /v1/scans/full scans:write

Request Body

FieldTypeDescription
entity_typestringRequired"person" or "organisation"
namestringRequiredFull legal name. Max 512 chars.
dobstringOptionalDate of birth YYYY-MM-DD. Person only. Improves match precision.
genderstringOptional"M", "F", or "X". Person only.
nationality_codestringOptionalISO 3166-1 alpha-2 country code. Person only.
incorporation_country_codestringOptionalISO 3166-1 alpha-2. Organisation only.
country_codestringOptionalCountry of operation. Affects risk scoring.
risk_profileobjectOptionalAML context. See risk profile tables below.

Person Risk Profile (risk_profile when entity_type = "person")

FieldTypeDefaultValues
resident_statusstring"resident""resident" | "non_resident"
professionstringnulle.g. "accountant", "lawyer", "financial_advisor"
client_visit_typestring"face_to_face""face_to_face" | "non_face_to_face"

Organisation Risk Profile (risk_profile when entity_type = "organisation")

FieldTypeDefaultDescription
legal_statusstringnulle.g. "private_company", "trust", "shell"
industry_typestringnulle.g. "financial_services", "gambling", "real_estate"
has_sanctionsbooleanfalseTrue if any officer/director is on a sanctions list
is_sanctionedbooleanfalseTrue if the entity itself is on a sanctions list
is_high_risk_countrybooleanfalseTrue if entity operates in a FATF high-risk jurisdiction

Response

{
  "scan_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "entity_type": "person",
  "entity_name": "Anthony Albanese",
  "created_at": "2026-03-28T12:00:00+00:00",
  "scan_result": {
    "match_count": 0,
    "matches": [],
    "filter_applied": false,
    "filter_explanation": null,
    "source_staleness": "fresh",
    "sources_checked": ["dfat_au", "ofac_us", "un_consolidated"]
  },
  "risk_assessment": {
    "risk_level": "low",
    "risk_score": 12,
    "risk_factors": [],
    "recommendation": "Standard due diligence sufficient"
  },
  "metadata": {
    "service_id": "ironbark_scan_v1",
    "query_time_ms": 84,
    "data_sources": ["dfat_au", "ofac_us", "un_consolidated"]
  }
}

Code Examples

response = requests.post(
    "https://api.ironbarkaml.com.au/v1/scans/full",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "entity_type": "person",
        "name": "Anthony Albanese",
        "dob": "1963-03-02",
        "nationality_code": "AU",
        "risk_profile": {
            "resident_status": "resident",
            "profession": "politician",
            "client_visit_type": "non_face_to_face",
        },
    },
)
scan = response.json()
print(scan["scan_id"], scan["risk_assessment"]["risk_level"])
const scan = await fetch(
  "https://api.ironbarkaml.com.au/v1/scans/full",
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      entity_type: "person",
      name: "Anthony Albanese",
      dob: "1963-03-02",
      nationality_code: "AU",
      risk_profile: {
        resident_status: "resident",
        profession: "politician",
        client_visit_type: "non_face_to_face",
      },
    }),
  }
).then(r => r.json());
console.log(scan.scan_id, scan.risk_assessment.risk_level);
require 'net/http'
require 'json'

uri = URI('https://api.ironbarkaml.com.au/v1/scans/full')
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true

req = Net::HTTP::Post.new(uri.path, {
  'Authorization' => "Bearer #{token}",
  'Content-Type'  => 'application/json'
})
req.body = {
  entity_type: 'person',
  name: 'Anthony Albanese',
  dob: '1963-03-02',
  nationality_code: 'AU'
}.to_json

scan = JSON.parse(http.request(req).body)
String body = """
    {
        "entity_type": "person",
        "name": "Anthony Albanese",
        "dob": "1963-03-02",
        "nationality_code": "AU"
    }
    """;

HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.ironbarkaml.com.au/v1/scans/full"))
    .header("Authorization", "Bearer " + token)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();

HttpResponse<String> response = client.send(req,
    HttpResponse.BodyHandlers.ofString());
curl -X POST https://api.ironbarkaml.com.au/v1/scans/full \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "entity_type": "person",
    "name": "Anthony Albanese",
    "dob": "1963-03-02",
    "nationality_code": "AU",
    "risk_profile": {
      "resident_status": "resident",
      "profession": "politician"
    }
  }'
Try it — Full Scan
Response

Get Scan

Retrieve a previously executed scan by its ID. Idempotent — calling this multiple times never re-bills or re-executes the scan. Returns the original result exactly as delivered.

Ironbark advantage NameScan added GET-by-scanId only in API v3.1. Ironbark has this from day one on both person and organisation scans.
GET /v1/scans/{scan_id} scans:read
ParameterInDescription
scan_idpathUUID from the original POST /v1/scans/full response

Code Examples

scan_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
response = requests.get(
    f"https://api.ironbarkaml.com.au/v1/scans/{scan_id}",
    headers={"Authorization": f"Bearer {token}"},
)
scan = response.json()
const scanId = "3fa85f64-5717-4562-b3fc-2c963f66afa6";
const scan = await fetch(`https://api.ironbarkaml.com.au/v1/scans/${scanId}`, {
  headers: { Authorization: `Bearer ${token}` },
}).then(r => r.json());
scan_id = "3fa85f64-5717-4562-b3fc-2c963f66afa6"
uri = URI("https://api.ironbarkaml.com.au/v1/scans/#{scan_id}")
req = Net::HTTP::Get.new(uri, { 'Authorization' => "Bearer #{token}" })
scan = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.ironbarkaml.com.au/v1/scans/" + scanId))
    .header("Authorization", "Bearer " + token)
    .GET()
    .build();
HttpResponse<String> response = client.send(req,
    HttpResponse.BodyHandlers.ofString());
curl https://api.ironbarkaml.com.au/v1/scans/SCAN_ID \
  -H "Authorization: Bearer YOUR_TOKEN"

Filter Behaviour

When optional fields like dob, nationality_code, or gender are supplied, the scan engine applies post-match filters to reduce false positives. Every scan response transparently reports whether filtering was applied and why.

FieldTypeDescription
scan_result.filter_applied boolean true if one or more filters reduced the raw match set. false if all raw matches are returned as-is.
scan_result.filter_explanation string | null Human-readable description of which filters were applied. Example: "DOB filter removed 2 candidate matches"
Why this matters for regulators AML/CTF compliance programs require documenting why a match was cleared. filter_explanation gives you a ready-made audit trail string — no extra logic required.

Filter priority

  1. DOB exact match — highest precision, applied first
  2. Gender filter — applied when DOB is absent or ambiguous
  3. Nationality filter — applied as a secondary tiebreaker
  4. Country of operation — lowest priority, signals regional relevance only

Example — filter applied

{
  "scan_result": {
    "match_count": 1,
    "filter_applied": true,
    "filter_explanation": "DOB filter removed 3 candidate matches; 1 remaining",
    "matches": [{ /* ... */ }]
  }
}

Submit Batch Scan

Submit up to 100 entities for asynchronous scanning. Returns 202 Accepted immediately with a job_id. Processing runs in the background — poll GET /v1/scans/batch/{job_id} for status, or supply a webhook_url to receive a batch.completed event.

POST /v1/scans/batch scans:write

Request Body

FieldTypeDescription
entitiesarrayRequired1–100 entity objects. Each has the same fields as POST /v1/scans/full.
webhook_urlstringOptionalHTTPS URL. Receives batch.completed event when all items finish.

Code Examples

response = requests.post(
    "https://api.ironbarkaml.com.au/v1/scans/batch",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "entities": [
            {"entity_type": "person", "name": "John Smith", "nationality_code": "AU"},
            {"entity_type": "organisation", "name": "Acme Pty Ltd", "country_code": "AU"},
        ],
        "webhook_url": "https://yourapp.com/webhooks/ironbark",
    },
)
job_id = response.json()["job_id"]
const { job_id } = await fetch(
  "https://api.ironbarkaml.com.au/v1/scans/batch",
  {
    method: "POST",
    headers: {
      "Authorization": `Bearer ${token}`,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      entities: [
        { entity_type: "person", name: "John Smith", nationality_code: "AU" },
        { entity_type: "organisation", name: "Acme Pty Ltd", country_code: "AU" },
      ],
      webhook_url: "https://yourapp.com/webhooks/ironbark",
    }),
  }
).then(r => r.json());
uri = URI('https://api.ironbarkaml.com.au/v1/scans/batch')
req = Net::HTTP::Post.new(uri, {
  'Authorization' => "Bearer #{token}",
  'Content-Type'  => 'application/json'
})
req.body = {
  entities: [
    { entity_type: 'person', name: 'John Smith', nationality_code: 'AU' }
  ],
  webhook_url: 'https://yourapp.com/webhooks/ironbark'
}.to_json
result = JSON.parse(Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |h| h.request(req) }.body)
job_id = result['job_id']
String body = """
    {
        "entities": [
            {"entity_type": "person", "name": "John Smith", "nationality_code": "AU"},
            {"entity_type": "organisation", "name": "Acme Pty Ltd", "country_code": "AU"}
        ],
        "webhook_url": "https://yourapp.com/webhooks/ironbark"
    }
    """;
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("https://api.ironbarkaml.com.au/v1/scans/batch"))
    .header("Authorization", "Bearer " + token)
    .header("Content-Type", "application/json")
    .POST(HttpRequest.BodyPublishers.ofString(body))
    .build();
curl -X POST https://api.ironbarkaml.com.au/v1/scans/batch \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "entities": [
      {"entity_type": "person", "name": "John Smith", "nationality_code": "AU"},
      {"entity_type": "organisation", "name": "Acme Pty Ltd", "country_code": "AU"}
    ],
    "webhook_url": "https://yourapp.com/webhooks/ironbark"
  }'

Response (202 Accepted)

{
  "job_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "status": "pending",
  "total_items": 2,
  "completed_items": 0,
  "failed_items": 0,
  "webhook_url": "https://yourapp.com/webhooks/ironbark",
  "created_at": "2026-03-28T12:00:00+00:00",
  "completed_at": null,
  "items": []
}

Batch Job Status

Poll for batch job progress. Returns status, per-item scan_ids, and any item-level errors. Safe to poll repeatedly — idempotent.

GET /v1/scans/batch/{job_id} scans:read

Job Status Values

StatusMeaning
pendingJob accepted, not yet started
runningItems being processed
completedAll items processed (some may have failed — check failed_items)

Response

{
  "job_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
  "status": "completed",
  "total_items": 2,
  "completed_items": 2,
  "failed_items": 0,
  "webhook_url": "https://yourapp.com/webhooks/ironbark",
  "created_at": "2026-03-28T12:00:00+00:00",
  "completed_at": "2026-03-28T12:00:03+00:00",
  "items": [
    {
      "item_index": 0,
      "entity_name": "John Smith",
      "entity_type": "person",
      "status": "completed",
      "scan_id": "a1b2c3d4-...",
      "error_message": null
    },
    {
      "item_index": 1,
      "entity_name": "Acme Pty Ltd",
      "entity_type": "organisation",
      "status": "completed",
      "scan_id": "e5f6g7h8-...",
      "error_message": null
    }
  ]
}

Create Case

Create a compliance case from an existing scan. Cases are the audit record — they capture who reviewed a match, what decision was made, and when. The audit log is immutable and append-only.

POST /v1/cases cases:write
FieldTypeDescription
scan_idstringRequiredUUID of an existing scan
reviewerstringRequiredReviewer identifier — email or name. Stored in audit log.

Response (201 Created)

{
  "case_id": "c9d8e7f6-...",
  "scan_id": "3fa85f64-...",
  "reviewer": "compliance@yourfirm.com.au",
  "decision": "pending",
  "decision_notes": null,
  "scan_summary": {
    "entity_name": "Anthony Albanese",
    "entity_type": "person",
    "risk_level": "low",
    "risk_score": 12,
    "match_count": 0,
    "scanned_at": "2026-03-28T12:00:00+00:00"
  },
  "audit_log": [
    {
      "event_type": "case_created",
      "actor": "compliance@yourfirm.com.au",
      "event_data": {"reviewer": "compliance@yourfirm.com.au"},
      "created_at": "2026-03-28T12:05:00+00:00"
    }
  ],
  "created_at": "2026-03-28T12:05:00+00:00",
  "updated_at": "2026-03-28T12:05:00+00:00"
}

Get Case

Retrieve a compliance case with its full audit log.

GET /v1/cases/{case_id} cases:read

Log Decision

Record a compliance decision on an existing case. Multiple decisions are allowed — each appends a new event to the audit log. The decision field reflects the most recent ruling.

PATCH /v1/cases/{case_id}/decision cases:write
FieldTypeDescription
decisionstringRequired"pending" | "true_match" | "false_positive"
reviewerstringRequiredReviewer identifier. Stored in audit log.
notesstringOptionalFree-text reviewer notes. Stored in audit log.

Code Examples

case_id = "c9d8e7f6-..."
response = requests.patch(
    f"https://api.ironbarkaml.com.au/v1/cases/{case_id}/decision",
    headers={"Authorization": f"Bearer {token}"},
    json={
        "decision": "false_positive",
        "reviewer": "compliance@yourfirm.com.au",
        "notes": "Name match only, DOB and nationality do not match listed entity.",
    },
)
await fetch(`https://api.ironbarkaml.com.au/v1/cases/${caseId}/decision`, {
  method: "PATCH",
  headers: {
    "Authorization": `Bearer ${token}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    decision: "false_positive",
    reviewer: "compliance@yourfirm.com.au",
    notes: "Name match only — DOB and nationality do not match.",
  }),
});
uri = URI("https://api.ironbarkaml.com.au/v1/cases/#{case_id}/decision")
req = Net::HTTP::Patch.new(uri, {
  'Authorization' => "Bearer #{token}",
  'Content-Type'  => 'application/json'
})
req.body = { decision: 'false_positive', reviewer: 'compliance@yourfirm.com.au' }.to_json
HttpRequest req = HttpRequest.newBuilder()
    .uri(URI.create("...v1/cases/" + caseId + "/decision"))
    .header("Authorization", "Bearer " + token)
    .header("Content-Type", "application/json")
    .method("PATCH", HttpRequest.BodyPublishers.ofString(
        "{\"decision\":\"false_positive\",\"reviewer\":\"compliance@yourfirm.com.au\"}"
    ))
    .build();
curl -X PATCH https://api.ironbarkaml.com.au/v1/cases/CASE_ID/decision \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "decision": "false_positive",
    "reviewer": "compliance@yourfirm.com.au",
    "notes": "Name match only — DOB and nationality do not match."
  }'

PDF Case Report

Generate a regulator-ready PDF report for a compliance case. The PDF includes: case details, scan summary, chronological audit log, and a SHA-256 document integrity hash in the footer.

AU compliance requirement AUSTRAC Tranche 2 businesses (lawyers, accountants, real estate agents) must retain AML/CTF records for 7 years. Ironbark generates and stores these records — NameScan does not.
GET /v1/cases/{case_id}/report reports:read

Returns application/pdf with Content-Disposition: attachment; filename="ironbark-case-{id}.pdf".

Code Examples

case_id = "c9d8e7f6-..."
response = requests.get(
    f"https://api.ironbarkaml.com.au/v1/cases/{case_id}/report",
    headers={"Authorization": f"Bearer {token}"},
)
with open(f"ironbark-case-{case_id[:8]}.pdf", "wb") as f:
    f.write(response.content)
const res = await fetch(
  `https://api.ironbarkaml.com.au/v1/cases/${caseId}/report`,
  { headers: { Authorization: `Bearer ${token}` } }
);
const buffer = await res.arrayBuffer();
fs.writeFileSync(`ironbark-case-${caseId.slice(0,8)}.pdf`, Buffer.from(buffer));
curl https://api.ironbarkaml.com.au/v1/cases/CASE_ID/report \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -o ironbark-case.pdf

Webhook Events

Ironbark delivers webhook events to your HTTPS endpoint when async operations complete. Supply webhook_url when submitting a batch job — no separate webhook registration required.

EventWhenPhase
batch.completedAll items in a batch job have been processedPhase 2
scan.completedIndividual async scan finishedPhase 3
monitoring.alertMonitored entity appears on a new listPhase 3
credits.low_80Credit balance drops below 80%Phase 3
credits.low_95Credit balance drops below 5%Phase 3
api_key.expiring_soonClient registration expires in <30 daysPhase 3

Example payload — batch.completed

{
  "event": "batch.completed",
  "timestamp": "2026-03-28T12:00:03.142857+00:00",
  "data": {
    "job_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7",
    "completed": 2,
    "failed": 0
  }
}

Signature Verification

Every webhook delivery includes an X-Ironbark-Signature header. Verify this to confirm the request originated from Ironbark and was not tampered with.

Verification algorithm

  1. Read the raw request body as bytes
  2. Compute HMAC-SHA256(body, your_webhook_secret)
  3. Compare hex digest to the value after sha256= in the header
  4. Reject the request if they do not match
import hashlib, hmac

def verify_signature(body: bytes, secret: str, signature_header: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    provided = signature_header.removeprefix("sha256=")
    return hmac.compare_digest(expected, provided)
const crypto = require("crypto");

function verifySignature(body, secret, signatureHeader) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(body)
    .digest("hex");
  const provided = signatureHeader.replace("sha256=", "");
  return crypto.timingSafeEqual(
    Buffer.from(expected), Buffer.from(provided)
  );
}
require 'openssl'

def verify_signature(body, secret, signature_header)
  expected = OpenSSL::HMAC.hexdigest('SHA256', secret, body)
  provided = signature_header.sub('sha256=', '')
  ActiveSupport::SecurityUtils.secure_compare(expected, provided)
end

Error Codes

All errors return JSON with a "detail" field describing the problem.

StatusMeaningCommon causes
400Bad RequestInvalid field value, wrong grant_type, missing required field
401UnauthorizedMissing or expired Bearer token
403ForbiddenToken valid but missing required scope
404Not FoundUnknown scan_id, case_id, or job_id
422Validation ErrorRequest body fails schema validation (FastAPI)
429Too Many RequestsRate limit exceeded — check Retry-After header
500Internal Server ErrorScan execution failure — report to support
503Service UnavailableDatabase or Redis unavailable — check GET /health

Error response shape

{
  "detail": "Missing required scope: scans:write"
}

Validation error (422) shape

{
  "detail": [
    {
      "loc": ["body", "entity_type"],
      "msg": "value is not a valid enumeration member",
      "type": "type_error.enum"
    }
  ]
}

Ironbark API Reference · v1 · Built on solid ground.
Support: support@ironbark.ai