Claim Chaser Docs

Claim Chaser API Reference

Access your organization's claim, call, usage, and carrier data — and push claims in — from your own software, scripts, or the Claim Chaser CLI. It's the same data you see in the app, over HTTP.


Overview

Access model

All keys are self-serve — generate one in the Developer tab of the app and assign the scopes your integration needs at creation:

The write, call-triggering, and webhook capabilities documented below are fully live — just select the scope you need when you create the key.


Authentication

  1. In the Developer tab, generate a key — give it a name and select the scopes you need (read, write, or calls:write).
  2. The full key (format cc_live_…) is shown once — copy it immediately and store it somewhere safe. You will not be able to see it again (we only keep a hashed copy).
  3. Send it on every request as the x-api-key header:
curl -H "x-api-key: cc_live_YOUR_KEY_HERE" \
  https://app.claimchaser.ai/api/claims

Scopes

A key carries one or more scopes. Assign the least it needs.

ScopeGrantsHow you get it
readAll read endpoints below (claims, calls, usage, carriers, doctors, offices).Self-serve in the Developer tab.
writeAdds bulk claim upload (POST /api/uploads/claims).Self-serve in the Developer tab.
calls:writeAdds starting claim-chasing calls (POST /api/make-call). Calls cost real money, so every calls-enabled key carries a daily call cap — and dry_run lets you rehearse a dispatch without dialing or billing.Self-serve in the Developer tab.

Scopes are cumulative — a write key can also read, and a calls:write key can read and write.

Daily call cap: a calls:write key is minted with a per-day ceiling on how many calls it can start (default 50, max 500, resets at 00:00 UTC). When the cap is reached, make-call returns 429 until the next UTC day. A key's cap is managed from the Developer page and can never be changed by the API key itself.


Endpoints (v1)

All endpoints are org-scoped to your key automatically. Only the exact routes below are reachable by a key; every other route (and method) is denied with a uniform 404 so existence never leaks across orgs.

Reads — require the read scope

Reads return the matching rows for your organization, newest first. There are no pagination parameters in v1. Very large result sets may be subject to a platform row cap — if you expect to pull thousands of rows, reach out; cursor-based pagination is on the roadmap.

MethodPathReturns
GET/api/claimsYour org's claims (full rows) plus lookup lists — doctors, carriers, offices, callability.
GET/api/claims/denial-reasons?claim_ids=…Denial reasons for a batch of claim ids (comma-separated).
GET/api/claims/{id}/claim-statusesStatus history for one claim.
GET/api/claims/{id}/denial-reasonsDenial reasons extracted for one claim — the core call output.
GET/api/calls/allEvery call for your org (flattened summary rows).
GET/api/calls/recent?limit=NMost recent calls (default 10).
GET/api/calls/statsCall aggregates: active, today, average duration, success rate.
GET/api/calls/{id}One call in full detail, with its linked claim.
GET/api/carriers/{id}/callsCalls placed for a given carrier.
GET/api/usage/summaryUsage summary: current month, last 12 months, all time.
GET/api/usage/report?year=YYYY&month=MMDetailed usage report — year is optional and defaults to the current year (add include_calls=true for per-call rows).
GET/api/organization-carriersYour org's carrier network, with phone numbers.
GET/api/doctor-carriers?carrier_id=…Doctor ↔ carrier links for your org.
GET/api/doctorsDoctors in your org (plus offices).
GET/api/officesOffices in your org.
GET/api/uploads/contextReference data for building an upload — offices, doctors, carriers, value mappings.

Writes — require the write scope

MethodPathDoes
POST/api/uploads/claimsBulk-upload claims into your org ("action":"insert") or patch existing claims ("action":"update") — the primary integration point (push claims from your PMS/software).

Calls — require the calls:write scope

A key with the calls:write scope can start real claim-chasing calls — see Triggering calls below for the request body, the no-cost dry_run rehearsal, and the full error surface.

MethodPathDoes
POST/api/make-callStarts the next claim-chasing call for your org (or for a specific claim via {"claim_id":"…"}). Subject to the key's daily call cap, your org's concurrent-call limit, and calling-hours rules. Pass "dry_run": true to verify eligibility without dialing or billing.

Read examples

A request and a representative response for each read endpoint. Fields marked are ids/values specific to your org. The /api/claims list returns a fixed partner field set. Other read endpoints may return additional fields beyond those shown in these examples — treat any field not documented here as internal, unstable, and not to be relied on; it may change or be removed without notice.

GET /api/claims

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/claims

