Workflow guide

Auto-detect customer payments

For recurring or embedded fintech flows, PayChainHQ can monitor permanent customer addresses, auto-generate invoice records for detected supported payments, and emit normal invoice webhooks without requiring a pre-created invoice every time.

When this flow fits

  • Your app has repeat users and you want each customer to keep a stable deposit address.
  • You want webhook-driven payment confirmation without forcing your backend to create a fresh invoice before every repeat payment.
  • You still want every detected receipt normalized into an invoice-shaped accounting object inside PayChainHQ.

How the auto-detection flow works

  1. Create a customer with your stable `externalRef`. PayChainHQ provisions permanent customer deposit addresses for supported chains.
  2. Surface that permanent address inside your app wherever the customer is expected to pay again.
  3. PayChainHQ keeps those customer addresses registered for provider webhook monitoring.
  4. If a supported payment lands and there is no still-open invoice for that customer, PayChainHQ can auto-generate a new invoice record from the detected receipt.
  5. The resulting invoice is settled from the detected payment, and normal signed `invoice.paid` / invoice lifecycle webhooks continue from there.
  6. If a payment ever needs recovery or backfill, operators can use the payment-discovery flow to rescan business-owned customer and invoice addresses.

Backend implementation sequence

  1. Create the PayChainHQ customer with your stable `externalRef`.
  2. Store the returned PayChainHQ customer ID next to your own customer/user record.
  3. Render the returned customer address metadata in your cashier, including network, supported token, and QR payload.
  4. Receive a signed `invoice.*` webhook when PayChainHQ materializes a detected payment into an invoice.
  5. Fetch invoice detail from PayChainHQ using `data.invoiceId`.
  6. Idempotently credit your ledger exactly once for that invoice ID.

Rules that matter

RuleWhat it means in practice
Customer identity comes firstThis flow depends on a stored customer with permanent deposit addresses. It is not the right model for anonymous one-off receipts.
Invoices still remain the accounting primitiveEven when no invoice is pre-created, detected payments are materialized into auto-generated invoices so your downstream systems can keep one invoice-shaped source of truth.
Open invoices win firstIf the customer already has an active pending / confirming / partially paid invoice on that chain and network, the detected payment is applied to that invoice instead of creating a duplicate.
Small detected transfers may be skippedAuto-generated invoice handling enforces a minimum value threshold, so dust or very small transfers do not create noisy receivable records.

Do not treat raw address activity as fulfillment truth

Your product should still react to the resulting invoice lifecycle and signed webhooks, not to chain activity alone. The address is the collection rail; the invoice event stream is the business-state contract.

Endpoints involved

POST/api/v1/businesses/:id/customersx-api-keySandbox + live

Create customer

Create a reusable customer identity with permanent deposit addresses.

When to use it

Use before recurring invoicing or when you want to track invoices by a stable customer record.

Idempotency: recommended

FieldLocationRequiredDescriptionExample / default
idpathYesBusiness ID.-
externalRefbodyYesYour internal customer identifier.-
emailbodyNoOptional customer email.-

Cautions

  • For one-off anonymous payments, create invoices without a customerId instead of creating throwaway customers.
  • Address provisioning depends on the business wallet being ready in the selected environment.

Common errors

  • 401: Missing or invalid API key.
  • 400: Payload shape, query params, or business-state validation failed.

Sample request body

json
{
  "externalRef": "customer_001",
  "email": "customer@example.com"
}

Sample response

json
{
  "id": "cus_123",
  "externalRef": "customer_001",
  "email": "customer@example.com",
  "addresses": [
    { "chainFamily": "eth", "address": "0xE5fa2F71065fD49823D33EdD84ecFD2D6245c916" },
    { "chainFamily": "sol", "address": "8gRLe6GiQx2jo9mQFcbwy34u2W4zD2rN5q4mmD8iP2Mp" }
  ]
}

cURL example

bash
curl -X POST "https://api.paychainhq.io/api/v1/businesses/biz_123/customers" \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_your_business_key" \
  -H "Idempotency-Key: example-request-001" \
  -d '{
  "externalRef": "customer_001",
  "email": "customer@example.com"
}'

Node.js example

ts
const response = await fetch('https://api.paychainhq.io/api/v1/businesses/biz_123/customers', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'pk_live_your_business_key',
    'Idempotency-Key': 'example-request-001',
  },
  body: JSON.stringify({
  "externalRef": "customer_001",
  "email": "customer@example.com"
}),
});
const payload = await response.json();
console.log(payload);
GET/api/v1/businesses/:id/customers/:customerIdx-api-keySandbox + live

Get customer

Return a single customer record and its addresses.

When to use it

