Payments
Esta página aún no está disponible en tu idioma.
A payment represents a settlement fact against an invoice. Positive collection facts have status succeeded; returns, reversals, and adjustments use recorded. A mistaken manual payment that has been voided has status voided: the money never arrived, and the void is not a refund. The invoice’s refunded_amount reports refunds.
Voided payments are omitted from list results but remain available by exact ID for audit. See Payment statuses and the voided payment example.
Imported-refund marker rows, BenchKey’s data importer writes a special internal marker row (
source_extras.imported_refund = true) to attach an imported refund to an invoice. These rows are not real receipts of money and are excluded from the Payments API: they never appear in list results, and retrieving one by ID returns404. The refund amount itself is visible on the parent invoice’srefunded_amountfield.
All money values are returned as integer cents alongside a formatted string and a currency code. Money inputs (amount_cents) must be strictly positive integers, fractional or negative values return 400.
Currency note, BenchKey is multi-location. Each payment’s currency follows its sale location, which may differ from the tenant-global default. The
currencyfield on the returned object reflects the correct per-location currency.
The payment object
Section titled “The payment object”{ "object": "payment", "id": "canonical:8812", "invoice_id": "5831", "ticket_id": "10042", "status": "succeeded", "entry_type": "charge", "amount": { "amount_cents": 14071, "amount": "140.71", "currency": "USD" }, "currency": "USD", "payment_method": "card", "processor": "square", "location_id": "1", "processor_reference": "sq_pay_abc123xyz", "paid_at": "2025-11-03T18:02:11.000Z", "created_at": "2025-11-03T18:02:11.000Z"}| Field | Type | Description |
|---|---|---|
id | string | Source-qualified payment ID, such as canonical:8812 or legacy:8812. Use the returned ID for subsequent requests. |
invoice_id | string|null | The invoice this payment is applied to |
ticket_id | string|null | The ticket associated with this payment. Resolved via the payment row’s own ticket_id when present, otherwise falls back to the parent invoice’s ticket_id (import and reconciliation rows sometimes omit a direct ticket link). |
status | string | succeeded, recorded, or voided; see Payment statuses |
voided_at | string | ISO-8601 timestamp when the payment was voided. Present only when status is voided; omitted otherwise. |
entry_type | string | Settlement fact type, for example charge, refund, or collection_reversal |
amount | Money | Signed amount of the settlement fact. A voided payment retains its original amount for audit; it is not collected money. |
currency | string | ISO 4217 currency code, resolved per sale location |
payment_method | string|null | How it was paid (cash, card, check, affirm, zelle, store_credit, other) |
processor | string|null | Processor or source (square, affirm, system, repairdesk, …) |
location_id | string|null | Sale location ID (multi-location tenants), or null |
processor_reference | string|null | Opaque processor reference ID, never a secret or raw token |
paid_at | string|null | ISO-8601 timestamp when the payment was recorded |
created_at | string|null | ISO-8601 creation timestamp |
All Money objects have amount_cents (integer), amount (decimal string), and currency.
Payment statuses
Section titled “Payment statuses”| Status | Meaning |
|---|---|
succeeded | A positive collection fact (charge, deposit_collection, or deposit_carryforward) |
recorded | A settlement fact that is not a positive collection, including typed returns, reversals, and adjustments. Inspect entry_type and the signed amount; do not treat it as a successful receipt. |
voided | A manual payment recorded by mistake and then voided because the money never arrived. Returned only by exact ID, with voided_at. This is not a refund. |
The staff-entered void reason is not exposed in the public Payment object. The public Payments API does not provide a void endpoint.
List payments
Section titled “List payments”GET /api/v1/paymentsReturns a cursor-paginated list of payment settlement facts, newest first. Voided payments are omitted, including when filtering by invoice or ticket. Retrieve a known payment by ID to check whether it was voided.
Scope required: payments.read
Query parameters
Section titled “Query parameters”| Parameter | Type | Description |
|---|---|---|
invoice_id | string | Filter to payments on a specific invoice (must be a positive integer) |
ticket_id | string | Filter to payments on a specific ticket |
payment_method | string | Filter by method (cash, card, check, …), case-insensitive |
processor | string | Filter by processor/source (square, affirm, system, …), case-insensitive |
location_id | string | Filter to one sale location |
paid_after | string | ISO-8601, only payments recorded at or after this time |
paid_before | string | ISO-8601, only payments recorded at or before this time |
limit | integer | Page size, 1–100 (default: 25) |
cursor | string | Opaque cursor from a previous response’s next_cursor |
curl "https://app.benchkey.com/api/v1/payments?invoice_id=5831" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const params = new URLSearchParams({ invoice_id: "5831", limit: "25" });const res = await fetch( `https://app.benchkey.com/api/v1/payments?${params}`, { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const { data, has_more, next_cursor } = await res.json();Response
Section titled “Response”{ "object": "list", "data": [ { "object": "payment", "id": "canonical:8812", "invoice_id": "5831", "ticket_id": "10042", "status": "succeeded", "entry_type": "charge", "amount": { "amount_cents": 14071, "amount": "140.71", "currency": "USD" }, "currency": "USD", "payment_method": "card", "processor": "square", "location_id": "1", "processor_reference": "sq_pay_abc123xyz", "paid_at": "2025-11-03T18:02:11.000Z", "created_at": "2025-11-03T18:02:11.000Z" } ], "has_more": false, "next_cursor": null}See Pagination for how to page through results.
Retrieve a payment
Section titled “Retrieve a payment”GET /api/v1/payments/:idReturns a single payment, including a voided manual payment. A voided payment returns 200 with status: "voided" and voided_at, subject to the same tenant, location, and ticket visibility rules as other payments.
Scope required: payments.read
curl "https://app.benchkey.com/api/v1/payments/canonical:8812" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"Node.js
Section titled “Node.js”const res = await fetch( "https://app.benchkey.com/api/v1/payments/canonical:8812", { headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" } });const payment = await res.json();Response
Section titled “Response”Returns the payment object. Returns 404 if no payment with that ID exists for this tenant, if the payment is linked to a hidden or soft-deleted ticket (matching the list behavior), or if the ID refers to an internal imported-refund marker row (which is not a real payment).
Voided payment example
Section titled “Voided payment example”GET /api/v1/payments/canonical:8813 can return the following after a mistaken manual cash payment is voided. The original amount and recording timestamps remain available for audit; this payment does not appear in GET /api/v1/payments.
{ "object": "payment", "id": "canonical:8813", "invoice_id": "5831", "ticket_id": "10042", "entry_type": "charge", "status": "voided", "voided_at": "2026-09-24T09:15:00.000Z", "amount": { "amount_cents": 10000, "amount": "100.00", "currency": "USD" }, "currency": "USD", "payment_method": "cash", "processor": "system", "processor_reference": null, "location_id": "1", "paid_at": "2026-09-24T09:00:00.000Z", "created_at": "2026-09-24T09:00:00.000Z"}Record a payment
Section titled “Record a payment”POST /api/v1/paymentsRecords a payment against an invoice through BenchKey’s split-checkout pipeline. All side effects fire: the invoice status advances to partial or paid, inventory is consumed on full payment, store credit is debited when payment_method is store_credit, cash is linked to the open register session, revenue and commission entries are written, Square payables are settled or recreated, and receipts and automation fire.
Returns 201 on success with a Location header pointing to the new payment.
Scope required: payments.write
An Idempotency-Key of 8 to 220 characters is required. Retain it before sending and reuse it only for an identical retry. See Idempotency.
This endpoint records a payment. The card method records an externally collected card payment; it does not authorize a card or start a Terminal charge. Confirm the external collection before recording it. Cash recording requires an open register at the invoice’s location.
Generate and save an operation key before the first request. The examples keep it in payment-5831.key and read the same file after a timeout or process restart. Keep the request unchanged and retain the file with the result. Use a different file and key for a separate operation. Completed responses are normally replayable for 24 hours; after that, check saved results and the resource state before resubmitting.
Request body
Section titled “Request body”| Field | Required | Description |
|---|---|---|
invoice_id | yes | The invoice to apply the payment to (positive integer) |
payment_method | yes | cash, card, check, zelle, store_credit, or other |
amount_cents | yes | Amount in integer cents, strictly positive. Must not exceed the invoice balance |
reference | no | Optional external reference (check number, processor reference, …), max 200 chars |
set -e# Keep this file for this operation, including across process restarts.if [ ! -e payment-5831.key ]; then (umask 077; set -C; uuidgen > payment-5831.key)fiBENCHKEY_OPERATION_KEY=$(cat payment-5831.key): "${BENCHKEY_OPERATION_KEY:?The saved operation key is empty}"
curl -X POST https://app.benchkey.com/api/v1/payments \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $BENCHKEY_OPERATION_KEY" \ -d '{ "invoice_id": "5831", "payment_method": "card", "amount_cents": 14071 }'Node.js
Section titled “Node.js”import { randomUUID } from "node:crypto";import { readFileSync, writeFileSync } from "node:fs";
// Create once; read the existing key after a timeout or process restart.const keyFile = "payment-5831.key";try { writeFileSync(keyFile, randomUUID(), { flag: "wx", mode: 0o600 });} catch (error) { if (error.code !== "EEXIST") throw error;}const paymentKey = readFileSync(keyFile, "utf8").trim();if (!paymentKey) throw new Error("The saved operation key is empty");
const res = await fetch("https://app.benchkey.com/api/v1/payments", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", "Idempotency-Key": paymentKey, }, body: JSON.stringify({ invoice_id: "5831", payment_method: "card", amount_cents: 14071, }),});const payment = await res.json(); // HTTP 201Response
Section titled “Response”Returns the payment object with HTTP 201.
Refund a payment
Section titled “Refund a payment”POST /api/v1/payments/:id/refundRefunds a recorded payment through BenchKey’s invoice refund pipeline. All side effects fire: the provider refund is issued via Square, Stripe, or Affirm when applicable, inventory is restored on a full refund, and register, revenue, and commission entries are reversed. The refunded_amount on the invoice is updated atomically.
Like an invoice refund, every payment refund records what happens to any unpaid invoice balance afterwards (obligation_disposition) and a machine-readable reason code.
Since refunds are applied at the invoice level, the amount defaults to this payment’s remaining refundable amount. Pass a smaller amount_cents for a partial refund. The internal route caps refunds at the invoice’s total refundable balance (payments collected minus amounts already refunded).
Returns 200 when the refund is applied immediately. A 202 returns a pending, unknown, or review outcome; it does not by itself prove the customer has received money. See refund response handling.
Imported card payments (Credit Card / Debit Card labels): payments imported from other platforms may be recorded as "Credit Card" or "Debit Card" rather than the native "card" type. These rows are treated as providerless card payments, they have no Square charge to reverse, so you must supply an explicit method (e.g. "cash", "check", or "other") to complete the refund. Omitting method on an imported card row returns 422 payment_refund_unattributable.
Stripe payments: a Stripe card payment refunds through Stripe (method stripe, or omit method). This endpoint accepts it only when the selected payment is the invoice’s only Stripe charge; otherwise it returns 422 payment_refund_unattributable, and you refund through POST /invoices/{id}/refund instead.
Scope required: payments.write
An Idempotency-Key is required; without one the request returns 400 refund_intent_token_required. Keep the same key for an identical retry (a replay returns the original result without refunding twice) and inspect the returned outcome before presenting the refund as completed.
Generate and save an operation key before the first request. The examples keep it in refund-payment-8812.key and read the same file after a timeout or process restart. Keep the request unchanged and retain the file with the result. Use a different file and key for a separate operation. Completed responses are normally replayable for 24 hours; after that, check saved results and the resource state before resubmitting.
Request body
Section titled “Request body”obligation_disposition and reason are required; the other fields are optional. Any other field returns 400 unknown_field.
| Field | Required | Description |
|---|---|---|
obligation_disposition | yes | What happens to any unpaid invoice balance after the refund. keep_open: the customer still owes it, and the invoice stays open with a balance due. close: the unpaid balance is closed and no longer owed |
reason | yes | Machine-readable reason code: lowercase letters and digits, words joined by _, -, or :, starting with a letter, at most 80 characters (uppercase is accepted and lowercased), for example customer_request or duplicate_charge |
note | no | Optional free-text note recorded with the refund decision, at most 1000 characters |
amount_cents | no | Amount to refund in integer cents. Defaults to the payment’s full remaining refundable amount. Must not exceed it |
method | no | Refund delivery method: square, stripe, affirm, cash, check, zelle, or other. Omit to auto-detect from the original payment |
reference | no | Optional external reference for the refund, max 200 chars |
set -e# Keep this file for this operation, including across process restarts.if [ ! -e refund-payment-8812.key ]; then (umask 077; set -C; uuidgen > refund-payment-8812.key)fiBENCHKEY_OPERATION_KEY=$(cat refund-payment-8812.key): "${BENCHKEY_OPERATION_KEY:?The saved operation key is empty}"
curl -X POST "https://app.benchkey.com/api/v1/payments/canonical:8812/refund" \ -H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: $BENCHKEY_OPERATION_KEY" \ -d '{ "amount_cents": 5000, "obligation_disposition": "close", "reason": "customer_request", "note": "Customer not satisfied with repair" }'Node.js
Section titled “Node.js”import { randomUUID } from "node:crypto";import { readFileSync, writeFileSync } from "node:fs";
// Create once; read the existing key after a timeout or process restart.const keyFile = "refund-payment-8812.key";try { writeFileSync(keyFile, randomUUID(), { flag: "wx", mode: 0o600 });} catch (error) { if (error.code !== "EEXIST") throw error;}const refundKey = readFileSync(keyFile, "utf8").trim();if (!refundKey) throw new Error("The saved operation key is empty");
const res = await fetch("https://app.benchkey.com/api/v1/payments/canonical:8812/refund", { method: "POST", headers: { Authorization: "Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa", "Content-Type": "application/json", "Idempotency-Key": refundKey, }, body: JSON.stringify({ amount_cents: 5000, obligation_disposition: "close", reason: "customer_request", note: "Customer not satisfied with repair", }),});const refund = await res.json(); // HTTP 200 or 202Response
Section titled “Response”An immediately applied refund returns a refund object:
{ "object": "refund", "settled": true, "payment_id": "canonical:8812", "invoice_id": "5831", "status": "succeeded", "amount": { "amount_cents": 5000, "amount": "50.00", "currency": "USD" }, "currency": "USD", "method": "square", "reference": null}| Field | Type | Description |
|---|---|---|
object | string | "refund" for an immediately applied result; a 202 can return "refund_outcome" or "refund_reconciliation" |
settled | boolean | true for an applied refund. Typed 202 outcomes carry settled too, and it is true only once an applied settlement entry exists |
payment_id | string | The payment that was refunded |
invoice_id | string | The invoice the refund was applied to |
status | string | "succeeded" for an applied result. Typed 202 outcomes can be pending, needs_review, or outcome_unknown; inspect settled and the outcome fields |
amount | Money | Amount refunded |
currency | string | ISO 4217 currency code |
method | string|null | Refund delivery method used |
reference | string|null | External reference supplied in the request |
Error codes
Section titled “Error codes”| HTTP status | Code | Meaning |
|---|---|---|
400 | missing_field | A required field is absent (invoice_id, payment_method, amount_cents) |
400 | invalid_field | A field value is invalid (non-integer cents, unknown method, non-integer invoice_id, malformed date; on refund, amount_cents over this payment’s remaining refundable amount, reason/note/reference not a string, or note/reference over its length limit) |
400 | invalid_body | The request body is not a JSON object |
400 | unknown_field | The request body contains a field this endpoint does not accept |
400 | invalid_id | The :id path segment is not a valid payment ID, such as canonical:8812 |
400 | invalid_query | A list filter parameter was supplied as an array or object instead of a single scalar value |
400 | refund_intent_token_required | A refund was sent without an Idempotency-Key header |
400 | obligation_disposition_required | A refund omitted obligation_disposition |
400 | obligation_disposition_invalid | A refund’s obligation_disposition is not keep_open or close |
400 | obligation_closure_reason_required | A refund omitted reason |
400 | obligation_closure_reason_invalid | A refund’s reason is not a machine-readable code (lowercase letters and digits joined by _, -, or :, starting with a letter, at most 80 characters) |
400 | refund_failed | The refund was rejected by a business rule (invoice not in refundable state, amount over cap, or method mismatch). A more specific code is returned instead when BenchKey has one |
404 | not_found | No payment or invoice with that ID exists for this tenant |
409 | refund_conflict | The refund conflicts with the invoice’s current refund state. A more specific code is returned instead when BenchKey has one, for example refund_in_progress (this refund is still being processed) or refund_balance_changed (the refundable balance changed; review the invoice and submit a new refund) |
422 | invoice_voided | The target invoice is voided and cannot accept payments |
422 | invoice_already_paid | The target invoice is already fully paid |
422 | invoice_refunded | The target invoice has been fully refunded and cannot accept payments |
422 | payment_failed | The payment could not be recorded (balance exceeded, or the route returned no inserted row) |
422 | duplicate_payment | This payment was already recorded, use an Idempotency-Key to retry safely |
422 | store_credit_customer_required | Store credit payments require an invoice customer email or phone |
422 | insufficient_store_credit | The customer does not have enough store credit |
422 | payment_method_disabled | That payment method is disabled for this tenant |
422 | payment_not_refundable | The payment is not linked to an invoice, or has no positive amount to refund |
422 | payment_refund_unattributable | The refund cannot be safely attributed to this specific payment, use POST /invoices/{id}/refund instead. Returned when the invoice already has a refund recorded (BenchKey tracks refunds at the invoice level only), when the payment’s method cannot be matched to a refund rail without an explicit method, or when the payment is not the invoice’s current Square or Affirm payment or its only Stripe charge |
422 | payment_refund_method_mismatch | The requested method does not match the selected payment’s instrument, so it could refund a different payment. Refund this payment with its own method, or use POST /invoices/{id}/refund |
422 | store_credit_refund_unsupported | The payment was made with store credit; refunding store-credit money via the API is not yet supported, reverse the store-credit payment in the app |
422 | refund_failed | The refund failed for a non-business-rule reason (e.g. provider error) |
429 | rate_limited | Rate limit exceeded |
503 | refund_authority_unavailable | Refund history could not be read, so no refund was attempted. Retry shortly |
503 | payment_authority_unavailable | Payment records could not be read. Retry after the Retry-After delay with the same Idempotency-Key |
403 | insufficient_scope | API key lacks payments.read or payments.write |
See Errors for the full error envelope format.