Skip to content
LICIUM

Payer enrollment / API

Payer enrollment status, pushed into your platform

For billing and practice software whose practices wait on payer enrollment applications. Send each pending application once; we follow it up with the payer until it is approved or denied, and your system gets every change as it happens.

  • Statuses pushed into your system

    Each time an application's status changes, a signed webhook event reaches your endpoint: what the payer said, its reference number, the effective date once approved, and what is missing when documents are needed.

  • Every payer

    Commercial plans, Medicare and Medicaid in every state. Send the payer's name as your practice knows it; no payer list to map.

  • Checked until decided

    Payers that post their status online are read every 30 minutes, day and night; the others are checked every week, or as often as every business day. No time limit and nothing more to pay.

  • Your practices hear nothing from us

    Set notify to platform_only and every email comes to your address instead, or to webhook_only for none at all.

Base URL
https://www.licium.ai
Access
No API key: each follow-up has its own access token, returned once when you create it; send it as X-Payer-Follow-Up-Token (or Authorization: Bearer).
Replies
Every reply is JSON: { "success": true, "data": ... } or { "success": false, "error": "..." }.
OpenAPI 3.1
The document for these calls: https://www.licium.ai/payer-follow-up/api/openapi.json

Integrate in under an hour

  1. Create a follow-up for a practice with its pending applications and your webhook address.
  2. Pay at checkout_url ($79 per application at the list price). Nothing is checked before.
  3. Verify the signature on each event and store the new status.
  4. Read the full status and check log whenever you like.
  5. Handle an application stopped with no decision (reopen it, at no charge).

POST /v1/payer-follow-ups

1. Create a follow-up

One follow-up per practice. POST /v1/payer-follow-ups with the practice's email, a plan, how we may reach the payer for it (access; call_only needs nothing from the practice), and its pending applications. Add webhook_url for status events, notify so we never email the practice, and check_every_days to choose how often the checks that are not status page reads run.

Send an Idempotency-Key header (1 to 128 printable ASCII characters, no spaces; a UUID works) so a retry is safe. For 24 hours, the same key with the same email and the same body returns the first reply again, with the same account_id and checkout_url, instead of making a second follow-up. That repeated reply has replayed: true and the header Idempotent-Replayed: true, and access_token and webhook.secret are null: they are shown only in the first reply. The same key with a different body is refused with 409.

Body

  • emailstringrequired

    The practice's email (or whoever owns the follow-up). It can sign in on licium.ai to see the status page. With notify platform_only or webhook_only we never email it.

  • companystring

    The practice's name. Emails sent to your address are marked with it.

  • planper_application | group | application_bundle

    per_application: $79 per application, one time. See Pricing for the others.

  • accessarrayrequired

    portal_user, pecos_surrogate, delegation_letter, caqh_admin, or call_only by itself. call_only: we call the payer; nothing to set up.

  • applications[]array

    npi, payer, state (two letters), submitted_on (2026-09-14 or 9/14/2026); payer_reference, notes and check_every_days optional. Up to 300.

  • check_every_daysinteger 1 to 7

    Optional, 1 to 7 (default 7, weekly): days between the checks of an application that are not reads of the payer's public status page (calls, emails, portal checks), always on a business day; 1 is every business day. For every application of the request, or set in an application entry for that one. The price does not change. Status pages are read every 30 minutes, day and night.

  • webhook_urlstring

    Where status events go (step 3): HTTPS on a public host, port 443 or 8443, at most 300 characters.

  • notifypractice | platform_only | webhook_only

    Optional. Who gets this follow-up's emails: practice (default) sends them to email; platform_only sends every email (payment confirmation, missing documents, document requests and reminders, roster changes, the Monday status email, stopped applications) only to notify_email, your platform's address, marked with the practice it is about, and never to email; webhook_only sends no email at all (webhook_url is required) and status reaches you by the webhook and GET /v1/payer-follow-ups/{id}.

  • notify_emailstring

    Required with notify platform_only, and only then: the address every email of this follow-up goes to instead of email.

  • partner_codestring

    Optional. A partner code Licium gave you: your agreed price is charged instead of the list price. A code that is not active is refused and nothing is saved.