Response

{
  "claims": [
    {
      "id": "…",
      "claim_number": "CLM-1001",
      "patient_name": "Jane Doe",
      "carrier_id": "…",
      "office_id": "…",
      "doctor_id": "…",
      "insurance_phone": "8005551234",
      "date_of_service": "2026-05-01",
      "billed_amount": 420.0,
      "claim_status": "Denied",
      "created_at": "2026-05-02T14:03:00Z"
    }
  ],
  "doctors":  [ { "id": "…", "name": "Dr. Smith", "npi": "1234567890" } ],
  "carriers": [ { "id": "…", "name": "Acme Health" } ],
  "offices":  [ { "id": "…", "name": "Main Street Clinic" } ],
  "carrier_requests": [ { "id": "…", "requested_name": "New Carrier LLC" } ],
  "callability": [ { "claim_id": "…", "callability_status": "callable", "missing_fields": [] } ]
}

The claims row returns the documented partner fields — identity, dates, amounts, claim_status, and the carrier_id / office_id / doctor_id references (plus the subscriber_*, patient_*, and payment fields). Internal pipeline columns and status notes are not returned. The example shows the load-bearing fields.

GET /api/claims/{id}/denial-reasons

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/claims/CLAIM_ID/denial-reasons

Response

{
  "success": true,
  "denial_reasons": [
    {
      "id": "…",
      "claim_id": "…",
      "denial_reason": "Timely filing limit exceeded",
      "date_recorded": "2026-05-03T00:00:00+00:00",
      "resubmission_instructions": "Refile with proof of timely submission",
      "date_reason_resubmitted": null,
      "date_accepted": null,
      "status": "Pending",
      "created_at": "2026-05-03T09:20:00Z",
      "updated_at": "2026-05-03T09:20:00Z"
    }
  ]
}

GET /api/claims/denial-reasons?claim_ids=… (batch)

Request

curl -H "x-api-key: cc_live_…" \
  "https://app.claimchaser.ai/api/claims/denial-reasons?claim_ids=ID_1,ID_2"

Response

{
  "denial_reasons": [
    { "id": "…", "claim_id": "ID_1", "denial_reason": "…", "date_recorded": "2026-05-03T00:00:00+00:00", "status": "Pending" }
  ]
}

GET /api/claims/{id}/claim-statuses

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/claims/CLAIM_ID/claim-statuses

Response

{
  "success": true,
  "claim_statuses": [
    {
      "id": "…",
      "status": "Denied",
      "call_id": "…",
      "status_changed_at": "2026-05-03T09:20:00Z"
    }
  ]
}

GET /api/calls/all

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/calls/all

Response

{
  "calls": [
    {
      "id": "…",
      "carrier": "Acme Health",
      "numberCalled": "8005551234",
      "patient": "Jane Doe",
      "claimNumber": "CLM-1001",
      "startTime": "2026-05-03T09:00:00Z",
      "endTime": "2026-05-03T09:07:00Z",
      "createdAt": "2026-05-03T09:00:00Z",
      "duration": "7:00",
      "status": "completed",
      "conversationId": "…",
      "successStatus": []
    }
  ]
}

GET /api/calls/recent?limit=N

Request

curl -H "x-api-key: cc_live_…" \
  "https://app.claimchaser.ai/api/calls/recent?limit=5"

Response

{
  "calls": [
    {
      "id": "…",
      "claim_id": "…",
      "call_status": "completed",
      "started_at": "2026-05-03T09:00:00Z",
      "ended_at": "2026-05-03T09:07:00Z",
      "to_number": "8005551234",
      "conversation_id": "…",
      "duration": "7:00",
      "carrier": "Acme Health",
      "claim_number": "CLM-1001",
      "patient_name": "Jane Doe",
      "status": "completed"
    }
  ]
}

GET /api/calls/stats

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/calls/stats

Response

{
  "activeCalls": 0,
  "today": 3,
  "avgDuration": "4:12",
  "successRate": 82
}

GET /api/calls/{id}

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/calls/CALL_ID

Response

{
  "call": {
    "id": "…",
    "call_status": "completed",
    "started_at": "2026-05-03T09:00:00Z",
    "ended_at": "2026-05-03T09:07:00Z",
    "to_number": "8005551234",
    "ai_summary": null,
    "duration": "7:00",
    "claims": {
      "id": "…",
      "organization_id": "…",
      "patient_name": "Jane Doe",
      "patient_id": "…",
      "date_of_birth": "1985-04-12",
      "claim_number": "CLM-1001",
      "date_of_service": "2026-05-01",
      "billed_amount": 420.0,
      "claim_status": "Denied"
    }
  }
}

