Payments quickstart

Read a patient's charges and post a payment to the ledger.

In this guide you create a payment through the Synchronizer API. The payment pays off a sample patient balance in a working health record system, Open Dental.

By the end, a test patient's balance is paid off in Open Dental through your code.

⚠️

Before you start

You need a sandbox location connected to a health record system. If you do not have one, follow Setting up an Open Dental sandbox sync.

1. Get ready

Set up a developer account

If you do not have one, follow the Quickstart and save your API key.

Find your subdomain and location ID

  1. Log in to the Developer Portal, click Institutions, then your demo practice.
  2. The subdomain is under the institution's name. Click it to copy it.
  3. The Open Dental location's ID is on the right. Click it to copy it.

Pick a language

Every step has the same call in cURL, Node and Python.

  • cURL. Install curl and jq, which pulls values out of each response. Run each step in your terminal.
  • Node. Node 18 or later. Add each step to payments.mjs and run it after the last step.
  • Python. Install requests. Add each step to payments.py and run it after the last step.

Save your API key, subdomain and location ID in your terminal. All three languages read them from there.

export NEX_API_KEY="your-sandbox-api-key"
export NEX_SUBDOMAIN="your-subdomain"
export NEX_LOCATION_ID="your-location-id"

Get a token

Trade your API key for a bearer token. Every other call uses it.

export NEX_TOKEN=$(curl -s --request POST \
  --url "https://nexhealth.info/authenticates" \
  --header "Nex-Api-Version: v3.0.0" \
  --header "Authorization: $NEX_API_KEY" | jq -r '.data.token')

echo $NEX_TOKEN
// Save as payments.mjs and run: node payments.mjs
// Needs Node 18 or later.
const BASE = "https://nexhealth.info";
const scope = { subdomain: process.env.NEX_SUBDOMAIN, location_id: process.env.NEX_LOCATION_ID };

async function call(method, path, opts = {}) {
  const url = new URL(BASE + path);
  for (const [key, value] of Object.entries(opts.query || {})) {
    url.searchParams.set(key, value);
  }
  const res = await fetch(url, {
    method,
    headers: {
      "Nex-Api-Version": "v3.0.0",
      "Content-Type": "application/json",
      Authorization: opts.token ? "Bearer " + opts.token : process.env.NEX_API_KEY,
    },
    body: opts.body ? JSON.stringify(opts.body) : undefined,
  });
  const text = await res.text();
  const json = text ? JSON.parse(text) : {};
  if (!res.ok || json.code === false) {
    throw new Error(method + " " + path + " failed: " + res.status + " " + JSON.stringify(json.error));
  }
  return json.data;
}

// 1. Get a token
const { token } = await call("POST", "/authenticates");
# Save as payments.py and run: python payments.py
# Needs the requests package: pip install requests
import os

import requests

BASE = "https://nexhealth.info"
scope = {"subdomain": os.environ["NEX_SUBDOMAIN"], "location_id": os.environ["NEX_LOCATION_ID"]}


def call(method, path, token=None, params=None, body=None):
    res = requests.request(
        method,
        BASE + path,
        params=params,
        json=body,
        headers={
            "Nex-Api-Version": "v3.0.0",
            "Authorization": "Bearer " + token if token else os.environ["NEX_API_KEY"],
        },
    )
    payload = res.json() if res.content else {}
    if not res.ok or payload.get("code") is False:
        raise RuntimeError(f"{method} {path} failed: {res.status_code} {payload.get('error')}")
    return payload.get("data")


# 1. Get a token
token = call("POST", "/authenticates")["token"]

2. Gather the charges

The Open Dental demo comes with test patients who have balances. A charge is a line item on the ledger that raises the patient's balance; each one belongs to a completed procedure.

The three charges on John Smith's ledger in Open Dental

The image shows the charges for the test patient John Smith, including $70 for a prophylaxis (routine cleaning). In this step you open John's ledger in Open Dental, look him up through the API, then list his charges.

Find the patient in Open Dental

  1. On the appointments page in Open Dental, click Select Patient.
  2. Click John Smith, then OK.
  3. Click the Account tab to see his ledger.

Find the patient through the API

Search GET /patients by name to get John's NexHealth patient ID.

export NEX_PATIENT_ID=$(curl -s --request GET \
  --url "https://nexhealth.info/patients?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&name=John%20Smith" \
  --header "Nex-Api-Version: v3.0.0" \
  --header "Authorization: Bearer $NEX_TOKEN" | jq -r '.data[0].id')

echo $NEX_PATIENT_ID
// 2. Find the patient
const [patient] = await call("GET", "/patients", { token, query: { ...scope, name: "John Smith" } });
console.log("Patient ID:", patient.id);
# 2. Find the patient
patient = call("GET", "/patients", token, {**scope, "name": "John Smith"})[0]
print("Patient ID:", patient["id"])

List the patient's charges

Send John's patient ID to GET /charges and sort by updated_at. These are the line items you saw on the ledger.

curl -s --request GET \
  --url "https://nexhealth.info/charges?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&patient_id=$NEX_PATIENT_ID&sort=updated_at&per_page=100" \
  --header "Nex-Api-Version: v3.0.0" \
  --header "Authorization: Bearer $NEX_TOKEN" > charges.json