Rosters, CSV or XLSX files, CAQH attestation dates and the application bundle (we write and submit new applications) are in the OpenAPI document under the same call.

POST /v1/payer-follow-ups
curl -X POST https://www.licium.ai/v1/payer-follow-ups \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: 6f1c2e8a-3b5d-4c7e-9a1f-0d2b4c6e8a10" \
  -d '{
    "email": "owner@brighttherapy.com",
    "company": "Bright Therapy Group",
    "plan": "per_application",
    "access": ["call_only"],
    "applications": [
      { "npi": "1234567893", "payer": "Aetna", "state": "TX",
        "submitted_on": "2026-09-14", "payer_reference": "APP-55102" },
      { "npi": "1234567893", "payer": "Cigna", "state": "TX",
        "submitted_on": "2026-09-10", "check_every_days": 1 }
    ],
    "check_every_days": 2,
    "notify": "platform_only",
    "notify_email": "credentialing@yourplatform.com",
    "webhook_url": "https://hooks.yourplatform.com/licium"
  }'

The reply. access_token and webhook.secret are shown only here: store them with the practice.

200 OK
{
  "success": true,
  "data": {
    "account_id": "8f2d4c1a-6b3e-4f7a-9c21-5d0e8b7a3f10",
    "access_token": "Qm9yZWQ_eW91IGRlY29kZWQgdGhpcyBleGFtcGxlIQ0",
    "status_url": "/payer-follow-up/8f2d4c1a-6b3e-4f7a-9c21-5d0e8b7a3f10",
    "checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
    "plan": "per_application",
    "monitoring": false,
    "roster_count": 1,
    "pending_clinician_count": 1,
    "application_count": 2,
    "price": {
      "plan": "per_application", "monitoring": false,
      "lines": [ { "key": "application", "label": "One payer application, checked until decided", "unit_cents": 7900,
                   "quantity": 2, "amount_cents": 15800, "recurring": false } ],
      "first_payment_cents": 15800, "monthly_cents": 0, "one_time_cents": 15800,
      "checkout_mode": "payment"
    },
    "payment_status": "awaiting_payment",
    "next": "Pay at checkout_url. Keep access_token: ...",
    "webhook": { "url": "https://hooks.yourplatform.com/licium", "secret": "lws_9a4e1c0b7d3f2a6e8c5b1d0f4a7e3c9b2d6f8a1c5e0b3d7f9a2c4e6b8d0f1a3c" },
    "notify": "platform_only"
  }
}

The same key and body again:

Repeated with the same Idempotency-Key
HTTP/1.1 200 OK
Idempotent-Replayed: true

{
  "success": true,
  "data": {
    "account_id": "8f2d4c1a-6b3e-4f7a-9c21-5d0e8b7a3f10",
    "access_token": null,
    "checkout_url": "https://checkout.stripe.com/c/pay/cs_live_...",
    ...
    "webhook": { "url": "https://hooks.yourplatform.com/licium", "secret": null },
    "replayed": true,
    "note": "This repeats the reply to the first request sent with this Idempotency-Key. ..."
  }
}

POST /v1/payer-follow-ups/{id}/checkout

2. Pay

checkout_url is a Stripe Checkout page for the price in the reply. Pay it yourself or send it to whoever pays; with notify platform_only the page and Stripe's receipt are addressed to your notify_email, never to the practice. Nothing is checked until the payment settles; then standing turns active and the first checks start. A page that expired is replaced by calling checkout again, as shown here.

POST /v1/payer-follow-ups/{id}/checkout
curl -X POST https://www.licium.ai/v1/payer-follow-ups/$ACCOUNT_ID/checkout \
  -H "X-Payer-Follow-Up-Token: $ACCESS_TOKEN"

PUT /v1/payer-follow-ups/{id}/webhook

3. Receive status webhooks

Each time an application's status changes, we POST one application.status_changed event to your address. Set it with webhook_url at creation, or later (each set returns a new secret, and the old one stops signing at once). Send a signed webhook.test event to check your endpoint, before or after paying.

