Ir al contenido

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 returns 404. The refund amount itself is visible on the parent invoice’s refunded_amount field.

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 currency field on the returned object reflects the correct per-location currency.

{
"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"
}
FieldTypeDescription
idstringSource-qualified payment ID, such as canonical:8812 or legacy:8812. Use the returned ID for subsequent requests.
invoice_idstring|nullThe invoice this payment is applied to
ticket_idstring|nullThe 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).
statusstringsucceeded, recorded, or voided; see Payment statuses
voided_atstringISO-8601 timestamp when the payment was voided. Present only when status is voided; omitted otherwise.
entry_typestringSettlement fact type, for example charge, refund, or collection_reversal
amountMoneySigned amount of the settlement fact. A voided payment retains its original amount for audit; it is not collected money.
currencystringISO 4217 currency code, resolved per sale location
payment_methodstring|nullHow it was paid (cash, card, check, affirm, zelle, store_credit, other)
processorstring|nullProcessor or source (square, affirm, system, repairdesk, …)
location_idstring|nullSale location ID (multi-location tenants), or null
processor_referencestring|nullOpaque processor reference ID, never a secret or raw token
paid_atstring|nullISO-8601 timestamp when the payment was recorded
created_atstring|nullISO-8601 creation timestamp

All Money objects have amount_cents (integer), amount (decimal string), and currency.

StatusMeaning
succeededA positive collection fact (charge, deposit_collection, or deposit_carryforward)
recordedA 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.
voidedA 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.


GET /api/v1/payments

Returns 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

ParameterTypeDescription
invoice_idstringFilter to payments on a specific invoice (must be a positive integer)
ticket_idstringFilter to payments on a specific ticket
payment_methodstringFilter by method (cash, card, check, …), case-insensitive
processorstringFilter by processor/source (square, affirm, system, …), case-insensitive
location_idstringFilter to one sale location
paid_afterstringISO-8601, only payments recorded at or after this time
paid_beforestringISO-8601, only payments recorded at or before this time
limitintegerPage size, 1–100 (default: 25)
cursorstringOpaque cursor from a previous response’s next_cursor
Terminal window
curl "https://app.benchkey.com/api/v1/payments?invoice_id=5831" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
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();
{
"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.


GET /api/v1/payments/:id

Returns 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

Terminal window
curl "https://app.benchkey.com/api/v1/payments/canonical:8812" \
-H "Authorization: Bearer bk_live_01J8X4_kvWz9nP3mRqTs7uYeBfGhDjLa"
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();

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

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"
}

POST /api/v1/payments

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

FieldRequiredDescription
invoice_idyesThe invoice to apply the payment to (positive integer)
payment_methodyescash, card, check, zelle, store_credit, or other
amount_centsyesAmount in integer cents, strictly positive. Must not exceed the invoice balance
referencenoOptional external reference (check number, processor reference, …), max 200 chars
Terminal window
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)
fi
BENCHKEY_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
}'
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 201

Returns the payment object with HTTP 201.


POST /api/v1/payments/:id/refund

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

obligation_disposition and reason are required; the other fields are optional. Any other field returns 400 unknown_field.

FieldRequiredDescription
obligation_dispositionyesWhat 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
reasonyesMachine-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
notenoOptional free-text note recorded with the refund decision, at most 1000 characters
amount_centsnoAmount to refund in integer cents. Defaults to the payment’s full remaining refundable amount. Must not exceed it
methodnoRefund delivery method: square, stripe, affirm, cash, check, zelle, or other. Omit to auto-detect from the original payment
referencenoOptional external reference for the refund, max 200 chars
Terminal window
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)
fi
BENCHKEY_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"
}'
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 202

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
}
FieldTypeDescription
objectstring"refund" for an immediately applied result; a 202 can return "refund_outcome" or "refund_reconciliation"
settledbooleantrue for an applied refund. Typed 202 outcomes carry settled too, and it is true only once an applied settlement entry exists
payment_idstringThe payment that was refunded
invoice_idstringThe invoice the refund was applied to
statusstring"succeeded" for an applied result. Typed 202 outcomes can be pending, needs_review, or outcome_unknown; inspect settled and the outcome fields
amountMoneyAmount refunded
currencystringISO 4217 currency code
methodstring|nullRefund delivery method used
referencestring|nullExternal reference supplied in the request

HTTP statusCodeMeaning
400missing_fieldA required field is absent (invoice_id, payment_method, amount_cents)
400invalid_fieldA 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)
400invalid_bodyThe request body is not a JSON object
400unknown_fieldThe request body contains a field this endpoint does not accept
400invalid_idThe :id path segment is not a valid payment ID, such as canonical:8812
400invalid_queryA list filter parameter was supplied as an array or object instead of a single scalar value
400refund_intent_token_requiredA refund was sent without an Idempotency-Key header
400obligation_disposition_requiredA refund omitted obligation_disposition
400obligation_disposition_invalidA refund’s obligation_disposition is not keep_open or close
400obligation_closure_reason_requiredA refund omitted reason
400obligation_closure_reason_invalidA refund’s reason is not a machine-readable code (lowercase letters and digits joined by _, -, or :, starting with a letter, at most 80 characters)
400refund_failedThe 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
404not_foundNo payment or invoice with that ID exists for this tenant
409refund_conflictThe 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)
422invoice_voidedThe target invoice is voided and cannot accept payments
422invoice_already_paidThe target invoice is already fully paid
422invoice_refundedThe target invoice has been fully refunded and cannot accept payments
422payment_failedThe payment could not be recorded (balance exceeded, or the route returned no inserted row)
422duplicate_paymentThis payment was already recorded, use an Idempotency-Key to retry safely
422store_credit_customer_requiredStore credit payments require an invoice customer email or phone
422insufficient_store_creditThe customer does not have enough store credit
422payment_method_disabledThat payment method is disabled for this tenant
422payment_not_refundableThe payment is not linked to an invoice, or has no positive amount to refund
422payment_refund_unattributableThe 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
422payment_refund_method_mismatchThe 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
422store_credit_refund_unsupportedThe 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
422refund_failedThe refund failed for a non-business-rule reason (e.g. provider error)
429rate_limitedRate limit exceeded
503refund_authority_unavailableRefund history could not be read, so no refund was attempted. Retry shortly
503payment_authority_unavailablePayment records could not be read. Retry after the Retry-After delay with the same Idempotency-Key
403insufficient_scopeAPI key lacks payments.read or payments.write

See Errors for the full error envelope format.

Estado del sistema