The call row returns structured fields — status, timing, phone numbers, and the linked claim. Agent-generated summaries (ai_summary) and raw transcripts are gated (returned as null); read the call outcome from the linked claim's claim_status and the denial-reasons endpoint. The example shows the load-bearing fields.

GET /api/carriers/{id}/calls

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/carriers/CARRIER_ID/calls

Response

{
  "calls": [
    {
      "id": "…",
      "call_status": "completed",
      "claims": { "id": "…", "patient_name": "Jane Doe", "claim_number": "CLM-1001" }
    }
  ]
}

The call row returns structured fields — status, timing, phone numbers, and the linked claim. Agent-generated summaries (ai_summary) and raw transcripts are gated (returned as null); read the call outcome from the linked claim's claim_status and the denial-reasons endpoint. The example shows the load-bearing fields.

GET /api/usage/summary

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/usage/summary

Response

{
  "organization_id": "…",
  "current_month": {
    "year": 2026, "month": 5, "month_name": "May",
    "duration_seconds": 5400, "call_count": 12, "duration_formatted": "1h 30m"
  },
  "last_12_months": {
    "duration_seconds": 64800, "call_count": 140, "duration_formatted": "18h 0m",
    "monthly_breakdown": [
      { "year": 2026, "month": 5, "month_name": "May",
        "total_duration_seconds": 5400, "total_duration_formatted": "1h 30m", "call_count": 12 }
    ]
  },
  "all_time": { "duration_seconds": 90000, "call_count": 200, "duration_formatted": "25h 0m" }
}

Durations are formatted as a compact human-readable string (e.g. 1h 30m, 4m 14s, 15s), not H:MM:SS. Use duration_seconds for exact arithmetic.

GET /api/usage/report?year=YYYY&month=MM

Request

curl -H "x-api-key: cc_live_…" \
  "https://app.claimchaser.ai/api/usage/report?year=2026&month=5"

Response

{
  "organization_id": "…",
  "year": 2026,
  "month": 5,
  "total_duration_seconds": 5400,
  "total_duration_formatted": "1h 30m",
  "total_call_count": 12,
  "monthly_breakdown": [
    { "year": 2026, "month": 5, "total_duration_seconds": 5400, "total_duration_formatted": "1h 30m", "call_count": 12 }
  ]
  // add ?include_calls=true for a "calls": [ … ] array of per-call rows
  // year is optional — omit it and the report defaults to the current year
}

GET /api/organization-carriers

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/organization-carriers

Response

{
  "organization_carriers": [
    {
      "id": "…",
      "carrier_id": "…",
      "active": true,
      "inactivation_reason": null,
      "inactivated_at": null,
      "created_at": "2026-01-10T00:00:00Z",
      "updated_at": "2026-01-10T00:00:00Z",
      "carrier": {
        "id": "…",
        "name": "Acme Health",
        "claims_phone_number": "8005551234",
        "carrier_phone_numbers": [
          { "id": "…", "phone_number": "8005551234", "is_primary": true, "is_active": true }
        ]
      }
    }
  ]
}

GET /api/doctor-carriers?carrier_id=…

Request

curl -H "x-api-key: cc_live_…" \
  "https://app.claimchaser.ai/api/doctor-carriers?carrier_id=CARRIER_ID"

Response

{
  "doctor_carriers": [
    {
      "id": "…",
      "doctor_id": "…",
      "carrier_id": "…",
      "active": true,
      "inactivation_reason": null,
      "inactivated_at": null,
      "inactivated_by_kind": null,
      "doctor": { "id": "…", "name": "Dr. Smith", "organization_id": "…" }
    }
  ]
}

GET /api/doctors

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/doctors

Response

{
  "doctors": [
    { "id": "…", "name": "Dr. Smith", "npi": "1234567890", "office": { "id": "…", "name": "Main Street Clinic" } }
  ],
  "offices": [ { "id": "…", "name": "Main Street Clinic" } ]
}

GET /api/offices

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/offices

Response

{
  "offices": [
    { "id": "…", "name": "Main Street Clinic" }
  ]
}

Full office rows are returned; the example shows the load-bearing fields.

GET /api/uploads/context

Request

curl -H "x-api-key: cc_live_…" \
  https://app.claimchaser.ai/api/uploads/context

Response