Event

  • iduuid

    The event. The same id on every try, and in the Licium-Event-Id header.

  • typestring

    application.status_changed, or webhook.test (data.message only).

  • account_iduuid

    The follow-up (account_id from step 1).

  • data.application_iduuid

    The application: applications[].id in the status read.

  • data.statusstring

    The new status (see Statuses). data.previous_status is the one before.

  • data.effective_datedate or null

    approved: the effective date the payer gave.

  • data.missing_documentsstring or null

    needs_documents: what the payer is missing.

  • data.decidedboolean

    approved or denied: no more checks or events for this application.

  • data.checked_atdate-time

    When the check that found the change was made. Keep the status with the latest one.

  • data.next_check_atdate-time or null

    When it is checked next.

  • data.payer_saidstring or null

    What the payer said, in its words (null for a stop or a reopen).

  • data.sourceobject or null

    method (portal, phone or email), automated (read from the payer's public status page), reference (the payer's reference number or the representative) and note (where it came from). Null for a stop or a reopen.

  • data.stoppedobject or null

    no_decision: reason (no_record or twelve_months) and reopen_until. Null otherwise.

Set the address
curl -X PUT https://www.licium.ai/v1/payer-follow-ups/$ACCOUNT_ID/webhook \
  -H "X-Payer-Follow-Up-Token: $ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://hooks.yourplatform.com/licium" }'
Send a test event
curl -X POST https://www.licium.ai/v1/payer-follow-ups/$ACCOUNT_ID/webhook/test \
  -H "X-Payer-Follow-Up-Token: $ACCESS_TOKEN"
200 OK
{ "success": true,
  "data": { "event_id": "5e1f...", "delivered": true, "status_code": 200, "error": null, "duration_ms": 184 } }

An event, with its headers:

application.status_changed
POST /licium HTTP/1.1
Host: hooks.yourplatform.com
Content-Type: application/json
User-Agent: Licium-Webhooks/1.0
Licium-Event-Id: 0b6f3a9e-7c2d-4e51-8a4f-1d9c6b2e7f05
Licium-Event-Type: application.status_changed
Licium-Signature: t=1792674251,v1=5f0c8b1e...e2a1

{
  "id": "0b6f3a9e-7c2d-4e51-8a4f-1d9c6b2e7f05",
  "type": "application.status_changed",
  "created_at": "2026-10-21T13:04:11.000Z",
  "account_id": "8f2d4c1a-6b3e-4f7a-9c21-5d0e8b7a3f10",
  "data": {
    "application_id": "3c9a7e52-1f4b-4d8e-a6c0-2b7f9d1e4a83",
    "npi": "1234567893",
    "payer": "Aetna",
    "state": "TX",
    "status": "approved",
    "previous_status": "in_review",
    "effective_date": "2026-11-01",
    "missing_documents": null,
    "decided": true,
    "checked_at": "2026-10-21T13:04:10.512Z",
    "next_check_at": null,
    "payer_said": "Approved, effective November 1, 2026.",
    "source": { "method": "phone", "automated": false, "reference": "Ref 55102, rep Dana", "note": null },
    "stopped": null
  }
}

Check the signature

Headers on every event: Licium-Signature, Licium-Event-Id, Licium-Event-Type, Content-Type: application/json. Licium-Signature is t=<unix seconds>,v1=<hex>, where the hex is HMAC-SHA256, keyed with your webhook.secret (the whole lws_... string), of <t>.<raw body>: the timestamp, a dot, then the body bytes exactly as received. Compute it before parsing the JSON, compare in constant time, and refuse a t more than five minutes old. In Node, saved as licium-signature.mjs, or in Python.

  • Answer any 2xx within 5 seconds; do the work after answering. Redirects are not followed.
  • Anything else (an error status, a timeout, no connection) is tried again after 1 minute, 10 minutes, 1 hour and 6 hours: 5 tries in all, then the event is marked failed.
  • Delivery is at least once and not in order: skip a Licium-Event-Id you already stored, and keep the status with the latest data.checked_at.
  • A missed event never loses a status: GET /v1/payer-follow-ups/{id} always has the current one. GET /v1/payer-follow-ups/{id}/webhook lists the latest 20 events with every try (time, HTTP status, error, duration).
  • An event goes to the address set when it is sent. DELETE /v1/payer-follow-ups/{id}/webhook removes the address and cancels the events still waiting.
