Reference

API reference

The complete list of integration points for building your own integration on the Pay-Pi Connect API — no POS required. The /v1 surface is frozen: breaking changes ship under a new version prefix. Every route below is relative to the base URL.

https://connect.pay-pi.com
Interactive reference: explore and try every endpoint in the Swagger UI, or download the OpenAPI spec (openapi.json) — both generated from the live API, always in sync with this page.

Integration points at a glance

Area How you connect What it does
Tokenization Finix.js in the browser Collect the card/bank, get a single-use token.
Charge POST /v1/payments Create a payment from a token or stored instrument.
Refund POST /v1/payments/{id}/refund Full or partial reversal.
Lookup GET /v1/payments/{id} Current ledger state of a payment.
Reconcile GET /v1/payments · /v1/payouts · /v1/disputes List payments, payouts, and disputes via the API.
Onboarding POST /v1/onboarding + GET /v1/onboarding/status Hosted merchant onboarding + readiness.
Card-present /v1/terminal/* + POST /v1/payments/terminal In-person sales on PAX readers.
Config GET /v1/meta Public app id + environment (no auth).
Events Your callback URL Signed callbacks for async settlement, disputes, onboarding.

Authentication

Every tenant request carries your secret API key (lc_live_…), as either the X-Api-Key header or a Bearer token. Keys are created and revoked in your dashboard and are shown once at issue time — Pay-Pi stores only a hash, so keep the raw value as a backend secret.

X-Api-Key: lc_live_XXXXXXXXXXXXXXXXXXXX
# or, equivalently:
Authorization: Bearer lc_live_XXXXXXXXXXXXXXXXXXXX

GET /v1/meta and the /health* checks are the only anonymous routes.

Idempotency

POST /v1/payments and POST /v1/payments/terminal require an Idempotency-Key header — derive it from your order key. A replay with the same key returns the original result; the same key with a different body returns 409, as does a key whose original request is still in flight. On POST /v1/payments/{id}/refund the key is optional (supply one to make a retried refund safe to replay).

Endpoints

Payments

Auth: Tenant key
POST /v1/payments
Create a charge. Idempotency-Key required.
POST /v1/payments/{id}/refund
Refund full or partial. Idempotency-Key optional.
GET /v1/payments/{id}
Fetch the ledger record for an lcPaymentId.

Reconciliation

Auth: Tenant key
GET /v1/payments
List/search payments — from, to, status, limit (≤200); cursor/nextCursor paging.
GET /v1/payouts
List settlements/payouts to your bank (net, fees, date).
GET /v1/disputes
List disputes (chargebacks) with state + respond-by.
POST /v1/disputes/{id}/evidence
Upload evidence (multipart) to contest a dispute.
POST /v1/disputes/{id}/submit
Submit the uploaded evidence for review.

Terminal / card-present

Auth: Tenant key
POST /v1/terminal/readers
Register a PAX reader to your merchant.
GET /v1/terminal/readers
List your readers.
GET /v1/terminal/readers/{id}
One reader. ?connection=true adds live Open/Closed state.
POST /v1/terminal/readers/{id}/activate
Activate a reader.
POST /v1/terminal/readers/{id}/deactivate
Deactivate a reader.
POST /v1/payments/terminal
Start an in-person sale. Idempotency-Key required. Async (processing).

Onboarding

Auth: Tenant key
POST /v1/onboarding
Create a hosted onboarding form → { formId, status, onboardingUrl, expiresAt }.
GET /v1/onboarding/form/{formId}
Read form status and finalize (attach the merchant when complete).
GET /v1/onboarding/status
Current readiness. ?refresh=true re-pulls from Finix.

Meta / health

Auth: Anonymous
GET /v1/meta
Public service/environment info + finixApplicationId. No secrets.
GET /health
Simple { status: "ok" }.
GET /health/live
Liveness — the process is up.
GET /health/ready
Readiness — the datastore is reachable.

Create a payment

Provide either finixToken (from Finix.js) or a reusable finixInstrumentId (PI…) — exactly one. When using a token, buyer is required. amountCents is in the smallest currency unit.

curl -X POST https://connect.pay-pi.com/v1/payments \
  -H "X-Api-Key: lc_live_..." \
  -H "Idempotency-Key: SO-1001" \
  -H "Content-Type: application/json" \
  -d '{
    "amountCents": 5000,
    "currency": "USD",
    "finixToken": "TKxxxx",
    "buyer": { "firstName": "Ada", "lastName": "Lovelace", "email": "ada@example.com" },
    "orderRef": "SO-1001"
  }'

Card charges settle synchronously; ACH returns processing and settles via a callback.

{
  "lcPaymentId": "lcpay_9f2c4b1e…",
  "status": "succeeded",
  "amountCents": 5000,
  "applicationFeeCents": 155,
  "finixTransferId": "TR…",
  "finixState": "SUCCEEDED"
}
lcPaymentIdPay-Pi's stable id (lcpay_…) — store it for lookups and refunds.
statussucceeded, processing, failed, canceled, partially_refunded, refunded, or disputed.
applicationFeeCentsThe Pay-Pi fee applied to this payment.
finixTransferIdThe underlying Finix transfer id.
finixStateThe raw Finix transfer state.

Refund a payment

Omit amountCents for a full refund. Refunds are guarded: the payment must be refundable (succeeded or partially_refunded) and the amount ≤ the remaining balance, else 409. The response is a PaymentResponse — the same shape returned by GET /v1/payments/{id}.

curl -X POST https://connect.pay-pi.com/v1/payments/lcpay_9f2c…/refund \
  -H "X-Api-Key: lc_live_..." \
  -H "Content-Type: application/json" \
  -d '{
    "amountCents": 2500,
    "reason": "requested_by_customer",
    "refundApplicationFee": true
  }'
{
  "lcPaymentId": "lcpay_9f2c4b1e…",
  "status": "partially_refunded",
  "amountCents": 5000,
  "applicationFeeCents": 155,
  "currency": "USD",
  "orderRef": "SO-1001",
  "processorTransferId": "TR…"
}

List & reconcile

For a headless integration (no dashboard), read and act on your own data with your API key. GET /v1/payments lists/searches payments — filter with from, to (UTC ISO), and status; limit caps the page (≤200). When hasMore is true, pass the returned nextCursor back as ?cursor= for the next page. GET /v1/payouts and GET /v1/disputes return your settlements and chargebacks, and you can contest a dispute with POST /v1/disputes/{id}/evidence (a multipart file) followed by POST /v1/disputes/{id}/submit.

curl "https://connect.pay-pi.com/v1/payments?status=succeeded&limit=50" \
  -H "X-Api-Key: lc_live_..."
{
  "data": [
    {
      "lcPaymentId": "lcpay_9f2c…",
      "status": "succeeded",
      "amountCents": 5000,
      "currency": "USD",
      "applicationFeeCents": 155,
      "amountRefundedCents": 0,
      "method": "card",
      "orderRef": "SO-1001",
      "processorTransferId": "TR…",
      "createdAtUtc": "2026-08-03T14:05:11Z",
      "updatedAtUtc": "2026-08-03T14:05:12Z"
    }
  ],
  "hasMore": false
}

Onboarding & readiness

Onboarding is a Finix hosted form. Create one with POST /v1/onboarding, send the merchant to the returned onboardingUrl, and finalize by reading GET /v1/onboarding/form/{formId} on return. Charges are gated until the merchant is approved — check GET /v1/onboarding/status:

{
  "processorMerchantId": "MU…",
  "onboardingStatus": "enabled",
  "chargesEnabled": true,
  "payoutsEnabled": true,
  "reasons": [],
  "requirements": [],
  "pci": { "status": "valid", "validUntil": "2027-07-17", "actionUrl": null },
  "onboardingUrl": null
}

onboardingStatus is one of not_started, pending, enabled, or restricted. Charging while chargesEnabled is false returns a 403 merchant-not-ready. See Onboarding & readiness.

Meta (public config)

GET /v1/meta is the one source of truth for the public values your frontend needs — no auth. Read it at runtime so a sandbox→live move needs no code change.

{
  "service": "connect.pay-pi.com",
  "processor": "DUMMY_V1",
  "finixEnvironment": "sandbox",
  "finixApplicationId": "APxxxx",
  "finixApiVersion": "…",
  "currency": "USD"
}

Callbacks (Pay-Pi → your app)

Some outcomes arrive after an API call returns — ACH settlement, disputes, and onboarding changes. Pay-Pi POSTs these as signed events to the callback URL you register in your dashboard, with the raw JSON body and these headers:

LC-Signature: t=1718900000,v1=3f9a…      # HMAC-SHA256, verify before trusting
LC-Event-Type: transfer.updated          # {entity}.{type}
LC-Event-Id: event_…                      # dedupe on this

LC-Event-Type carries the underlying Finix {entity}.{type} (e.g. transfer.updated, merchant.updated, dispute.created). Verify the signature before trusting the body: v1 = HMAC_SHA256(signingSecret, "{t}.{rawBody}") as lowercase hex. Reject if t is older than your tolerance (e.g. 5 min), dedupe on LC-Event-Id, and ack with 2xx (non-2xx is retried with backoff).

const crypto = require("crypto");
function verify(secret, header, rawBody, toleranceSec = 300) {
  const parts = Object.fromEntries(header.split(",").map((kv) => kv.split("=")));
  const t = Number(parts.t);
  if (Math.abs(Date.now() / 1000 - t) > toleranceSec) return false;
  const expected = crypto.createHmac("sha256", secret).update(t + "." + rawBody).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}
// Use the RAW request body — any re-serialization changes the signature.

See Webhooks for the delivery model and handler guidance.

Test your integration

Signed-in merchants can run a full tokenize → charge → refund against their own sandbox merchant from the dashboard's Developer area — it embeds Finix.js, uses your sandbox credentials, and shows the exact request and response (plus the equivalent curl) so you can replicate it in your own backend. No code required to prove the path works end to end.

Errors

All failures are application/problem+json (RFC 7807) with a type, title, status, detail, and traceId. See the full error reference.

Next steps