{
  "organization_id": "…",
  "offices": [ { "id": "…", "name": "Main Street Clinic", "address": "…", "ein": "…", "callback_number": "…" } ],
  "doctors": [ { "id": "…", "name": "Dr. Smith", "npi": "1234567890" } ],
  "carriers": [ { "id": "…", "name": "Acme Health", "in_network": true, "turned_off": false, "inactivation_reason": null } ],
  "valueMappings": [ { "organization_id": "…", "entity_type": "carrier", "input_value": "ACME", "entity_id": "…" } ]
}

Uploading claims

POST /api/uploads/claims (requires write) takes an action of "insert" and a claims array. Each claim object maps to columns on your claims table. Your organization is bound from the key, so you do not send an organizationId. Use GET /api/uploads/context to resolve valid carrier_id, office_id, and doctor_id values. insurance_phone is normalized to digits, and a matching carrier_id may be resolved from it.

Request

curl -X POST -H "x-api-key: cc_live_…" \
  -H "Content-Type: application/json" \
  https://app.claimchaser.ai/api/uploads/claims \
  -d '{
    "action": "insert",
    "claims": [
      {
        "claim_number": "CLM-1001",
        "patient_name": "Jane Doe",
        "date_of_birth": "1985-04-12",
        "insurance_phone": "8005551234",
        "carrier_id": "…",
        "office_id": "…",
        "doctor_id": "…",
        "date_of_service": "2026-05-01",
        "billed_amount": 420.0,
        "claim_status": "Denied"
      }
    ]
  }'

Response

{
  "data": [
    { "id": "…", "claim_number": "CLM-1001", "patient_name": "Jane Doe" /* …inserted claim row… */ }
  ],
  "documentId": null
}

Claim row schema

Only patient_name is required to insert a row — everything else is optional (a missing patient_name surfaces as a 400 database-constraint error). The recommended fields below are what make a claim callable, so include them whenever you have them.

FieldFormatNotes
patient_namestringRequired — the only field the database itself insists on.
patient_id / subscriber_idstringRecommended — carriers need at least one identifier to locate the claim.
date_of_birth, date_of_service, date_sent_to_insurance, subscriber_dobdate, YYYY-MM-DDISO 8601 date strings. date_of_birth and date_of_service are recommended.
billed_amountnumberA JSON number (e.g. 420.0) — not a currency string. Recommended.
insurance_phonestringNormalized to digits; a matching carrier_id may be resolved from it.
carrier_id, office_id, doctor_iduuidResolve valid ids via GET /api/uploads/context.
claim_statusstringAccepts the app's status strings — e.g. Open, Pending, Denied, Payment Pending, Paid, No Claim on File, Incorrect Claim Details, Duplicate Claim.
claim_number, group_number, subscriber_name, patient_zip, patient_phonestringOptional — include what your PMS has.

Updating claims — action: "update"

The same endpoint patches existing claims: send "action":"update" and an updates array of { "id": "…", "payload": { … } } objects. Each payload is a partial claim — only the fields you send change. As with inserts, your organization is bound from the key.

An API key may patch exactly these fields: claim_status, insurance_phone, subscriber_id, patient_zip, patient_phone, patient_name, patient_id, date_of_birth, subscriber_name, subscriber_dob, group_number, claim_number, date_of_service, date_sent_to_insurance.

Any other key in payload — including all financial-amount fields (billed_amount, insurance_paid_amount, patient_responsibility) and organization_id — is silently stripped, not errored: the update still applies the allowed fields, and every stripped key is reported back per-row in stripped so your integration can see exactly what applied. Write access to amount fields may open later.

Request

curl -X POST -H "x-api-key: cc_live_…" \
  -H "Content-Type: application/json" \
  https://app.claimchaser.ai/api/uploads/claims \
  -d '{
    "action": "update",
    "updates": [
      {
        "id": "CLAIM_ID",
        "payload": {
          "claim_status": "Denied",
          "insurance_phone": "8005551234",
          "billed_amount": 999.0
        }
      }
    ]
  }'

Response

{
  "results": [
    {
      "id": "CLAIM_ID",
      "stripped": ["billed_amount"]   // dropped, not applied — the other fields were updated
    }
  ]
}

A row whose payload contains no patchable fields is not written at all — its result carries "error": "No patchable fields in payload." alongside stripped. Per-row database errors (bad id, constraint violations) surface the same way, as an error on that row while the rest of the batch proceeds.


Triggering calls

POST /api/make-call (requires calls:write) starts a claim-chasing call. Send {"claim_id":"…"} to chase a specific claim. Every dispatch counts against the key's daily call cap and is subject to your org's concurrent-call limit and calling-hours rules.