licium-signature
import { createHmac, timingSafeEqual } from "node:crypto";

// rawBody: the request body exactly as received (a Buffer or string), before any JSON parsing.
// signatureHeader: the Licium-Signature header. secret: webhook.secret (lws_...).
export function isFromLicium(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = String(signatureHeader ?? "").split(",").map((p) => p.trim());
  const t = Number(parts.find((p) => p.startsWith("t="))?.slice(2));
  if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.`).update(rawBody).digest();
  return parts.filter((p) => p.startsWith("v1=")).some((p) => {
    const given = Buffer.from(p.slice(3), "hex");
    return given.length === expected.length && timingSafeEqual(given, expected);
  });
}

A receiver using it:

receiver.mjs
import { createServer } from "node:http";
import { isFromLicium } from "./licium-signature.mjs";

createServer((req, res) => {
  const chunks = [];
  req.on("data", (chunk) => chunks.push(chunk));
  req.on("end", () => {
    const raw = Buffer.concat(chunks);
    if (!isFromLicium(raw, req.headers["licium-signature"], process.env.LICIUM_WEBHOOK_SECRET)) {
      res.writeHead(401).end();
      return;
    }
    res.writeHead(204).end(); // within 5 seconds; do the work after answering
    const event = JSON.parse(raw);
    // Skip an event.id you already stored; keep the status with the latest data.checked_at.
    queueForProcessing(event);
  });
}).listen(3000);

GET /v1/payer-follow-ups/{id}

4. Read the status

GET /v1/payer-follow-ups/{id} returns the payment state and every application: its status, effective date, missing documents, the payer's reference or representative, the last and next check, and the log of every check (date, method, what the payer said, where it came from). webhook is the status webhook's url and updated_at (when it was last set), or null; the secret is never shown again. For an application we write, submission says where it stands (its step, the signer, when it was sent and delivered, and dated events), or is null before we start it. Reading is free and never starts a check.

GET /v1/payer-follow-ups/{id}
curl https://www.licium.ai/v1/payer-follow-ups/$ACCOUNT_ID \
  -H "X-Payer-Follow-Up-Token: $ACCESS_TOKEN"
200 OK
{
  "success": true,
  "data": {
    "account_id": "8f2d4c1a-6b3e-4f7a-9c21-5d0e8b7a3f10",
    "standing": "active",
    "payment": { "status": "paid", "paid_at": "2026-10-21T13:02:44.000Z", ... },
    "summary": { "not_checked": 0, "received": 0, "in_review": 1, "needs_documents": 0,
                 "approved": 1, "denied": 0, "no_record": 0, "no_decision": 0 },
    "applications": [
      {
        "id": "3c9a7e52-1f4b-4d8e-a6c0-2b7f9d1e4a83",
        "npi": "1234567893", "clinician_name": "ADA LOVELACE",
        "payer": "Aetna", "state": "TX", "submitted_on": "2026-09-14",
        "status": "approved", "effective_date": "2026-11-01", "missing_documents": null,
        "reference_or_rep": "Ref 55102, rep Dana",
        "last_checked_at": "2026-10-21T14:31:07.000Z", "next_check_at": null,
        "check_every_days": 2, "coverage": "decided", "stopped": null,
        "checks": [
          { "checked_at": "2026-10-21T14:31:07.000Z", "method": "phone", "status": "approved",
            "payer_said": "Approved, effective November 1, 2026.", "effective_date": "2026-11-01", ... }
        ]
      },
      { "id": "...", "payer": "Cigna", "status": "in_review", "check_every_days": 1,
        "next_check_at": "2026-10-22T13:00:00.000Z", ... }
    ],
    "notify": "platform_only", "notify_email": "credentialing@yourplatform.com",
    "webhook": { "url": "https://hooks.yourplatform.com/licium", "updated_at": "2026-10-21T13:00:12.000Z" },
    "next": "Each open application is checked until it is decided: ...",
    "authorized_by": "token"
  }
}

Statuses

StatusMeaningFinal
not_checkedNot checked yet. Every application starts here; no event is sent for it.No
receivedThe payer has the application and has not started its review.No
in_reviewThe payer is reviewing it.No
needs_documentsThe payer is missing something: missing_documents says what. You hear as soon as a check finds it.No
approvedDecided: approved. effective_date is the date the payer gave. No more checks or events for it.Yes
deniedDecided: denied. No more checks or events for it.Yes
no_recordThe payer has no record of it yet. It is checked again; three such checks in a row over at least three weeks stop it (no_decision).No
no_decisionThe checks stopped without a decision; stopped.reason says why. Reopen it at no charge within 30 days to start the checks again.Yes, unless reopened

How often each application is checked

Applications at payers that post their status online are read every 30 minutes, day and night, so a change the payer posts is caught within about half an hour; the others are checked every week, or as often as every business day with check_every_days (1 to 7 days, for the whole request or per application). Each application's next_check_at says when it is checked next and check_every_days what was set for it; an entry's own value wins over the request's. A status page read that finds what the latest check already says moves last_checked_at without adding an entry to checks, and sends no event. The price does not change with how often it is checked.

POST /v1/payer-follow-ups/{id}/applications/{application_id}/reopen

When the checks stop, and reopening

An application is checked until the payer approves or denies it, with no time limit. An application that is never decided is not checked forever: on the per-application plan and the application bundle it is closed as no_decision, and you get an email, when the payer says it has no record of the application on 3 checks in a row spanning at least 3 weeks, or 12 months after the later of the day it was submitted and the day it was paid for, without a decision. Reopen it at no charge within 30 days: reply to that email, or POST /v1/payer-follow-ups/{id}/applications/{application_id}/reopen with the access token. A reopened application is checked again right away, and the 12 months and the no-record checks count again from the reopen. A stop sends an application.status_changed event with status no_decision and data.stopped; the status read shows the same under stopped, with reopen_path while the 30 days last. Reopening sends an event too (previous_status no_decision).

Reopen an application
curl -X POST https://www.licium.ai/v1/payer-follow-ups/$ACCOUNT_ID/applications/$APPLICATION_ID/reopen \
  -H "X-Payer-Follow-Up-Token: $ACCESS_TOKEN"

What your practices see

notifyEmailsStripe checkout and receipt
practice (default)To the follow-up's email: payment confirmation, a missing document, the Monday status table, a stopped application. Each links to its status page.Addressed to the follow-up's email.
platform_onlyEvery one of those goes only to notify_email, its subject marked with the practice ("[For Bright Therapy Group] ..."). None to the practice.Addressed to notify_email.
webhook_onlyNone at all. Status reaches you by the webhook and the status read (webhook_url is required, and cannot be removed).Not addressed: whoever pays types an email.

We follow up with the payer on the practice's behalf. With an access method other than call_only, someone at the practice adds our user or signs the letter, and the steps go to whoever gets the emails. The status page /payer-follow-up/{id} opens with the access token, or for someone who signs in on licium.ai with the follow-up's email.

Pricing

PlanPriceWhat it covers
per_application$79 per application, one timeOne clinician with one payer, checked until decided, no time limit. Stops and reopens are free. Optional roster monitoring: $3 per clinician per month.
group$15 per clinician per month, plus $149 per month for each clinician with a pending applicationThe whole roster monitored weekly (license, Medicare revalidation, exclusion list, NPI registry), plus every pending application checked; a clinician leaves the $149 line when all their applications are decided or after six months.
application_bundle$199 per application, one timeWe write and submit the application, then check it until decided.

Paid up front at checkout; no free trial. A subscription can be cancelled any time and stops from the next month.

For billing firms and software platforms: volume pricing, set up with us rather than at checkout. Book 20 minutes or write to hello@licium.ai. Once it is agreed, send the partner_code we give you with each create and your price is charged instead of the list price.

Errors

HTTPWhenWhat to do
400Input that cannot be used: the error names the field; on create, invalid_entries lists each entry with its reason. A webhook_url that is not public HTTPS, or a malformed Idempotency-Key. Nothing is saved.Fix and send again (the same Idempotency-Key is fine).
401No access token sent.Send X-Payer-Follow-Up-Token.
403A change sent from a browser on another site (its Origin header is not licium.ai).Call from your server, which sends no Origin header.
404The follow-up or application does not exist, or this token does not open it.Check the id and that the token is this follow-up's.
409Not possible in this state: reopening an application that is not stopped (or after 30 days), a webhook test with no address, removing the webhook of a webhook_only follow-up, a checkout for one already paid. On create: an Idempotency-Key already used with a different body, or one whose first request is still being handled (with Retry-After).Read the error; nothing changed. For a new follow-up, send a new key.
413More than 500 clinicians or 300 applications, or a body over 4.4 MB.Split it into several follow-ups.
429Over a rate limit (below).Wait the seconds in Retry-After.
502Create only: saved, but the payment page did not open. data has account_id, access_token and webhook.Store them, then POST /v1/payer-follow-ups/{id}/checkout. Do not create again.
503A temporary failure (on create with nothing saved).Try again in a minute.
400 Bad Request
HTTP/1.1 400 Bad Request

{
  "success": false,
  "error": "One entry could not be used. Each one is listed with its reason; fix them and send again.",
  "invalid_entries": [
    {
      "entry": "Application 2: 1234567890, Cigna",
      "reason": "NPI: The last digit does not match the NPI check digit. Check for a typo."
    }
  ]
}

Rate limits

Per hour, from one IP address. A 429 carries Retry-After in seconds. If your platform needs more, write to hello@licium.ai.

CallPer hour
POST /v1/payer-follow-ups60
GET /v1/payer-follow-ups/{id}1200
POST /v1/payer-follow-ups/{id}/checkout30
GET /v1/payer-follow-ups/{id}/webhook300
PUT and DELETE /v1/payer-follow-ups/{id}/webhook (together)30
POST /v1/payer-follow-ups/{id}/webhook/test20
POST /v1/payer-follow-ups/{id}/applications/{applicationId}/reopen60
GET /v1/payer-follow-ups/{id}/documents1200
POST /v1/payer-follow-ups/{id}/documents120
GET /v1/payer-follow-ups/{id}/report1200
GET /v1/payer-follow-ups/{id}/signers1200
POST /v1/payer-follow-ups/{id}/signers120
the signing page600
the signing page's codes40
the signing page's actions120
the clinician's list600
the clinician's list actions120

Retries and idempotency

  • Send an Idempotency-Key with every create and keep it until you have the reply. A retry with the same key and body within 24 hours returns the first reply (replayed: true, the same account_id), never a second follow-up. The token is not in that repeated reply: if the first reply was lost, sign in on licium.ai with the follow-up's email to open it.
  • Without the header, create is not idempotent: a retry makes a second follow-up awaiting payment. Nothing is charged or checked until a checkout is paid, so pay only the one whose account_id you stored; an unpaid one does nothing.
  • A 502 on create means it was saved: never create again with a new key; open the payment page with POST /v1/payer-follow-ups/{id}/checkout. A retry with the same key repeats the 502.
  • Reads, webhook reads and the checkout call are safe to repeat (checkout returns the open page again). Setting the webhook again makes a new secret each time: keep the latest.
  • Reopening twice: the second call answers 409, nothing changes.
  • There is no separate test mode. Creating a follow-up costs nothing until it is paid, and the webhook test works before payment, so you can build your receiver first.

Using MCP instead

The hosted MCP server at https://www.licium.ai/mcp has the same calls as tools, with the same input and the same replies: payer_follow_up_request (the fields of POST /v1/payer-follow-ups, plus idempotency_key, sent as the Idempotency-Key header) and payer_follow_up_status (account_id and access_token). With the same two: payer_follow_up_webhook_set (url; returns the new secret once, like PUT /v1/payer-follow-ups/{id}/webhook), payer_follow_up_webhook_test (like POST /v1/payer-follow-ups/{id}/webhook/test) and payer_follow_up_reopen (application_id; like the reopen call). For an application bundle, payer_follow_up_documents and payer_follow_up_document_send list and send the documents, and payer_follow_up_signers and payer_follow_up_signer_set show and set who signs each clinician's applications (like GET and POST /v1/payer-follow-ups/{id}/signers; only the clinician signs). Reading the webhook's deliveries and removing it are REST only.

Questions while you build: hello@licium.ai. The full Licium API is at https://www.licium.ai/openapi.json.

Add the server
claude mcp add --transport http licium https://www.licium.ai/mcp