curl --request GET \
--url https://api.maxcare.ai/v3/era-service-lines \
--header 'Authorization: Bearer <token>' \
--header 'X-Organization-Id: <x-organization-id>'import requests
url = "https://api.maxcare.ai/v3/era-service-lines"
headers = {
"X-Organization-Id": "<x-organization-id>",
"Authorization": "Bearer <token>"
}
response = requests.get(url, headers=headers)
print(response.text)const options = {
method: 'GET',
headers: {'X-Organization-Id': '<x-organization-id>', Authorization: 'Bearer <token>'}
};
fetch('https://api.maxcare.ai/v3/era-service-lines', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.maxcare.ai/v3/era-service-lines",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"X-Organization-Id: <x-organization-id>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.maxcare.ai/v3/era-service-lines"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-Organization-Id", "<x-organization-id>")
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.maxcare.ai/v3/era-service-lines")
.header("X-Organization-Id", "<x-organization-id>")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.maxcare.ai/v3/era-service-lines")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-Organization-Id"] = '<x-organization-id>'
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"code": "success",
"data": {
"eraServiceLines": [
{
"id": "esl_a1b2c3d456784abc9def0123456789ab",
"eraClaimId": "erc_b2c3d4e567894abc9def0123456789ab",
"claimId": "clm_c3d4e5f678904abc9def0123456789ab",
"billId": "bil_d4e5f6a789014abc9def0123456789ab",
"patientId": "pat_e5f6a7b890124abc9def0123456789ab",
"eraClaimControlNumber": "12345678",
"payerClaimControlNumber": "0000123456789",
"claimStatusCode": "1",
"claimStatusCodeDescription": "Processed as Primary",
"eraClaimChargeAmount": "13775.61",
"eraClaimPaidAmount": "13669.70",
"eraClaimPatientResponsibilityAmount": "0.00",
"checkNumber": "EFT2601234567",
"checkDate": "2026-07-08T00:00:00.000Z",
"isEra": true,
"payerName": "Blue Cross Blue Shield of Michigan",
"payerCode": "00710",
"postedAmount": "6420.00",
"unpostedAmount": "0.00",
"omittedAmount": "0.00",
"procedureCode": "J3245",
"procedureDescription": "Injection, tildrakizumab, 1 mg",
"modifiers": [
"JW"
],
"serviceDate": "2026-02-10T00:00:00.000Z",
"billedUnits": "100.000",
"paidUnits": "100.000",
"chargeAmount": "12000.00",
"allowedAmount": "8420.00",
"paidAmount": "6420.00",
"deductibleAmount": "2000.00",
"coinsuranceAmount": "0.00",
"copayAmount": "0.00",
"patientResponsibilityAmount": "2000.00",
"adjustmentAmount": "3580.00",
"adjustments": [
{
"id": "ead_e1f2a3b4c5d64e7f8a9b0c1d2e3f4a5b",
"carcGroup": "CO",
"carc": "45",
"carcDescription": "Charge exceeds fee schedule/maximum allowable",
"rarcs": [
"N130"
],
"adjustmentAmount": "9820.00",
"patientResponsibilityAmount": "0.00"
}
],
"createdAt": "2026-03-01T12:00:00.000Z",
"updatedAt": "2026-03-14T09:30:00.000Z"
}
],
"pagination": {
"page": 1,
"pageSize": 100,
"totalCount": 250,
"totalPages": 3
}
}
}{
"code": "bad_request",
"message": "'id' must be a valid UUID",
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}{
"code": "unauthorized",
"message": "Invalid or missing API key",
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}{
"code": "forbidden",
"message": "Insufficient scope",
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}{
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Maximum 1000 requests per 60 seconds.",
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}List ERA service lines
Returns a paginated list of payer remittance service lines — per-procedure allowed, paid and patient-responsibility amounts with their CARC/RARC adjustment reasons (deductible, coinsurance and copay are EzDerm only — NULL on every ModMed line, so never sum the three to derive patient responsibility; use patientResponsibilityAmount). Sync on updatedSince: payer money moves for 90+ days after service. Requires read:eras scope. UNANCHORED MONEY: pass matched=false to get only the lines with no claim behind them, instead of sweeping the whole remit history to find them. TOTALS ARE ALWAYS RETURNED here — pagination.totalCount/totalPages are never null and there is no includeTotals opt-out (unlike /bills, where the second aggregate dominates the request). They are counted over the whole filtered set on every page, so they move with concurrent writes: treat a totalCount that SHRINKS between pages of one walk as a walk you must not trust — a delete has slid rows past your offset — and re-read the window rather than concluding you saw everything.
curl --request GET \
--url https://api.maxcare.ai/v3/era-service-lines \
--header 'Authorization: Bearer <token>' \
--header 'X-Organization-Id: <x-organization-id>'import requests
url = "https://api.maxcare.ai/v3/era-service-lines"
headers = {
"X-Organization-Id": "<x-organization-id>",
"Authorization": "Bearer <token>"
}
response = requests.get(url, headers=headers)
print(response.text)const options = {
method: 'GET',
headers: {'X-Organization-Id': '<x-organization-id>', Authorization: 'Bearer <token>'}
};
fetch('https://api.maxcare.ai/v3/era-service-lines', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api.maxcare.ai/v3/era-service-lines",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "GET",
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"X-Organization-Id: <x-organization-id>"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"net/http"
"io"
)
func main() {
url := "https://api.maxcare.ai/v3/era-service-lines"
req, _ := http.NewRequest("GET", url, nil)
req.Header.Add("X-Organization-Id", "<x-organization-id>")
req.Header.Add("Authorization", "Bearer <token>")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.get("https://api.maxcare.ai/v3/era-service-lines")
.header("X-Organization-Id", "<x-organization-id>")
.header("Authorization", "Bearer <token>")
.asString();require 'uri'
require 'net/http'
url = URI("https://api.maxcare.ai/v3/era-service-lines")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Get.new(url)
request["X-Organization-Id"] = '<x-organization-id>'
request["Authorization"] = 'Bearer <token>'
response = http.request(request)
puts response.read_body{
"code": "success",
"data": {
"eraServiceLines": [
{
"id": "esl_a1b2c3d456784abc9def0123456789ab",
"eraClaimId": "erc_b2c3d4e567894abc9def0123456789ab",
"claimId": "clm_c3d4e5f678904abc9def0123456789ab",
"billId": "bil_d4e5f6a789014abc9def0123456789ab",
"patientId": "pat_e5f6a7b890124abc9def0123456789ab",
"eraClaimControlNumber": "12345678",
"payerClaimControlNumber": "0000123456789",
"claimStatusCode": "1",
"claimStatusCodeDescription": "Processed as Primary",
"eraClaimChargeAmount": "13775.61",
"eraClaimPaidAmount": "13669.70",
"eraClaimPatientResponsibilityAmount": "0.00",
"checkNumber": "EFT2601234567",
"checkDate": "2026-07-08T00:00:00.000Z",
"isEra": true,
"payerName": "Blue Cross Blue Shield of Michigan",
"payerCode": "00710",
"postedAmount": "6420.00",
"unpostedAmount": "0.00",
"omittedAmount": "0.00",
"procedureCode": "J3245",
"procedureDescription": "Injection, tildrakizumab, 1 mg",
"modifiers": [
"JW"
],
"serviceDate": "2026-02-10T00:00:00.000Z",
"billedUnits": "100.000",
"paidUnits": "100.000",
"chargeAmount": "12000.00",
"allowedAmount": "8420.00",
"paidAmount": "6420.00",
"deductibleAmount": "2000.00",
"coinsuranceAmount": "0.00",
"copayAmount": "0.00",
"patientResponsibilityAmount": "2000.00",
"adjustmentAmount": "3580.00",
"adjustments": [
{
"id": "ead_e1f2a3b4c5d64e7f8a9b0c1d2e3f4a5b",
"carcGroup": "CO",
"carc": "45",
"carcDescription": "Charge exceeds fee schedule/maximum allowable",
"rarcs": [
"N130"
],
"adjustmentAmount": "9820.00",
"patientResponsibilityAmount": "0.00"
}
],
"createdAt": "2026-03-01T12:00:00.000Z",
"updatedAt": "2026-03-14T09:30:00.000Z"
}
],
"pagination": {
"page": 1,
"pageSize": 100,
"totalCount": 250,
"totalPages": 3
}
}
}{
"code": "bad_request",
"message": "'id' must be a valid UUID",
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}{
"code": "unauthorized",
"message": "Invalid or missing API key",
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}{
"code": "forbidden",
"message": "Insufficient scope",
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}{
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Maximum 1000 requests per 60 seconds.",
"trace_id": "550e8400-e29b-41d4-a716-446655440000"
}Authorizations
Marketplace API key
Headers
Target clinic organization ID
Query Parameters
Filter from service date (ISO 8601). A bare YYYY-MM-DD starts at 00:00:00 UTC. EXCLUDES ROWS WITH NO SERVICE DATE, and a remit line CAN have none. Most lines — anchored or not — do carry one, so a date window is NOT a way to isolate unanchored money (use matched=false for that); it is simply not exhaustive. Sweep with no date filter, or on updatedSince, when you need every row.
"2025-01-01"
Filter to service date (ISO 8601). A bare YYYY-MM-DD is inclusive to the END of that day in UTC, so a caller in a negative-offset timezone gets a window that closes earlier in local time. Excludes rows with no service date, same as dateFrom — see that field.
"2025-12-31"
Filter to remit lines matched to this bill. Unmatched remit lines (billId: null) can never satisfy this filter, so a bill-scoped pull will not see payer dollars the EHR failed to tie to a claim.
Filter to remit lines matched to this claim.
Filter by procedure code (CPT/HCPCS), comma-separated. Exact match, case-insensitive on both the value you send and the code as stored. Supplying the parameter with NO values — ?code= or ?code=, — is a 400, not an empty filter: build the query string so an empty cohort OMITS the parameter, because silently dropping it would return every remit line in the organization for a request that asked for a few.
"J3245,J0717"
Only return service lines whose record was created or modified at/after this timestamp (ISO 8601). Payer money is a moving target — recoupments, secondary payers and 835 PLB adjustments re-touch a line for 90+ days after service — so this is the cursor to sync on. Results are ALWAYS ordered by (updatedAt, id) ascending, cursor or not — a first backfill with no cursor gets the same order. THIS CURSOR CANNOT REPORT DELETIONS. ModMed's unposted -> posted transition hard-deletes a remit's composite-keyed lines and re-inserts them under new ids (see the id field). The delete bumps no timestamp and leaves no tombstone, so a delta-only consumer accumulates both copies and double-counts payer dollars. Pair this cursor with a periodic full re-pull of the claims/bills you track. UNMATCHED LINES HAVE NO ANCHOR: when claimId is null there is no claim or bill to reconcile against, and ?billId=/?claimId= cannot return those rows at all. Only a periodic pull with NO billId, claimId, dateFrom or dateTo filter can detect that one was deleted — serviceDate can be absent, so a date window is not exhaustive either. Scope the sweep with matched=false instead. RECOMMENDED RECIPE: request page=1 every time and advance updatedSince to the LAST row's updatedAt after each page, rather than walking page=2,3,… through one long run. Deep paging is not stable here: a row re-touched by an ERA re-sync while you page moves to the end of the ordering, every later row shifts down one, and the row that crosses the page boundary is skipped — its updatedAt stays below the cursor you end up storing, so it is never delivered again. Rows sharing the boundary updatedAt re-deliver on the next pull, since the bound is inclusive — dedupe on id. One caveat to the recipe: ERA sync re-touches a whole claim at once, so if MORE rows share a single EMITTED updatedAt than fit in one page, advancing the cursor cannot make progress — page deeper (page=2,3,…) within that timestamp until it is exhausted, then advance. The threshold is a shared MILLISECOND, not a shared instant: the column is a microsecond timestamp and this field truncates, so rows stored at .999750 and .999999 are distinct in the database but are both emitted — and both matched by a cursor — as .999Z. OVERLAP YOUR CURSOR. updatedAt is stamped with the WRITING TRANSACTION'S START time, and rows only become visible at its COMMIT. A sync that starts at 10:00 and commits at 10:05 writes rows stamped 10:00 that a poll at 10:02 cannot see — if that poll advanced your cursor to 10:01, those rows are below it forever. Re-request from (your stored cursor MINUS a margin at least as long as the longest sync transaction; minutes, not seconds) and dedupe on id. Storing the cursor with no overlap will silently lose whole ERAs.
"2025-06-01T00:00:00.000Z"
Only return service lines whose record was last modified at/before this timestamp (ISO 8601). A bare YYYY-MM-DD is treated as the END of that day IN UTC, so a window like updatedSince=2026-03-01&updatedUntil=2026-03-03 includes everything re-touched on the 3rd. PREFER THE BARE DATE for calendar windows. An explicit instant is compared inclusively at MILLISECOND precision, and the column stores microseconds — so …T23:59:59.999Z silently excludes rows at .999001-.999999, which your next window then starts above. If you must pass an instant, pass the next window's start (e.g. 2025-07-01T00:00:00.000Z), which over-delivers by one instant rather than losing rows.
"2025-06-30"
Anchor filter. false returns ONLY the lines this API reports with claimId: null — payer dollars with no claim of ours behind them — which is otherwise findable only by sweeping the organization's entire remit history: billId=/claimId= can never return them, and a dateFrom/dateTo window is not exhaustive, because serviceDate can be absent. true returns only anchored lines. Omit for both. KEYED ON THE CLAIM, NOT THE BILL. billId is resolved THROUGH the claim, so a line with no claim never has a bill — but a line whose claim carries no unarchived bill is still matched=true with billId: null. If you are looking for money you cannot post, filter matched=false AND check billId on the rest. It follows the RESPONSE, not the EHR: a line whose matched claim was later archived reports claimId: null and is returned by matched=false, because that is the row whose linkage you cannot follow. It is not a cursor. Matched-ness changes without re-touching a line's updatedAt, so a line can move between the two cohorts and never reappear in an updatedSince walk (see claimId) — re-run the matched=false sweep on a schedule instead of subscribing to it.
false
