advancedmd_post_payment, the AdvancedMD payment posting tool exposed through the Agents Platform. For charge lookup before posting, see AdvancedMD Find Charge.
Authentication
AdvancedMD payment posting uses the existing AdvancedMD connector:office_codeusernamepassword
processrequest.aspx URLs.
If AdvancedMD is not connected and dry_run=false, the tool returns a structured advancedmd_not_connected result with suggested actions:
- Connect AdvancedMD and try again
- Stop task
Safety
advancedmd_post_payment is a mutating tool. It creates an AdvancedMD paymententry.addpayments entry when posting is enabled.
dry_run defaults to true. In dry-run mode, the tool builds and returns the exact AdvancedMD payload without posting it.
To post to AdvancedMD, call the tool with:
profile_id resolution needs a connection
profile_id is the @profile on both payload shapes. When it is not supplied it is resolved from provider_code/ehr_provider_code via selectprovider, which requires a live AdvancedMD connection — including on a dry_run, and now for unapplied payments as well as charge payments. A dry run that supplies profile_id stays fully offline; one that omits it returns advancedmd_not_connected when AdvancedMD is not connected. Offline preview and eval callers should pass profile_id explicitly.
Pre-post guards: voided charges and duplicate payments
Every real charge post (not dry runs) first reads the charge row and its applied payments from transaction history. One read powers two guards:charge_voided— the charge is voided in AdvancedMD and can never accept a payment. Non-retryable; the line should be escalated as “charge voided in AdvancedMD”.duplicate_payment— a live (non-voided) payment with the same check number and amount is already applied to this charge, so posting again would double-pay the line. Non-retryable; mark the line as posted. If the EOB genuinely carries two identical payments for one charge, escalate to billing.
Amount guards: reversals and empty $0 posts
Two argument shapes are refused up front (both dry runs and real posts), as structured non-retryable results:negative_payment_amount— the payment amount is negative. EOB reversal/recoupment rows are not payments; AdvancedMD rejects them (“The field amount must be greater then 0”). The line should be handled by unapplying the original payment (AdvancedMD Unapply Payment) or escalated as a reversal.zero_amount_noop— a 0 unapplied payment (the unapplied payload carries no reason list, so $0 there is always a no-op). Posting either would only create an empty payment record in AdvancedMD without changing any balance. Zero-pay/denial charge lines are legitimate with their CARC codes (see the full-contractual-write-off section below); otherwise the line should be skipped.
AdvancedMD balance errors
Three distinct AdvancedMD rejections are reported as structured, non-retryable results rather than raw errors, because retrying with the same numbers reproduces them:charge_not_active means the payment referenced a charge id with no active-charge row — the charge was voided, deleted, or replaced. This is the server-side backstop for the case the pre-post voided guard fails open on (the charge missing from the scanned history pages). The guidance is to re-run advancedmd_find_charge and post to the live replacement charge, never to retry the same id.
insurance_balance_would_go_negative carries sent_balances (payment, allowed, computed write-off, and the insurance balance the caller supplied) and names three causes to check: the line is already posted; allowed_amount was missing/0 so AdvancedMD wrote off the whole charge; or — common on secondary EOBs — a prior payer already took the contractual adjustment, in which case the re-post should drop the CO write-off codes.
charge_balance_error’s trigger is not yet established. It appears both after a balance move (the post carrying balances read before the move) and when re-posting an already-posted charge. Until it is understood, the result carries the balances that were sent (sent_balances) plus AdvancedMD’s own amd_error_ref (e.g. AMD-154365-08030017) so an occurrence can be matched to AdvancedMD’s logs, and a warn-level log line is emitted per occurrence carrying grouping keys only — charge_id, amd_error_ref, payload_type. The dollar amounts are deliberately kept out of Loki (HIPAA logging posture); the full financial context lives in the step result’s sent_balances, queryable from the steps table.
To find occurrences:
advancedmd_find_charge and either skip the line if it is already posted or re-post with the freshly read balances — never to retry the same request.
allowed_amount and full contractual write-offs
allowed_amount is required when a charge is posted with a contractual write-off, because a missing value makes AdvancedMD write off the whole charge and drive the insurance balance negative.
For a real $0-paid post the insurance balance is re-confirmed against AdvancedMD before the exemption is applied, because a supplied balance is often stale by the time the post fires (45 of 54 Error In Balance occurrences had one). When the live balance agrees this costs one gettxhistory read; the retry-with-delay path (up to ~4.5s) only engages when the balance actually reads zero or the charge is missing — exactly the cases where posting on the supplied value would have been wrong. A read that cannot be confirmed is an error rather than a post on the unconfirmed value.
A dry run skips that read — it writes nothing. Consequence for preview callers: a dry run evaluates the full-write-off exemption against the supplied balance, the real post against the live one, so a dry run can preview “posts” and the real post then refuse with insurance_balance_zero if the charge settled in between. Treat a dry-run preview as validating the request’s shape, not the charge’s current state.
There is one sanctioned exception: when the payer paid nothing and the write-off clears the entire insurance balance — a CPT the plan does not cover, or a patient who has already used what the plan allows. The payer allowed nothing, so allowed_amount is legitimately 0, the charge is written off in full, and the post proceeds without human review (confirmed with RCM, 2026-08-03).
The guard still refuses every other shape:
Unapplied payments and the provider profile
When nocharge_id is supplied, the tool posts an unapplied payment (unappliedpaymentlist) instead of applying it to a charge. That payload’s @profile is the numeric provider profile id, never the provider code — a code there is an invalid FID reference and AdvancedMD rejects the insert with PD_FIDReferences on actv_PaymentDetails. This is why profile_id is now resolved for both payload shapes, not only when a charge was matched: pass profile_id, or provider_code/ehr_provider_code for the tool to resolve it via selectprovider.
Historical note: 7 of 11 unapplied posts failed this way between 24 Jul and 3 Aug 2026, while the applied-charge payload — which always used profile_id — never failed across 3,104 calls.
Payor (@carrierid) and payment method (@paymethod)
Payor. The payment record’s payor is @carrierid, which takes AdvancedMD’s carrier id (e.g. car7552) — not the display code advancedmd_find_charge returns (e.g. AET07). Sending the code is invalid, and the previous behaviour of sending nothing at all recorded every agent-posted payment with no payor, which is what a customer saw on 2026-08-12: an AdvancedMD payment report listing check numbers with a blank Payor Name column.
On a real charge post the charge’s own carrier wins. The pre-post history read already fetches the charge row, so its carrier code is used as the payor and the caller’s carrier/carrier_id argument is ignored (a mismatch is logged at warn). A payment belongs to the carrier the charge is billed to — on a secondary or tertiary EOB the remittance payer is not that carrier, and 39 of 3,296 recent posts passed a code that differed from the charge’s. The argument is still used where there is no charge row to read: unapplied payments, dry runs, and a charge the precheck could not find.
Every id is confirmed against AdvancedMD before it is sent, because a wrong payor is a worse record than no payor: an id-shaped or bare-numeric value is treated as a candidate and verified with selectcarriers (60054 is Aetna’s payer id, not an AMD carrier id — it resolves to nothing and is dropped), and anything else is resolved by code via lookupcarrier. Resolution is best-effort: an unknown code, an unconfirmed id, or a failed lookup posts with no carrier and logs a warn — a raw display code is never passed through, which is the invalid value #1096 removed. Both lookup paths (code and id confirmation) are memoised per client in an expirable LRU (15-minute TTL, 512 keys), so a 200-line EOB from one payer costs one call while a carrier added in AdvancedMD is picked up on the next TTL rather than at the next restart. The only unconfirmed value ever sent is an id supplied to an offline dry run, where there is nothing to ask.
Resolution runs after the voided/duplicate and zero-balance gates, so a line that never posts spends no lookup, and a dry run that supplies an id stays offline. Because a dry run skips the pre-post read, it never sees the charge’s carrier: its preview resolves from the argument and the summary marks it — provisional; a real post uses the charge's own carrier. Where the charge’s carrier does displace a supplied one, the summary names the discarded value (car4001 (the charge's own carrier UNI20; supplied AET07 ignored)) rather than only logging it.
A code and an id can look alike (CAR07 is a plausible display code, car7552 is an id), so the tool asks AdvancedMD first and only falls back to reading the value as an id when no carrier matches it.
Every result that carries a built payload ends with Payor (@carrierid) and Payment Method rows, and exposes carrier_id / payment_method in the structured output — the resolved id with its provenance (car4001 (the charge's own carrier UNI20)), or ⚠️ unresolved from <code> when nothing confirmed, so a lost payor is visible before anything posts. The refusals that fire before a payload is built (voided charge, duplicate payment, negative or zero amount) have no payor to report and omit the rows.
advancedmd_find_charge also returns ehr_carrier_id on a match, resolved from the same lookupcarrier call it already makes for the display name — pass that through and posting needs no lookup at all.
Verified live against the test office: posting with carrier: "AETN" sends @carrierid=car7159, AdvancedMD accepts it, and the payment reads back from transaction history carrying carrier="AETN".
Payment method. @paymethod defaults to 2 (check), which is why every payment posted before this change landed in AdvancedMD’s “CHECK PAYMENTS” bucket. Set payment_method (in the tool settings, so it applies to every post) to the method the remittance actually arrived as — for payer ACH remittances that is ERA-ACH.
payment_method accepts a name or a raw code:
Any raw numeric code is also passed through, for a method an office has added to its own Payment Methods masterfile. An unknown name is rejected by
Validate rather than sent to AdvancedMD.
The ERA codes are read off AdvancedMD’s own ERA payment-entry screen (selPaymentMethod in actv_paymententry_era.html), because the API exposes no payment-method masterfile to query — selectpaymentmethods, lookuppaymethod, getpaymentmethodlist and eight further spellings across the masterfiles, api and paymententry classes all return “action not found”, and no payment read echoes @paymethod back. Verified live: payment_method: "ach" sends @paymethod=9 and AdvancedMD accepts the post.
Inputs
Required:patient_id: AdvancedMD patient ID.payment_amount: Payment amount as a decimal string.deposit_date: Deposit date, accepted asMM/DD/YYYYorYYYY-MM-DD.
payment_status: AdvancedMD payment status.BandBatchmean the same thing: post the payment in batch/auto-post status.HandHoldmean the same thing: put the payment on hold. Matching is case-insensitive, sob,batch,h, andholdalso work. If omitted, the tool derives the status from CARC codes using the source script logic: start withall_codes_safe = true; for each CARC code, ignore writeoff codes (CO253,CR253,CR45,PR45,CO45) for the safety check; if any remaining non-writeoff code is not one of the auto-post-safe codes (PR1,PR2,PR3), setall_codes_safe = false; final status isBonly whenall_codes_safeis true and at least one CARC reason was parsed, otherwiseH.charge_id: AdvancedMD charge ID. If omitted, the tool builds an unapplied payment payload.allowed_amount: Allowed amount.patient_responsibility: Patient responsibility from the EOB.carc_codesorcarc_codes_raw: CARC code amounts. Prefer a JSON object string such as{"CO45":"145.26","PR3":"50.00"}. LooseCODE AMOUNTtext also works for simple cases, such asPR1 5.50.carrier_idorcarrier: the payment’s payor. An AdvancedMD carrier id (car7552), a bare carrier number, or the display codeadvancedmd_find_chargereturns (AET07) — see Payor and payment method.payment_method: the payment method for@paymethod— a name (check,ach,era-bop, …) or a raw AdvancedMD code. Defaults to2(check); useachfor payer ACH remittances. See Payor and payment method.check_number: Check or trace number.provider_codeorehr_provider_code: AdvancedMD provider/profile code. Defaults to114for unapplied payments.ehr_insurance_portion,ehr_patient_portion,ehr_insurance_balance,ehr_patient_balance: Values returned byadvancedmd_find_charge.
80 is sent as 80.00.
Payload Modes
Whencharge_id is present, the tool builds a charge_payment payload and attaches payment/writeoff information under chargelist.
When charge_id is absent, the tool builds an unapplied_payment payload with an unappliedpaymentlist.
For charge payments, a writeofflist is included only when the computed writeoff amount is greater than 0.00. The writeoff uses AdvancedMD code WOINS.
CARC Handling
The tool preserves the payment-posting behavior from the source Python workflow:- CARC codes are normalized by uppercasing and removing non-alphanumeric characters.
@groupis derived from the first two characters of the normalized code.- Known CARC codes are mapped to AdvancedMD reason IDs. The current built-in mapping covers
CO45,CR45,PR1,PR2,PR3, andPR45. Other CARC codes are still included in the payload with@code,@group, and@amount, but their@reasonidis empty. - Writeoff amount is summed for configured writeoff CARC codes.
- If
payment_statusis omitted, the payment status is derived from CARC codes using the source script logic:Bonly when at least one CARC reason was parsed and every non-writeoff CARC code is auto-post-safe (PR1,PR2,PR3); otherwiseH. Writeoff codes (CO253,CR253,CR45,PR45,CO45) contribute to writeoff amount but do not make the payment unsafe. - If
payment_statusis provided, human-readable words and AdvancedMD codes both work:B,b,Batch, orbatchall mean Batch and are sent to AdvancedMD asB.H,h,Hold, orholdall mean Hold and are sent to AdvancedMD asH.- Example:
payment_status: "Batch"is the same aspayment_status: "B". - Example:
payment_status: "Hold"is the same aspayment_status: "H".
Recommended Workflow
Standard Matched-Charge Flow
- Use
advancedmd_find_chargeto locate the charge and obtain EHR fields such ascharge_id,patient_id, balances, portions, carrier, and provider code. - Call
advancedmd_post_paymentwith those values, EOB payment fields, CARC codes, anddry_run=true. - Review
payload_type,payment_status,writeoff_amount, and the generatedpayload. - If the payload is correct, call
advancedmd_post_paymentagain with the same inputs anddry_run=false.
Dry-Run Review Flow
Use this when the agent or user wants to inspect the exact AdvancedMD payload before any mutation.- Call
advancedmd_post_paymentwithout passingdry_run, or passdry_run=true. - Confirm that
postedisfalse. - Review the
payloadand markdown summary. - Only make a second call with
dry_run=falsewhen posting should actually happen.
Charge Found With Explicit Hold
Use this when the charge was matched but the payment should not auto-post yet.- Use
advancedmd_find_chargeand copy the matchedpatient_id,charge_id,carrier_id, provider code, and balance/portion fields. - Call
advancedmd_post_paymentwithpayment_status: "Hold"orpayment_status: "H". - The payload sends AdvancedMD status
H. - Start with
dry_run=true; then post withdry_run=falseif the held payment should be created.
Charge Found With Explicit Batch
Use this when the payment is ready for batch/auto-post status.- Use
advancedmd_find_chargeand pass the matched EHR fields intoadvancedmd_post_payment. - Set
payment_status: "Batch"orpayment_status: "B". - The payload sends AdvancedMD status
B. - Dry-run first, then post with
dry_run=false.
Auto-Derived Status Flow
Use this when the workflow wants to follow the source script’s CARC safety logic.- Omit
payment_status. - Provide
carc_codesas a JSON string, for example{"PR1":"20.00","PR3":"10.00"}. - The tool derives
Bonly if at least one CARC reason is parsed and every non-writeoff CARC code is one ofPR1,PR2, orPR3. - The tool derives
Hif no CARC reason is parsed, or if any non-writeoff CARC code is not auto-post-safe.
Charge Not Found / Unapplied Payment Flow
Use this whenadvancedmd_find_charge cannot find a matching charge but the payment still needs to be recorded.
- Call
advancedmd_post_paymentwithpatient_id,payment_amount,deposit_date, carrier/check details if available, and nocharge_id. - The tool builds an
unapplied_paymentpayload. - Provide
provider_codeorehr_provider_codeif known; otherwise the payload uses114. - Dry-run first, then post with
dry_run=falseif the unapplied payment should be created.
Missing AdvancedMD Connector Flow
Use this when the tool reportsadvancedmd_not_connected.
- Do not retry with the same inputs immediately.
- Ask the user to either connect AdvancedMD and try again, or stop the task.
- After AdvancedMD is connected, call the tool again.
Minimal Dry-Run Example
Matched Charge Dry-Run Example
Post After Review Example
After reviewing a dry-run payload, repeat the same call with:Error Handling
The tool returns a structured tool error for invalid inputs, including missing required fields, invalid dates, invalid decimal amounts, or unsupportedpayment_status values.
If AdvancedMD returns an XML fault while posting, the client parses the fault and surfaces it as a tool error instead of returning opaque raw XML.
Output
The structured output includes:posted:trueonly whendry_run=falseand AdvancedMD returns no fault.dry_run: whether the call was a dry run.payload_type:charge_paymentorunapplied_payment.payment_status: the normalizedBorHpayload value, either provided directly/through an alias or derived from CARC codes.writeoff_amount: formatted writeoff amount.payload: the AdvancedMD payload sent, or the payload that would be sent.amd_response: parsed status and raw XML when posted.