Payments configuration guide
Read balances and ledger lines, post payments and adjustments, and confirm each write.
This guide covers everything the ledger endpoints do. If you have not posted a payment yet, start with the Payments quickstart.
The samples use the values from the Quickstart: NEX_TOKEN, NEX_SUBDOMAIN, NEX_LOCATION_ID and NEX_PATIENT_ID. They also use NEX_GUARANTOR_ID (the guarantor_id on the patient record), NEX_CHARGE_ID (an id from GET /charges), NEX_PAYMENT_TYPE_ID (an id from GET /payment_types), NEX_ADJUSTMENT_TYPE_ID (an id from GET /adjustment_types) and NEX_WEBHOOK_ID (from Setting up webhooks). Node samples need Node 18 or later, saved as .mjs files. Python samples need the requests package.
1. Turn on financial data
NexHealth reads financial data from the health record system on request, not by default.
- Sandbox locations have it turned on.
- For production, email [email protected] and ask for financial endpoints on your production key. The ledger backfill takes up to a day.
- Until then,
/charges,/payments,/adjustments,/claims,/fee_schedules,/insurance_plansand the balance endpoints answer403withYou don't have access to perform this action.
2. How the ledger adds up
A patient's balance is the sum of three kinds of record.
| Record | What it does to the balance | Sign of the amount |
|---|---|---|
| Charge | Raises it. One charge per completed procedure, with the procedure code and fee. | Positive. |
| Payment | Lowers it. Entered when a patient or an insurance plan pays. | Negative. A refund is positive. |
| Adjustment | Either. Practices use adjustments for discounts, membership fees and insurance write-offs. | A credit is negative. A debit is positive. |
Three more things to know before you build.
- Balances belong to the guarantor. The guarantor is the person responsible for paying. A patient's
guarantor_idpoints at them, and a family's balance sits on one guarantor. - Insurance payments have an
insurance_plan_id. On a patient payment it isnull. - Amounts come back as strings and go in as numbers. A response says
"fee": { "amount": "70.00", "currency": "USD" }. A request says"amount": -70.
3. Read a balance
GET /guarantor_balances returns the balance per guarantor, split into the insurance estimate, the guarantor's portion and four aging buckets: under 30 days, 31 to 60, 61 to 90 and over 90. Send guarantor_id or updated_since. One of the two is required. Zero balances are left out unless you send show_zero_balances=true.
curl -s --request GET \
--url "https://nexhealth.info/guarantor_balances?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&guarantor_id=$NEX_GUARANTOR_ID&show_zero_balances=true" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN"const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_LOCATION_ID, NEX_GUARANTOR_ID } = process.env;
const url = `https://nexhealth.info/guarantor_balances?subdomain=${NEX_SUBDOMAIN}&location_id=${NEX_LOCATION_ID}&guarantor_id=${NEX_GUARANTOR_ID}&show_zero_balances=true`;
const res = await fetch(url, {
headers: {
"Nex-Api-Version": "v3.0.0",
Authorization: `Bearer ${NEX_TOKEN}`,
},
});
console.log(await res.json());import os
import requests
res = requests.get(
"https://nexhealth.info/guarantor_balances",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
"location_id": os.environ["NEX_LOCATION_ID"],
"guarantor_id": os.environ["NEX_GUARANTOR_ID"],
"show_zero_balances": "true",
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
)
print(res.json())4. Read the ledger lines
GET /charges needs at least one filter: patient_id, provider_id, procedure_id, guarantor_id or updated_since. Two parameters are worth knowing.
include[]=claim_payoutsadds the insurance side of each charge: the amount billed to insurance, the estimated and actual insurance payment, the write-off and the allowed amount.include_deleted=truereturns charges the office deleted, withdeleted_atset. Without it they disappear from the list.
curl -s --globoff --request GET \
--url "https://nexhealth.info/charges?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&patient_id=$NEX_PATIENT_ID&include[]=claim_payouts&sort=-updated_at&per_page=100" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN"const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_LOCATION_ID, NEX_PATIENT_ID } = process.env;
const url = `https://nexhealth.info/charges?subdomain=${NEX_SUBDOMAIN}&location_id=${NEX_LOCATION_ID}&patient_id=${NEX_PATIENT_ID}&include[]=claim_payouts&sort=-updated_at&per_page=100`;
const res = await fetch(url, {
headers: {
"Nex-Api-Version": "v3.0.0",
Authorization: `Bearer ${NEX_TOKEN}`,
},
});
console.log(await res.json());import os
import requests
res = requests.get(
"https://nexhealth.info/charges",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
"location_id": os.environ["NEX_LOCATION_ID"],
"patient_id": os.environ["NEX_PATIENT_ID"],
"include[]": "claim_payouts",
"sort": "-updated_at",
"per_page": 100,
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
)
print(res.json())GET /payments and GET /adjustments work the same way. Payments also filter by charge_id, claim_id and transaction_id.
5. Post a payment
POST /payments takes one payment object.
| Field | What to send |
|---|---|
patient_id | Required. The NexHealth patient ID. The patient has to exist in the health record system already. |
amount | Required. Dollars, as a number. Negative for a payment. Positive for a refund. |
transaction_id | Required. A string you choose, up to 50 characters. It has to be unique: the health record system rejects a second payment with the same ID. Use your organization name and a running number, such as NexDev:0001. It is for tracking only and is not visible to the office. |
payment_type_id | An id from GET /payment_types for the same location_id. |
paid_at | When the payment was made, as a datetime. |
charge_splits | Which charges the payment covers. Each key is a charge ID and each value is the amount applied to it, negative. |
provider_splits | The same, by provider ID. Use it when the health record system cannot link payments to charges. |
notes | Saved into the health record system. Some systems cut long notes short. |
cat > payment.json <<EOF
{
"payment": {
"patient_id": $NEX_PATIENT_ID,
"amount": -70,
"payment_type_id": $NEX_PAYMENT_TYPE_ID,
"transaction_id": "NexDev:0001",
"paid_at": "2026-12-01T15:30:00+00:00",
"charge_splits": { "$NEX_CHARGE_ID": -70 },
"notes": "Paid online"
}
}
EOF
curl -s --request POST \
--url "https://nexhealth.info/payments?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" \
--header "Content-Type: application/json" \
--data @payment.json \
--write-out "\nHTTP %{http_code}\n"const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_LOCATION_ID, NEX_PATIENT_ID, NEX_CHARGE_ID, NEX_PAYMENT_TYPE_ID } = process.env;
const url = `https://nexhealth.info/payments?subdomain=${NEX_SUBDOMAIN}&location_id=${NEX_LOCATION_ID}`;
const res = await fetch(url, {
method: "POST",
headers: {
"Nex-Api-Version": "v3.0.0",
Authorization: `Bearer ${NEX_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
payment: {
patient_id: Number(NEX_PATIENT_ID),
amount: -70,
payment_type_id: Number(NEX_PAYMENT_TYPE_ID),
transaction_id: "NexDev:0001",
paid_at: "2026-12-01T15:30:00+00:00",
charge_splits: {
[NEX_CHARGE_ID]: -70,
},
notes: "Paid online",
},
}),
});
console.log(res.status, await res.text());import os
import requests
res = requests.post(
"https://nexhealth.info/payments",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
"location_id": os.environ["NEX_LOCATION_ID"],
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
json={
"payment": {
"patient_id": int(os.environ["NEX_PATIENT_ID"]),
"amount": -70,
"payment_type_id": int(os.environ["NEX_PAYMENT_TYPE_ID"]),
"transaction_id": "NexDev:0001",
"paid_at": "2026-12-01T15:30:00+00:00",
"charge_splits": {
os.environ["NEX_CHARGE_ID"]: -70,
},
"notes": "Paid online",
},
},
)
print(res.status_code, res.text)- The API answers
202. The write into the health record system happens next. Step 7 shows how to confirm it. - A payment split across providers can become several payment records in the health record system. They share your
transaction_id. - A refund is a positive payment. Dentrix, Dentrix Enterprise and Eaglesoft record refunds as adjustments, so NexHealth converts the refund into an adjustment and the response says so. Open Dental records refunds as positive payments.
6. Post an adjustment
Get the adjustment type first. GET /adjustment_types lists the types set up at the location.
curl -s --request GET \
--url "https://nexhealth.info/adjustment_types?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&per_page=100" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN"const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_LOCATION_ID } = process.env;
const url = `https://nexhealth.info/adjustment_types?subdomain=${NEX_SUBDOMAIN}&location_id=${NEX_LOCATION_ID}&per_page=100`;
const res = await fetch(url, {
headers: {
"Nex-Api-Version": "v3.0.0",
Authorization: `Bearer ${NEX_TOKEN}`,
},
});
console.log(await res.json());import os
import requests
res = requests.get(
"https://nexhealth.info/adjustment_types",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
"location_id": os.environ["NEX_LOCATION_ID"],
"per_page": 100,
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
)
print(res.json())Then send POST /adjustments. The fields match a payment, with adjustment_type_id in place of payment_type_id. A credit, such as a discount, is negative. A debit, such as a membership fee, is positive and has to carry charge_splits or provider_splits. A debit without either fails.
cat > adjustment.json <<EOF
{
"adjustment": {
"patient_id": $NEX_PATIENT_ID,
"amount": -25,
"adjustment_type_id": $NEX_ADJUSTMENT_TYPE_ID,
"transaction_id": "NexDev:0002",
"charge_splits": { "$NEX_CHARGE_ID": -25 },
"notes": "Membership discount"
}
}
EOF
curl -s --request POST \
--url "https://nexhealth.info/adjustments?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" \
--header "Content-Type: application/json" \
--data @adjustment.json \
--write-out "\nHTTP %{http_code}\n"const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_LOCATION_ID, NEX_PATIENT_ID, NEX_CHARGE_ID, NEX_ADJUSTMENT_TYPE_ID } = process.env;
const url = `https://nexhealth.info/adjustments?subdomain=${NEX_SUBDOMAIN}&location_id=${NEX_LOCATION_ID}`;
const res = await fetch(url, {
method: "POST",
headers: {
"Nex-Api-Version": "v3.0.0",
Authorization: `Bearer ${NEX_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
adjustment: {
patient_id: Number(NEX_PATIENT_ID),
amount: -25,
adjustment_type_id: Number(NEX_ADJUSTMENT_TYPE_ID),
transaction_id: "NexDev:0002",
charge_splits: {
[NEX_CHARGE_ID]: -25,
},
notes: "Membership discount",
},
}),
});
console.log(res.status, await res.text());import os
import requests
res = requests.post(
"https://nexhealth.info/adjustments",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
"location_id": os.environ["NEX_LOCATION_ID"],
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
json={
"adjustment": {
"patient_id": int(os.environ["NEX_PATIENT_ID"]),
"amount": -25,
"adjustment_type_id": int(os.environ["NEX_ADJUSTMENT_TYPE_ID"]),
"transaction_id": "NexDev:0002",
"charge_splits": {
os.environ["NEX_CHARGE_ID"]: -25,
},
"notes": "Membership discount",
},
},
)
print(res.status_code, res.text)7. Confirm the write
A 202 means NexHealth accepted the request. It does not mean the payment is on the ledger yet.
- Subscribe to
payment_insertionandadjustment_insertion. Each fires once the write into the health record system completes or fails, and the payload carries the payment you sent:patient_id,value,paid_at,transaction_id,charge_splits,provider_splitsandtype_name. payment_createdfires later, when NexHealth reads the new payment back from the health record system.
curl -s --request POST \
--url "https://nexhealth.info/webhook_endpoints/$NEX_WEBHOOK_ID/webhook_subscriptions?subdomain=$NEX_SUBDOMAIN" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN" \
--header "Content-Type: application/json" \
--data '{ "resource_type": "Payment", "event": "payment_insertion" }'const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_WEBHOOK_ID } = process.env;
const url = `https://nexhealth.info/webhook_endpoints/${NEX_WEBHOOK_ID}/webhook_subscriptions?subdomain=${NEX_SUBDOMAIN}`;
const res = await fetch(url, {
method: "POST",
headers: {
"Nex-Api-Version": "v3.0.0",
Authorization: `Bearer ${NEX_TOKEN}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
resource_type: "Payment",
event: "payment_insertion",
}),
});
console.log(await res.json());import os
import requests
res = requests.post(
"https://nexhealth.info/webhook_endpoints/" + os.environ["NEX_WEBHOOK_ID"] + "/webhook_subscriptions",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
json={
"resource_type": "Payment",
"event": "payment_insertion",
},
)
print(res.json())To check a payment later, ask for it by transaction_id.
curl -s --request GET \
--url "https://nexhealth.info/payments?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&transaction_id=NexDev:0001" \
--header "Nex-Api-Version: v3.0.0" \
--header "Authorization: Bearer $NEX_TOKEN"const { NEX_TOKEN, NEX_SUBDOMAIN, NEX_LOCATION_ID } = process.env;
const url = `https://nexhealth.info/payments?subdomain=${NEX_SUBDOMAIN}&location_id=${NEX_LOCATION_ID}&transaction_id=NexDev:0001`;
const res = await fetch(url, {
headers: {
"Nex-Api-Version": "v3.0.0",
Authorization: `Bearer ${NEX_TOKEN}`,
},
});
console.log(await res.json());import os
import requests
res = requests.get(
"https://nexhealth.info/payments",
params={
"subdomain": os.environ["NEX_SUBDOMAIN"],
"location_id": os.environ["NEX_LOCATION_ID"],
"transaction_id": "NexDev:0001",
},
headers={
"Nex-Api-Version": "v3.0.0",
"Authorization": "Bearer " + os.environ["NEX_TOKEN"],
},
)
print(res.json())Writes wait while write_status on GET /sync_status is red. See How a write reaches the health record system.
8. Differences by health record system
| System | Payments and charges | Refunds |
|---|---|---|
| Dentrix | Payments cannot be linked to charges. charge_id is always null. Use provider_splits. | Recorded as positive adjustments. |
| Dentrix Enterprise | Payments can be linked to charges. | Recorded as positive adjustments. |
| Eaglesoft | Payments can be linked to charges. | Recorded as positive adjustments. |
| Open Dental | Payments can be linked to charges. | Recorded as positive payments. |
When something goes wrong
See Troubleshooting payments. Every message the ledger endpoints return is in the Errors reference.
Updated about 1 hour ago