Skip to main content

Recording customer payments

A customer payment is an incoming remittance from a shipper/customer applied against their receivable invoices. It has two levels:
  • The payment (pym_…) — the money that arrived: an amount, a currency, a transaction date, and the check/ACH reference it came in on.
  • Its line items (pli_…) — how that money is split across your invoices. Each line item applies an amount to a specific invoice (inv_…) for a specific customer (cus_…).
This page covers creating a payment with its line items, reading it back, and reconciling it against receivables.
This is the accounts-receivable (money in) side. For paying carriers (money out) the resource is bill-payments, which is a separate endpoint family. For the AR concepts behind these records — reconciliation, short-pays, and the like — see Remittances Management and Accounts Receivable Management.
All amounts are integer cents. totalAmount: 1120000 is $11,200.00, not eleven million dollars. This matches every monetary field in the Upwell API.

Create a payment with its line items

The primary flow is a single POST /api/rest/customer_payments that inserts the payment and its line items together, using a nested paymentLineItems.data array:
curl -X POST https://api.upwell.com/api/rest/customer_payments \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
        "input": {
          "totalAmount": 1120000,
          "transactionDate": "2026-07-07",
          "currency": "USD",
          "checkOrAchNumber": "CHK004321",
          "checkDate": "2026-07-07",
          "sourceSystem": "aljex",
          "sourceSystemId": "REMIT-88123",
          "paymentLineItems": {
            "data": [
              {
                "invoiceId": "inv_jkMtQfYE",
                "customerId": "cus_kCRVpEUv",
                "totalAmount": 1120000,
                "status": "applied",
                "transactionDate": "2026-07-07"
              }
            ]
          }
        }
      }'
# -> { "createCustomerPayment": {
#      "id": "pym_LQ-3Nnmq", "totalAmount": 1120000, "currency": "USD",
#      "checkOrAchNumber": "CHK004321", "transactionDate": "...", "createdAt": "...", "updatedAt": "...",
#      "paymentLineItems": [
#        { "id": "pli_qi9RW1Pd", "invoiceId": "inv_jkMtQfYE", "customerId": "cus_kCRVpEUv",
#          "totalAmount": 1120000, "status": "applied", "createdAt": "...", "updatedAt": "..." } ] } }
When line items are nested under the payment like this, omit paymentId on each line item — the parent payment supplies the link. Fields you don’t set come back null.

Required fields

LevelRequiredNotes
PaymenttotalAmount, transactionDatecurrency defaults to USD if omitted. id, timestamps, and tenant are set for you.
Line itemtotalAmount, transactionDate, statusAdd invoiceId (+ customerId) to actually apply the amount to a receivable.

Field reference

Payment (pym_…)
FieldMeaning
totalAmountAmount received, in cents.
currencyISO-4217 code (CurrencyCodeEnum, e.g. USD). Defaults to USD.
transactionDateDate the payment was transacted (YYYY-MM-DD).
checkOrAchNumber / checkDateThe check or ACH reference and its date.
statusOptional payment-level status — see below.
sourceSystem / sourceSystemIdYour system’s identifier for this remittance — see idempotency.
paymentLineItemsNested { "data": [ … ] } array of line items.
Line item (pli_…)
FieldMeaning
totalAmountAmount applied to the invoice, in cents.
feesFees withheld, in cents (defaults to 0).
invoiceIdThe receivable invoice (inv_…) this amount is applied to.
customerIdThe customer (cus_…) the payment is from.
statusFree-form line-item state — see below.
transactionDateDate the line item was transacted (YYYY-MM-DD).
The line-item amounts don’t have to sum to the payment’s totalAmount — a remittance can be partially applied and reconciled later. For the complete, always-current column list, see the customer_payments and customer_payment_line_items entries in the API Reference.

Two different status fields

