Claim Scrubber — Integration Guide

icdsbs.com · API v1 · last updated 10 August 2026

What it does

You send a claim before submitting it to the payer. We answer with everything that would get it rejected — an invalid or unspecified diagnosis, a procedure code that is not in the CHI SBS list, a duplicate line, a drug the Saudi formulary does not license for that diagnosis, a missed filing deadline.

The check runs against the full CHI SBS V3 procedure list, the Saudi drug formulary (40,952 drug-to-diagnosis rules) and the diagnosis reference. Typical response time is under half a second.

You do not need to change how you build claims. If you already submit to NPHIES you already produce FHIR — send us a copy of the same resource. If you would rather not touch your submission code at all, export a CSV and upload it.

Getting started

  1. We create an account for your organisation and issue you an API key.
  2. Store the key somewhere your application can read it. It is shown once and cannot be recovered — if it is lost we issue a new one.
  3. Call the endpoint below and read the response. That is the whole integration.

Base URL   https://icdsbs.com/api/v1

Everything lives under /api/v1. When a breaking change is ever needed it will appear at /api/v2 and v1 will keep working — we will not change the meaning of an existing field underneath you.

Authentication

Send your key in the X-API-Key header on every request.

# check that your key works — start here
curl -H "X-API-Key: sk_live_your_key_here" \
     https://icdsbs.com/api/v1/scrub/rules

A key carries scopes. Most integrations get all three:

ScopeAllows
scrub:runSubmitting claims to be checked
scrub:readReading back batches, findings and reports
rules:readReading the rule catalogue
Treat the key like a password. Do not put it in front-end JavaScript, a mobile app, or a public repository. If it is exposed, tell us and we will revoke it immediately — revocation takes effect on the next request.

Option A — send a FHIR Claim (recommended)

Send the same Claim resource you are about to submit to NPHIES. We return a standard OperationOutcome.

POST /api/v1/scrub/fhir/Claim/$scrub

curl -X POST "https://icdsbs.com/api/v1/scrub/fhir/Claim/\$scrub" \
  -H "X-API-Key: sk_live_your_key_here" \
  -H "Content-Type: application/fhir+json" \
  -d @claim.json

Accepts a Claim on its own, or a Bundle containing the Claim plus Patient, Coverage and Organization. References resolve from bundle entries or from contained. What we read:

FHIR elementUsed for
Claim.identifier[0].valueYour claim reference, echoed back
Claim.billablePeriod.start or Claim.createdEncounter date — filing deadline checks
Patient.birthDate, Patient.genderAge and sex consistency checks
Coverage.subscriberIdCompleteness only. Hashed on arrival, never stored
Claim.diagnosis[].diagnosisCodeableConceptDiagnosis checks. Use system http://hl7.org/fhir/sid/icd-10-am
Claim.item[].productOrServiceProcedure or drug. The system decides which
Claim.item[].diagnosisSequenceWhich diagnosis justifies the line
Claim.item[].net, unitPrice, quantityAmount at risk

A coding system containing medication, drug, atc, rxnorm, ndc, sfda or scientific marks the line as a drug; anything else is treated as a procedure and checked against SBS.

What comes back

{
  "resourceType": "OperationOutcome",
  "issue": [{
    "severity": "error",
    "code": "business-rule",
    "details": {
      "coding": [{ "system": "https://icdsbs.com/fhir/CodeSystem/scrub-rules",
                    "code": "RX002" }],
      "text": "IBUPROFEN is not licensed in the formulary for N41.0."
    },
    "diagnostics": "Add the diagnosis that justifies it, or switch to a licensed alternative.",
    "expression": ["Claim.item[0].productOrService"]
  }],
  "extension": [{
    "url": "https://icdsbs.com/fhir/StructureDefinition/scrub-summary",
    "extension": [
      { "url": "verdict",      "valueCode": "would_reject" },
      { "url": "amountAtRisk", "valueMoney": { "value": 24.0, "currency": "SAR" } },
      { "url": "findingCount", "valueInteger": 1 }
    ]
  }]
}

expression is the field to build your UI on. It is a FHIRPath pointer to the element that failed, so you can highlight the offending line instead of showing the user a paragraph of text.

Read extension → verdict for the headline without walking the issue list. It is one of:

verdictMeaningSuggested behaviour
cleanNothing foundSubmit
reviewSomething a coder should look atWarn, allow submit
would_rejectA payer edit will catch thisBlock submission until fixed

Arabic

Send Accept-Language: ar and every message and suggestion comes back in Arabic. The rule codes do not change.

Checking at the point of care

Add "_mode": "clinical" at the top level of the payload when the check runs while a doctor is prescribing. It suppresses the structural and money rules — a prescription has no payer, no price and no filing deadline yet, and reporting those to a clinician trains them to ignore the warning.

