Skip to main content
This document covers 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_code
  • username
  • password
The tool uses the same audited AdvancedMD client as the other AdvancedMD tools. It does not accept raw AdvancedMD tokens or direct 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.
The duplicate check requires a check number; without one it is skipped. A charge not found in the scanned pages fails open (the post proceeds — AdvancedMD still rejects voided charges server-side), as does a transient read error, which is logged at warn level.

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 0paymentthatwouldrecordnothing:achargepaymentwithnoCARCcodes,orany0 payment that would record nothing: a charge payment with no CARC codes, or **any** 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:
The guidance returned to the agent is to re-read the charge with 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 no charge_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 as MM/DD/YYYY or YYYY-MM-DD.
Optional:
  • payment_status: AdvancedMD payment status. B and Batch mean the same thing: post the payment in batch/auto-post status. H and Hold mean the same thing: put the payment on hold. Matching is case-insensitive, so b, batch, h, and hold also work. If omitted, the tool derives the status from CARC codes using the source script logic: start with all_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), set all_codes_safe = false; final status is B only when all_codes_safe is true and at least one CARC reason was parsed, otherwise H.
  • 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_codes or carc_codes_raw: CARC code amounts. Prefer a JSON object string such as {"CO45":"145.26","PR3":"50.00"}. Loose CODE AMOUNT text also works for simple cases, such as PR1 5.50.
  • carrier_id or carrier: the payment’s payor. An AdvancedMD carrier id (car7552), a bare carrier number, or the display code advancedmd_find_charge returns (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 to 2 (check); use ach for payer ACH remittances. See Payor and payment method.
  • check_number: Check or trace number.
  • provider_code or ehr_provider_code: AdvancedMD provider/profile code. Defaults to 114 for unapplied payments.
  • ehr_insurance_portion, ehr_patient_portion, ehr_insurance_balance, ehr_patient_balance: Values returned by advancedmd_find_charge.
Amount fields are validated as decimal numbers and normalized to two decimals in the payload. For example, 80 is sent as 80.00.

Payload Modes

When charge_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.
  • @group is 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, and PR45. Other CARC codes are still included in the payload with @code, @group, and @amount, but their @reasonid is empty.
  • Writeoff amount is summed for configured writeoff CARC codes.
  • If payment_status is omitted, the payment status is derived from CARC codes using the source script logic: B only when at least one CARC reason was parsed and every non-writeoff CARC code is auto-post-safe (PR1, PR2, PR3); otherwise H. Writeoff codes (CO253, CR253, CR45, PR45, CO45) contribute to writeoff amount but do not make the payment unsafe.
  • If payment_status is provided, human-readable words and AdvancedMD codes both work:
    • B, b, Batch, or batch all mean Batch and are sent to AdvancedMD as B.
    • H, h, Hold, or hold all mean Hold and are sent to AdvancedMD as H.
    • Example: payment_status: "Batch" is the same as payment_status: "B".
    • Example: payment_status: "Hold" is the same as payment_status: "H".

Standard Matched-Charge Flow

  1. Use advancedmd_find_charge to locate the charge and obtain EHR fields such as charge_id, patient_id, balances, portions, carrier, and provider code.
  2. Call advancedmd_post_payment with those values, EOB payment fields, CARC codes, and dry_run=true.
  3. Review payload_type, payment_status, writeoff_amount, and the generated payload.
  4. If the payload is correct, call advancedmd_post_payment again with the same inputs and dry_run=false.

Dry-Run Review Flow

Use this when the agent or user wants to inspect the exact AdvancedMD payload before any mutation.
  1. Call advancedmd_post_payment without passing dry_run, or pass dry_run=true.
  2. Confirm that posted is false.
  3. Review the payload and markdown summary.
  4. Only make a second call with dry_run=false when posting should actually happen.

Charge Found With Explicit Hold

Use this when the charge was matched but the payment should not auto-post yet.
  1. Use advancedmd_find_charge and copy the matched patient_id, charge_id, carrier_id, provider code, and balance/portion fields.
  2. Call advancedmd_post_payment with payment_status: "Hold" or payment_status: "H".
  3. The payload sends AdvancedMD status H.
  4. Start with dry_run=true; then post with dry_run=false if the held payment should be created.

Charge Found With Explicit Batch

Use this when the payment is ready for batch/auto-post status.
  1. Use advancedmd_find_charge and pass the matched EHR fields into advancedmd_post_payment.
  2. Set payment_status: "Batch" or payment_status: "B".
  3. The payload sends AdvancedMD status B.
  4. 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.
  1. Omit payment_status.
  2. Provide carc_codes as a JSON string, for example {"PR1":"20.00","PR3":"10.00"}.
  3. The tool derives B only if at least one CARC reason is parsed and every non-writeoff CARC code is one of PR1, PR2, or PR3.
  4. The tool derives H if 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 when advancedmd_find_charge cannot find a matching charge but the payment still needs to be recorded.
  1. Call advancedmd_post_payment with patient_id, payment_amount, deposit_date, carrier/check details if available, and no charge_id.
  2. The tool builds an unapplied_payment payload.
  3. Provide provider_code or ehr_provider_code if known; otherwise the payload uses 114.
  4. Dry-run first, then post with dry_run=false if the unapplied payment should be created.

Missing AdvancedMD Connector Flow

Use this when the tool reports advancedmd_not_connected.
  1. Do not retry with the same inputs immediately.
  2. Ask the user to either connect AdvancedMD and try again, or stop the task.
  3. 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 unsupported payment_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: true only when dry_run=false and AdvancedMD returns no fault.
  • dry_run: whether the call was a dry run.
  • payload_type: charge_payment or unapplied_payment.
  • payment_status: the normalized B or H payload 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.