These are not the same field, and the difference matters:
  • Payment status is optional and drawn from PaymentStatusesEnum: PENDING, SUCCEEDED, FAILED, DISPUTED, REFUNDED, PAYMENT_REVERSED. Most payments leave it unset.
  • Line-item status is a required free-form string, not a fixed enum. In practice it carries reconciliation states like applied, paid, or completed. Don’t assume a closed set of values or a fixed casing — read it as an opaque string and set whatever your workflow uses.

Read payments back

# List (newest-first not guaranteed — use search for ordering)
curl "https://api.upwell.com/api/rest/customer_payments?limit=50" \
  -H "Authorization: YOUR_API_KEY"
# -> { "customerPayments": [ { "id": "pym_…", … } ] }

# One payment
curl https://api.upwell.com/api/rest/customer_payments/pym_LQ-3Nnmq \
  -H "Authorization: YOUR_API_KEY"
# -> { "customerPayment": { "id": "pym_LQ-3Nnmq", … } }
To filter or sort, use POST /api/rest/customer_payments/search with a Hasura-style where:
curl -X POST https://api.upwell.com/api/rest/customer_payments/search \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "where": { "sourceSystemId": { "_eq": "REMIT-88123" } },
        "orderBy": { "createdAt": "DESC" }, "limit": 10, "offset": 0 }'
# -> { "customerPayments": [ … ] }

Create many at once

POST /api/rest/bulk-customer-payments inserts an array of payments in one call. The response is a Hasura mutation envelope — read the created rows from returning:
curl -X POST https://api.upwell.com/api/rest/bulk-customer-payments \
  -H "Authorization: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "inputs": [ { "totalAmount": 3300, "transactionDate": "2026-07-08" },
                    { "totalAmount": 23880, "transactionDate": "2026-07-08" } ] }'
# -> { "createCustomerPayments": { "returning": [ { "id": "pym_…", … } ] } }

Update and delete

# Update — body carries an input object; id comes from the path
curl -X PUT https://api.upwell.com/api/rest/customer_payments/pym_LQ-3Nnmq \
  -H "Authorization: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "input": { "checkOrAchNumber": "1234567890" } }'
# -> { "updateCustomerPayment": { "id": "pym_LQ-3Nnmq", "checkOrAchNumber": "1234567890", … } }

# Delete
curl -X DELETE https://api.upwell.com/api/rest/customer_payments/pym_LQ-3Nnmq \
  -H "Authorization: YOUR_API_KEY"
# -> { "deleteCustomerPayment": { "id": "pym_LQ-3Nnmq" } }

Line items on their own

If you’d rather attach line items to an existing payment (or manage them independently), use the line-item endpoints directly — GET/POST /api/rest/customer_payment_line_items and .../customer_payment_line_items/{id}. Here, set paymentId explicitly to link the line item to its payment:
curl -X POST https://api.upwell.com/api/rest/customer_payment_line_items \
  -H "Authorization: YOUR_API_KEY" -H "Content-Type: application/json" \
  -d '{ "input": { "paymentId": "pym_LQ-3Nnmq", "invoiceId": "inv_jkMtQfYE",
                   "customerId": "cus_kCRVpEUv", "totalAmount": 1120000,
                   "status": "applied", "transactionDate": "2026-07-07" } }'
# -> { "createCustomerPaymentLineItem": { "id": "pli_qi9RW1Pd", … } }

Idempotency and deduplication

If you set sourceSystem and sourceSystemId, the pair is unique per tenant (there’s a uniqueness constraint on (tenant_id, source_system, source_system_id) for both payments and line items). Use your own remittance/transaction reference as sourceSystemId so re-posting the same remittance conflicts instead of creating a duplicate — a simple idempotency key for retried imports.
Testing against staging? Use the staging base URL https://staging.api.upwell.com with your staging API key — the paths are identical.
For the complete, always-current request and response schemas, see the customer_payments and customer_payment_line_items entries in the API Reference. To submit carrier invoices (the payables side) instead, see Submitting carrier invoices via API.