Dry run first: add "dry_run": true and every eligibility gate runs for real (billing, voice on/off, calling hours, claim ownership, carrier and phone resolution, duplicate-call guard) but no call is placed and nothing is billed — you get back the exact dispatch plan.

Org-state gates that would refuse a real call right now are collected into dispatch_blockers instead of failing the rehearsal: possible values are VOICE_OFF and HOLIDAY_WEEKEND_FLOOR, and when the array is non-empty the response's status is would_be_blocked rather than would_dispatch. That means a voice-off org can build and test its integration end-to-end before calling is enabled — a dry run never dials regardless.

Request (dry run)

curl -X POST -H "x-api-key: cc_live_…" \
  -H "Content-Type: application/json" \
  https://app.claimchaser.ai/api/make-call \
  -d '{ "claim_id": "…", "dry_run": true }'

Response (dry run)

{
  "success": true,
  "dry_run": true,
  "status": "would_dispatch",
  "dispatch_blockers": [],   // empty = a real call would dispatch right now
  "message": "Dry run: eligibility verified; no call was placed and nothing was billed.",
  "plan": {
    "claim_id": "…",
    "organization_id": "…",
    "carrier_id": "…",
    "carrier_name": "Acme Health",
    "to_number": "8005551234",
    "from_number": "…"
  }
}

Response (dry run, org gates would block a real call)

{
  "success": true,
  "dry_run": true,
  "status": "would_be_blocked",
  "dispatch_blockers": ["VOICE_OFF"],   // and/or "HOLIDAY_WEEKEND_FLOOR"
  "message": "Dry run: eligibility verified; a real call would currently be blocked by: VOICE_OFF. No call was placed and nothing was billed.",
  "plan": { /* same shape as above */ }
}

Dry-run behavior depends on the claim and org state. The would_dispatch / would_be_blocked shape above assumes an eligible claim on a voice-enabled org. You may also get: 422 {"error":"Claim is missing required fields for a call","missing":[...]} when the claim lacks data (e.g. date_of_birth); 202 {"suppressed":true,"reason":"carrier_hours"} when the carrier is outside its calling hours; or a 403 voice-off error when you call make-call without a claim_id on a voice-off org. Target a fully-populated, specific claim_id to exercise the documented rehearsal.

Request (real dispatch)

curl -X POST -H "x-api-key: cc_live_…" \
  -H "Content-Type: application/json" \
  https://app.claimchaser.ai/api/make-call \
  -d '{ "claim_id": "…" }'

Response

{
  "success": true,
  "status": "initiated",
  "call_id": "…",          // correlate the later call.completed webhook, or poll GET /api/calls/{call_id}
  "claim_id": "…",
  "conversation_id": "…"
}

Call-specific errors

StatusBodyWhen
403{"error":"API key is missing the 'calls:write' scope required to start calls."}The key lacks calls:write.
429{"error":"…","code":"API_KEY_DAILY_CAP_REACHED","daily_call_cap":50,"used_today":50}The key hit its daily call cap. Resets at 00:00 UTC.
429{"error":"Rate limit exceeded. Please try again later.","code":"RATE_LIMIT_EXCEEDED","retryAfter":42}Per-org rate limit on make-call: 30 requests per minute per organization (dry runs count too). The response carries a Retry-After header (seconds) — wait at least that long before retrying, and back off exponentially if you keep hitting it.
403{"error":"…","code":"VOICE_OFF"}Voice is turned off for your organization — keys never bypass the off-switch.
402{"error":"…","code":"ACCOUNT_SUSPENDED" | "ACCOUNT_CANCELLED"}Billing-suspended or cancelled orgs cannot start calls — recover from the Billing page.
202{"suppressed":true,"reason":"holiday_weekend_floor"}Dispatch suppressed (US federal holiday / weekend floor, or carrier out of business hours with "reason":"carrier_hours"). Not an error and not billed — retry on the next business day.

Outbound webhooks

Instead of polling, register an HTTPS endpoint on the Developer page and we POST you an event when a call completes (call.completed) or a claim moves to a new status (claim.status_changed). URLs must be https. Each endpoint gets a signing secret, shown once at registration (rotate it on the Developer page if it leaks). Registration and the signing secret are set up in the Developer page, not via the API — there is no key-reachable endpoint to create an endpoint or fetch its secret.

What you're subscribed to

Event envelope