Option B — upload a file

For systems that would rather not change their submission code, and for the initial audit of historical claims. Export one row per claim line, repeating the claim-level fields on each row.

curl -X POST https://icdsbs.com/api/v1/scrub/batch \
  -H "X-API-Key: sk_live_your_key_here" \
  -F "[email protected]" \
  -F "label=July claims"

Column names do not have to match anything — they are recognised by alias, including Arabic headers. These are understood:

FieldRecognised headers include
Claim referenceclaim ref, claim no, invoice no, bill no, رقم المطالبة
Payerpayer, insurer, insurance company, TPA, شركة التأمين
Membermember id, card no, policy no, رقم العضوية
Encounter datevisit date, service date, invoice date, تاريخ الزيارة
Age / Sexage, gender, sex, العمر, الجنس
Diagnosesdiagnosis, ICD codes — separate several with ;
Line typeline type, item type — drug, lab, radiology, procedure…
Codeservice code, procedure code, SBS code, رمز الخدمة
Amountsquantity, unit price, net amount, المبلغ
Outcome (optional)claim status, denial reason — see below
Include the payer's decision if you have it. When the export carries a status and denial reason, the report stops projecting and starts measuring: of the claims that really were rejected, how many would have been caught first. That number is worth more than any demonstration.

The response gives you a batch id and the headline figures. Then:

EndpointReturns
GET /scrub/batchesYour batches, newest first
GET /scrub/batches/{id}Summary, by rule and by payer
GET /scrub/batches/{id}/claimsEvery claim with its findings
GET /scrub/batches/{id}/findings.csvA coder's worklist
GET /scrub/batches/{id}/report.htmlA one-page audit report

Maximum upload 64 MB. Larger files should be split; a submit-and-collect mode for very large batches is on the roadmap.

The rules

Fetch the live catalogue — it is the authoritative list, in English and Arabic:

curl -H "X-API-Key: …" https://icdsbs.com/api/v1/scrub/rules
PrefixCovers
STRMissing header fields, empty claims, zero-priced lines, future dates, missed filing windows
DXMissing, malformed, unknown, unspecified, ICD-10-CM-instead-of-AM, sex and age conflicts
PXNon-SBS format, codes absent from SBS V3, lines with no diagnosis link, duplicates, dental without a tooth
RXUnknown drug, drug not licensed for the diagnosis, non-claimable products, prescribing edits needing documentation

Severity:

error a payer edit catches this mechanically · warning commonly rejected, a coder should look · information worth knowing, not a defect

Rule codes are a contract. Once published, a code never changes meaning and is never reused for something else. Build your workflow on the code, not on the message text — the wording may improve.

If a rule does not apply to your payers, tell us and we will switch it off for your account alone. Nothing is tuned globally to suit one customer.

Errors

StatusMeaningWhat to do
400The payload could not be read. FHIR calls get a fatal OperationOutcome, not a different error shapeRead issue[0].details.text
401Missing, unknown, expired or revoked keyCheck the header; ask us to reissue
402Monthly claim allowance reachedContact your account manager
403The key lacks the scope for this callAsk for a key with the right scopes
404No such batch for your accountCheck the id — batches are private to the account that created them
429Too many requestsHonour Retry-After and back off
Never let a check block care. If we are slow or unreachable, time out after a few seconds and carry on — record that the check did not run, and never present "not checked" as "no problems found". A scrubber that stops a doctor prescribing is worse than no scrubber.

Limits

Requests per minute600 by default, per account
Monthly claimsAgreed per contract; 402 when reached
Upload size64 MB
Typical responseUnder 500 ms for a single claim

What we store

We keepWe never keep
Diagnosis and procedure codes, quantities, amounts, payer, dates, age and sex Patient names, phone numbers, addresses, national identifiers

Membership numbers are hashed the moment they arrive — enough to count the same member twice within one file, never enough to identify anyone. Identifying columns found in an uploaded file are read past and discarded, and the response tells you which ones were dropped.

Your data is visible only to your own account. Another customer asking for your batch by id receives "not found".

Go-live checklist

1Key stored server-side, never in a browser or mobile app
2Timeout set to 3–5 seconds, with the check skipped on failure
3"Not checked" displayed differently from "no problems found"
4Workflow keyed on verdict and rule codes, not message text
5429 handled with back-off
6A sample of findings reviewed by your coding lead before enforcement
7Agreed which rules, if any, are switched off for your payers
Start by warning, not blocking. Run in advisory mode for the first few weeks and compare the findings against your actual rejections. It builds trust in the numbers, and it tells us which rules to tune for you before anyone is stopped from submitting.