> ## Documentation Index
> Fetch the complete documentation index at: https://agents.nanonets.com/docs/llms.txt
> Use this file to discover all available pages before exploring further.

# AdvancedMD Post Payment

> AdvancedMD payment posting tool exposed through the Agents Platform.

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](/docs/tools/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:

```json theme={null}
{
  "dry_run": false
}
```

## `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](/docs/tools/advancedmd-unapply-payment)) or escalated as a reversal.
* **`zero_amount_noop`** — a $0 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:

| AdvancedMD fault                                               | `reason`                              |
| -------------------------------------------------------------- | ------------------------------------- |
| `Insurance Balance cannot be negative` (`CD_Balances`)         | `insurance_balance_would_go_negative` |
| `Error In Balance For Charge ID <n>`                           | `charge_balance_error`                |
| FK insert failure (`FK_actv_PaymentDetails_actv_ChargeDetail`) | `charge_not_active`                   |

**`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:

```logql theme={null}
{service_name="nanonets-agents"} |= "Error In Balance For Charge"
```

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:

| shape                                                  | outcome                                                            |
| ------------------------------------------------------ | ------------------------------------------------------------------ |
| payment > 0, `allowed_amount` omitted                  | **refused** — pass the EOB's allowed amount                        |
| \$0 paid, write-off clears the balance                 | **posts** — full contractual write-off                             |
| \$0 paid, write-off leaves a residual balance          | **refused** — the remainder would be unaccounted for               |
| \$0 paid, write-off **exceeds** the balance            | **refused** — an over-stated reduction drives the balance negative |
| payment > 0 and a write-off covering the whole balance | **refused** — both cannot be right                                 |

## 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:

| name                      | `@paymethod` | AdvancedMD method |
| ------------------------- | ------------ | ----------------- |
| `check` / `chk`           | `2`          | Check (default)   |
| `ach` / `eft` / `era-ach` | `9`          | ERA-ACH           |
| `era-non`                 | `10`         | ERA-NON           |
| `era-chk`                 | `11`         | ERA-CHK           |
| `era-fwt`                 | `12`         | ERA-FWT           |
| `era-bop`                 | `13`         | ERA-BOP           |
| `era-other`               | `14`         | ERA-Other         |

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](#payor-carrierid-and-payment-method-paymethod).
* `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](#payor-carrierid-and-payment-method-paymethod).
* `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"`.

## Recommended Workflow

### 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

```json theme={null}
{
  "patient_id": "123",
  "payment_amount": "80.00",
  "deposit_date": "05/01/2026",
  "carc_codes": "{\"PR1\":\"20.00\"}"
}
```

### Matched Charge Dry-Run Example

```json theme={null}
{
  "dry_run": true,
  "patient_id": "123",
  "charge_id": "456",
  "payment_amount": "80.00",
  "allowed_amount": "100.00",
  "payment_status": "Hold",
  "deposit_date": "2026-05-01",
  "carrier_id": "789",
  "check_number": "CHK123",
  "ehr_insurance_portion": "70.00",
  "ehr_patient_portion": "30.00",
  "ehr_insurance_balance": "50.00",
  "ehr_patient_balance": "30.00",
  "carc_codes": "{\"CO45\":\"20.00\",\"PR3\":\"10.00\"}"
}
```

### Post After Review Example

After reviewing a dry-run payload, repeat the same call with:

```json theme={null}
{
  "dry_run": false
}
```

## 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.