Every delivery is a JSON envelope. The payload deliberately carries IDs and status only — no PHI rides in the webhook; fetch the full result over the read API (below).

{
  "id": "call.completed:<call_id>",     // unique per event — dedupe on this
  "type": "call.completed",
  "api_version": "2026-07-01",
  "created_at": "2026-07-13T18:04:11Z",
  "organization_id": "…",
  "data": {
    "call_id": "…",
    "claim_id": "…",
    "call_status": "completed",
    "started_at": "2026-07-13T17:58:02Z",
    "ended_at": "2026-07-13T18:04:05Z",
    "duration_seconds": 363
  }
}

A claim.status_changed event carries this data:

{
  "claim_id": "…",
  "status": "Denied",
  "status_changed_at": "2026-07-13T18:04:11Z",
  "call_id": "…"
}

Verifying signatures

Every delivery carries a Claim-Chaser-Signature header:

Claim-Chaser-Signature: t=<unix-seconds>,v0=<hex>

where v0 is HMAC_SHA256(secret, ${t}.${rawBody} ) over the raw request body (before any JSON parsing). Reject deliveries whose timestamp is more than 30 minutes from now — that bounds replay.

// Node.js — verify a Claim Chaser webhook (raw body required)
const crypto = require('crypto')

function verifyClaimChaserSignature(rawBody, signatureHeader, secret) {
  const parts = Object.fromEntries(
    signatureHeader.split(',').map((kv) => kv.split('=', 2))
  )
  const t = Number(parts.t)
  if (!Number.isFinite(t) || Math.abs(Date.now() / 1000 - t) > 30 * 60) {
    return false // stale or malformed timestamp — possible replay
  }
  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${t}.${rawBody}`)
    .digest('hex')
  const given = Buffer.from(parts.v0 || '', 'hex')
  const want = Buffer.from(expected, 'hex')
  return given.length === want.length && crypto.timingSafeEqual(given, want)
}

// Express example — capture the RAW body, not the parsed JSON:
// app.post('/webhooks/claim-chaser', express.raw({ type: 'application/json' }), (req, res) => {
//   res.sendStatus(200) // ack immediately, then process
//   if (!verifyClaimChaserSignature(req.body.toString('utf8'),
//         req.get('Claim-Chaser-Signature') || '', process.env.CC_WEBHOOK_SECRET)) return
//   const event = JSON.parse(req.body.toString('utf8'))
//   // dedupe on event.id, then act on event.type / event.data
// })

Delivery contract

Recommended flow

  1. Respond 2xx immediately — do the work after acknowledging, not before.
  2. Verify the signature over the raw body.
  3. Dedupe on the envelope id.
  4. Fetch the full result: GET /api/claims/{claim_id}/denial-reasons — the webhook payload deliberately carries IDs and status only.

Using the CLI with a key

The Claim Chaser CLI can authenticate with a key instead of email/password — and with a key you don't need any local database configuration on your machine. Set CLAIM_CHASER_API_KEY and the base URL defaults to https://app.claimchaser.ai.

export CLAIM_CHASER_API_KEY="cc_live_YOUR_KEY_HERE"
claim-chaser whoami            # shows your org (keyed: true)
claim-chaser usage summary
claim-chaser claims get <claim-id>

Read commands and claim upload work under a key, matching the endpoint allowlist above. Commands that dial (make-call) need a key with the calls:write scope and count against that key's daily call cap.


Not available via the API (Developer-page only)

Some operations are deliberately kept out of the keyed surface. If your integration needs any of these, a person does it in the app:


Errors

StatusBodyWhen
401{"error":"Unauthorized"}Missing, malformed, revoked, or expired key.
403{"error":"API key is missing the 'write' scope required for POST /api/uploads/claims."}The route is allowlisted but your key lacks the required scope. The message names the missing scope and route.
404(empty body)The route/method is not reachable by a key (not on the allowlist). Uniform and existence-neutral — it never reveals whether a route exists or that a resource belongs to another org.
400{"error":"…"}Validation error on a write — e.g. {"error":"Invalid action"}, or a database constraint message surfaced from the claim insert.
429{"error":"This API key has reached its daily call cap. Try again after 00:00 UTC.","code":"API_KEY_DAILY_CAP_REACHED","daily_call_cap":50,"used_today":50}A calls:write key hit its daily call cap on make-call. Resets at 00:00 UTC. (make-call is also rate-limited to 30 requests/min per org — "code":"RATE_LIMIT_EXCEEDED" with a Retry-After header; see the call-specific errors above.)

Security notes