jq '.data[] | {id, description, fee}' charges.json
// 3. List the patient's charges
const charges = await call("GET", "/charges", {
  token,
  query: { ...scope, patient_id: patient.id, sort: "updated_at", per_page: 100 },
});
for (const charge of charges) {
  console.log(charge.id, charge.description, charge.fee.amount);
}
# 3. List the patient's charges
charges = call("GET", "/charges", token, {**scope, "patient_id": patient["id"], "sort": "updated_at", "per_page": 100})
for charge in charges:
    print(charge["id"], charge["description"], charge["fee"]["amount"])

Each charge has an id and a fee. The fee amount is a string in dollars, such as "70.00". You need the ID and the amount of each charge to post the payment.

3. Post the payment

You have the patient's ID and the charges. Two more values, then the payment.

Get a payment type ID

GET /payment_types lists the payment types set up in the health record system. Find the ID of the "Credit Card" payment type.

export NEX_PAYMENT_TYPE_ID=$(curl -s --request GET \
  --url "https://nexhealth.info/payment_types?subdomain=$NEX_SUBDOMAIN&location_id=$NEX_LOCATION_ID&per_page=100" \
  --header "Nex-Api-Version: v3.0.0" \
  --header "Authorization: Bearer $NEX_TOKEN" | jq -r '.data[] | select(.name == "Credit Card") | .id')

echo $NEX_PAYMENT_TYPE_ID
// 4. Get the payment type
const paymentTypes = await call("GET", "/payment_types", { token, query: { ...scope, per_page: 100 } });
const creditCard = paymentTypes.find((type) => type.name === "Credit Card");
console.log("Payment type ID:", creditCard.id);
# 4. Get the payment type
payment_types = call("GET", "/payment_types", token, {**scope, "per_page": 100})
credit_card = next(item for item in payment_types if item["name"] == "Credit Card")
print("Payment type ID:", credit_card["id"])

Choose a transaction ID

Unlike most IDs in the API, the transaction ID is a string you choose. The samples use "test-transaction-id".

📘

Transaction IDs must be unique: use a new one for every payment. We recommend the name of your organization in the Developer Portal followed by a running number, for example "NexDev:0001" and "NexDev:0002". NexHealth uses the ID to identify the payment it writes into the health record system, and GET /payments can filter by it.

Post the payment through the Synchronizer API

Send a POST request to /payments with these fields.

FieldValue
patient_idJohn's patient ID.
amountThe total of the charges. It is negative because the payment lowers the patient's balance.
payment_type_idThe ID of the "Credit Card" payment type.
transaction_idThe string you chose.
charge_splitsWhich charges the payment covers and how much goes to each one. Each key is a charge ID. Each value is the amount of that charge. The amounts are negative.

This payment pays off the balance for all three of the patient's procedures at once.

jq '[.data[] | {key: (.id | tostring), value: (-(.fee.amount | tonumber))}] | from_entries' charges.json > charge-splits.json
export NEX_AMOUNT=$(jq '[.[]] | add | . * 100 | round / 100' charge-splits.json)

cat > payment.json <<EOF
{
  "payment": {
    "patient_id": $NEX_PATIENT_ID,
    "amount": $NEX_AMOUNT,
    "payment_type_id": $NEX_PAYMENT_TYPE_ID,
    "transaction_id": "test-transaction-id",
    "charge_splits": $(cat charge-splits.json)
  }
}
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"
// 5. Post the payment
const cents = charges.map((charge) => Math.round(Number(charge.fee.amount) * 100));
const chargeSplits = Object.fromEntries(charges.map((charge, i) => [charge.id, -cents[i] / 100]));
const amount = -cents.reduce((sum, value) => sum + value, 0) / 100;

await call("POST", "/payments", {
  token,
  query: scope,
  body: {
    payment: {
      patient_id: patient.id,
      amount,
      payment_type_id: creditCard.id,
      transaction_id: "test-transaction-id",
      charge_splits: chargeSplits,
    },
  },
});
console.log("Payment sent:", amount);
# 5. Post the payment
cents = [round(float(charge["fee"]["amount"]) * 100) for charge in charges]
charge_splits = {str(charge["id"]): -value / 100 for charge, value in zip(charges, cents)}
amount = -sum(cents) / 100

call(
    "POST",
    "/payments",
    token,
    scope,
    {
        "payment": {
            "patient_id": patient["id"],
            "amount": amount,
            "payment_type_id": credit_card["id"],
            "transaction_id": "test-transaction-id",
            "charge_splits": charge_splits,
        }
    },
)
print("Payment sent:", amount)

The API answers with 202. NexHealth then writes the payment into the health record system.

If you run the sample again, change the transaction ID first.

Check that the payment is on the Open Dental ledger

Go back to John's ledger in Open Dental. Open another page and come back to refresh it. The payment is there.

John Smith's ledger in Open Dental with the payment posted

What differs by health record system

Payments and adjustments are written to Dentrix, Dentrix Enterprise, Eaglesoft and Open Dental, and only where the practice has ledger sync turned on. On other systems the API still returns 202 and the payment never reaches the ledger. Subscribe to payment_insertion to know when a payment landed or failed. See Testing writes, per system.

Move to production

When you are ready to make production calls to the ledger and financial endpoints, email [email protected]. Our team has to enable your production API key for ledger and financial data.


What’s Next

Get told when a write lands or fails.

Did this page help you?
ChangelogMCP serverErrorsdevelopers@nexhealth.com