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(orAuthorization: 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
- Create a follow-up for a practice with its pending applications and your webhook address.
- Pay at checkout_url ($79 per application at the list price). Nothing is checked before.
- Verify the signature on each event and store the new status.
- Read the full status and check log whenever you like.
- 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.
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"
}'// Node 18 or later (fetch is built in).
import { randomUUID } from "node:crypto";
const BASE = "https://www.licium.ai";
// One key per follow-up: store it with the practice before sending, and send the same key on a retry.
const idempotencyKey = randomUUID();
const res = await fetch(`${BASE}/v1/payer-follow-ups`, {
method: "POST",
headers: { "Content-Type": "application/json", "Idempotency-Key": idempotencyKey },
body: JSON.stringify({
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" },
],
check_every_days: 2,
notify: "platform_only",
notify_email: "credentialing@yourplatform.com",
webhook_url: "https://hooks.yourplatform.com/licium",
}),
});
const reply = await res.json();
if (!reply.success) {
// 400: reply.error says what to fix; reply.invalid_entries lists each entry that cannot be used.
throw new Error(`${res.status}: ${reply.error} ${JSON.stringify(reply.invalid_entries ?? [])}`);
}
// Store these three now: access_token and webhook.secret are never shown again.
const { account_id, access_token, checkout_url, webhook } = reply.data;
// Pay at checkout_url (or send whoever pays there). Nothing is checked until the payment settles.The reply. access_token and webhook.secret are shown only here: store them with the practice.
{
"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:
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.
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.
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" }'curl -X POST https://www.licium.ai/v1/payer-follow-ups/$ACCOUNT_ID/webhook/test \
-H "X-Payer-Follow-Up-Token: $ACCESS_TOKEN"{ "success": true,
"data": { "event_id": "5e1f...", "delivered": true, "status_code": 200, "error": null, "duration_ms": 184 } }An event, with its headers:
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-Idyou already stored, and keep the status with the latestdata.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}/webhooklists 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}/webhookremoves the address and cancels the events still waiting.
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);
});
}import hashlib, hmac, time
def is_from_licium(raw_body: bytes, signature_header: str, secret: str, tolerance: int = 300) -> bool:
parts = [p.strip() for p in (signature_header or "").split(",")]
t = next((p[2:] for p in parts if p.startswith("t=")), "")
if not t.isdigit() or abs(time.time() - int(t)) > tolerance:
return False
expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(p[3:], expected) for p in parts if p.startswith("v1="))A receiver using it:
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.
curl https://www.licium.ai/v1/payer-follow-ups/$ACCOUNT_ID \
-H "X-Payer-Follow-Up-Token: $ACCESS_TOKEN"const res = await fetch(`${BASE}/v1/payer-follow-ups/${account_id}`, {
headers: { "X-Payer-Follow-Up-Token": access_token },
});
const { data: followUp } = await res.json();
// followUp.standing: awaiting_payment until the payment settles, then active.
for (const app of followUp.applications) {
console.log(app.payer, app.state, app.status, app.effective_date ?? "", app.next_check_at ?? "");
}{
"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
| Status | Meaning | Final |
|---|---|---|
| not_checked | Not checked yet. Every application starts here; no event is sent for it. | No |
| received | The payer has the application and has not started its review. | No |
| in_review | The payer is reviewing it. | No |
| needs_documents | The payer is missing something: missing_documents says what. You hear as soon as a check finds it. | No |
| approved | Decided: approved. effective_date is the date the payer gave. No more checks or events for it. | Yes |
| denied | Decided: denied. No more checks or events for it. | Yes |
| no_record | The 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_decision | The 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).
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
| notify | Emails | Stripe 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_only | Every 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_only | None 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
| Plan | Price | What it covers |
|---|---|---|
| per_application | $79 per application, one time | One 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 application | The 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 time | We 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
| HTTP | When | What to do |
|---|---|---|
| 400 | Input 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). |
| 401 | No access token sent. | Send X-Payer-Follow-Up-Token. |
| 403 | A change sent from a browser on another site (its Origin header is not licium.ai). | Call from your server, which sends no Origin header. |
| 404 | The 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. |
| 409 | Not 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. |
| 413 | More than 500 clinicians or 300 applications, or a body over 4.4 MB. | Split it into several follow-ups. |
| 429 | Over a rate limit (below). | Wait the seconds in Retry-After. |
| 502 | Create 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. |
| 503 | A temporary failure (on create with nothing saved). | Try again in a minute. |
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.
| Call | Per hour |
|---|---|
| POST /v1/payer-follow-ups | 60 |
| GET /v1/payer-follow-ups/{id} | 1200 |
| POST /v1/payer-follow-ups/{id}/checkout | 30 |
| GET /v1/payer-follow-ups/{id}/webhook | 300 |
| PUT and DELETE /v1/payer-follow-ups/{id}/webhook (together) | 30 |
| POST /v1/payer-follow-ups/{id}/webhook/test | 20 |
| POST /v1/payer-follow-ups/{id}/applications/{applicationId}/reopen | 60 |
| GET /v1/payer-follow-ups/{id}/documents | 1200 |
| POST /v1/payer-follow-ups/{id}/documents | 120 |
| GET /v1/payer-follow-ups/{id}/report | 1200 |
| GET /v1/payer-follow-ups/{id}/signers | 1200 |
| POST /v1/payer-follow-ups/{id}/signers | 120 |
| the signing page | 600 |
| the signing page's codes | 40 |
| the signing page's actions | 120 |
| the clinician's list | 600 |
| the clinician's list actions | 120 |
Retries and idempotency
- Send an
Idempotency-Keywith 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 sameaccount_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_idyou 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.
claude mcp add --transport http licium https://www.licium.ai/mcp