Use when showing a single customer detail page or resolving invoice ownership.

FieldLocationRequiredDescriptionExample / default
idpathYesBusiness ID.-
customerIdpathYesCustomer ID.-

Common errors

  • 401: Missing or invalid API key.
  • 404: Requested resource does not exist or is not owned by the business.

Sample response

json
{
  "id": "cus_123",
  "externalRef": "customer_001",
  "email": "customer@example.com",
  "addresses": [
    { "chainFamily": "eth", "address": "0xE5fa2F71065fD49823D33EdD84ecFD2D6245c916" },
    { "chainFamily": "sol", "address": "8gRLe6GiQx2jo9mQFcbwy34u2W4zD2rN5q4mmD8iP2Mp" }
  ]
}

cURL example

bash
curl -X GET "https://api.paychainhq.io/api/v1/businesses/biz_123/customers/cus_123" \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_your_business_key"

Node.js example

ts
const response = await fetch('https://api.paychainhq.io/api/v1/businesses/biz_123/customers/cus_123', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'pk_live_your_business_key',
  },
});
const payload = await response.json();
console.log(payload);
GET/api/v1/businesses/:id/invoicesx-api-keySandbox + live

List invoices

List invoices for a business with lifecycle and pagination data.

When to use it

Use for admin tables, internal reconciliation, or polling fallback when webhooks are delayed.

FieldLocationRequiredDescriptionExample / default
idpathYesBusiness ID.-
statusqueryNoFilter by invoice status.-
customerIdqueryNoRestrict results to a single customer.-
pagequeryNoPage number.1
limitqueryNoPage size.20

Cautions

  • Expired invoices may remain raw-pending briefly; UI should treat `expiresAt` as authoritative for effective open state.

Common errors

  • 401: Missing or invalid API key.

Sample response

json
{
  "data": [
    {
      "id": "inv_123",
      "status": "pending",
      "token": "USDC",
      "chain": "eth",
      "networkId": "base-mainnet",
      "amount": {
        "raw": "150000000",
        "decimals": 6,
        "display": "150",
        "symbol": "USDC",
        "chain": "eth",
        "networkId": "base-mainnet"
      },
      "expiresAt": "2026-03-15T13:00:00.000Z"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 1,
    "totalPages": 1,
    "hasMore": false
  }
}

cURL example

bash
curl -X GET "https://api.paychainhq.io/api/v1/businesses/biz_123/invoices?page=1&limit=20" \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_your_business_key"

Node.js example

ts
const response = await fetch('https://api.paychainhq.io/api/v1/businesses/biz_123/invoices?page=1&limit=20', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'pk_live_your_business_key',
  },
});
const payload = await response.json();
console.log(payload);
GET/api/v1/businesses/invoices/:invoiceIdx-api-keySandbox + live

Get business invoice

Return a business-scoped invoice detail with payment summary and tolerance metadata.

When to use it

Use after webhook receipt or as a fallback read before updating your order state.

FieldLocationRequiredDescriptionExample / default
invoiceIdpathYesInvoice ID.-

Cautions

  • `paidAmount` is always the actual deposited amount. A tolerance-settled invoice does not synthesize the shortfall.

Common errors

  • 401: Missing or invalid API key.
  • 404: Requested resource does not exist or is not owned by the business.

Sample response

json
{
  "id": "inv_123",
  "status": "paid",
  "settledByTolerance": true,
  "underpaymentToleranceBps": 50,
  "underpaymentToleranceMaxUsd": "0.50",
  "toleranceRaw": "500000",
  "toleranceShortfall": {
    "raw": "250000",
    "decimals": 6,
    "display": "0.25",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "amount": {
    "raw": "150000000",
    "decimals": 6,
    "display": "150",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "paidAmount": {
    "raw": "149750000",
    "decimals": 6,
    "display": "149.75",
    "symbol": "USDC",
    "chain": "eth",
    "networkId": "base-mainnet"
  },
  "depositAddress": "0xE5fa2F71065fD49823D33EdD84ecFD2D6245c916",
  "paymentSummary": {
    "paymentsReceived": 1
  }
}

cURL example

bash
curl -X GET "https://api.paychainhq.io/api/v1/businesses/invoices/inv_123" \
  -H "Content-Type: application/json" \
  -H "x-api-key: pk_live_your_business_key"

Node.js example

ts
const response = await fetch('https://api.paychainhq.io/api/v1/businesses/invoices/inv_123', {
  method: 'GET',
  headers: {
    'Content-Type': 'application/json',
    'x-api-key': 'pk_live_your_business_key',
  },
});
const payload = await response.json();
console.log(payload);
Auto-detect customer payments | PayChainHQ