Documentation

Payment Sessions

Payment Sessions API

Payment sessions power PerfectPay's QR code payment flow. A session represents a single payment opportunity with an HMAC-signed QR URL, a W3C Verifiable Presentation, and real-time status updates.

Create a Payment Session

POST /v2/payment-sessions

GET //sandbox.perfectpay.ai/v2/payment-sessions
curl https://sandbox.perfectpay.ai/v2/payment-sessions \
  -X POST \
  -H "api-key: YOUR_SECRET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 575,
    "currency": "USD",
    "supported_rails": ["fednow", "rtp", "push_to_card"],
    "terminal_id": "term_store_42",
    "metadata": {
      "order_id": "ORD-1234",
      "register": "3"
    }
  }'

Request fields:

FieldTypeRequiredDescription
amountintegeryesAmount in minor units (575 = $5.75)
currencystringyesISO 4217 currency code
supported_railsstring[]yesInstant rails to offer: fednow, rtp, push_to_card, stablecoin, same_day_ach
terminal_idstringnoPOS terminal identifier
idempotency_keystringnoPrevents duplicate session creation
metadataobjectnoArbitrary key-value pairs

Response:

JSON
{
  "session_id": "2mGFCDJt5RmJlk9WcPqN0123456",
  "status": "created",
  "amount": 575,
  "currency": "USD",
  "qr_url": "https://pay.perfectpay.ai/s/2mGFCDJt5RmJlk9WcPqN0123456?h=abc123...",
  "verifiable_presentation": {
    "@context": ["https://www.w3.org/ns/credentials/v2", "https://perfectpay.ai/ns/payments/v1"],
    "type": ["VerifiablePresentation"],
    "holder": "did:web:perfectpay.ai",
    "verifiableCredential": [{
      "type": ["VerifiableCredential", "PaymentCredential"],
      "credentialSubject": {
        "session_id": "2mGFCDJt5RmJlk9WcPqN0123456",
        "jti": "550e8400-e29b-41d4-a716-446655440000",
        "merchant": {"id": "merchant_abc", "name": "Corner Store", "mcc": "5411"},
        "amount": {"value": "5.75", "currency": "USD"},
        "supported_rails": ["fednow", "rtp", "push_to_card"],
        "expiry": "2026-03-23T15:15:00Z"
      }
    }],
    "proof": {
      "type": "JsonWebSignature2020",
      "jws": "eyJhbGciOiJFUzI1NiJ9...",
      "created": "2026-03-23T15:00:00Z"
    }
  },
  "ws_token": "wst_abc123...",
  "expires_at": "2026-03-23T15:15:00Z",
  "created_at": "2026-03-23T15:00:00Z"
}

The session_id is a 27-character KSUID. The qr_url should be rendered as a QR code for the customer to scan. The ws_token is used for customer-side WebSocket authentication.

Fetch a Session (After QR Scan)

GET /v2/payment-sessions/{session_id}?h={hmac}

This is a public endpoint authenticated by the HMAC parameter. It is called when a customer scans the QR code.

GET //sandbox.perfectpay.ai/v2/payment-sessions/2mGFCDJt5RmJlk9WcPqN0123456?h=abc123..."
curl "https://sandbox.perfectpay.ai/v2/payment-sessions/2mGFCDJt5RmJlk9WcPqN0123456?h=abc123..."

The response includes the session details, merchant info, amount, supported rails, and the Verifiable Presentation. Each session allows a maximum of 5 fetch attempts before it locks.

Check Session Status

GET /v2/payment-sessions/{session_id}/status

GET //sandbox.perfectpay.ai/v2/payment-sessions/2mGFCDJt5RmJlk9WcPqN0123456/status
curl https://sandbox.perfectpay.ai/v2/payment-sessions/2mGFCDJt5RmJlk9WcPqN0123456/status \
  -H "api-key: YOUR_SECRET_API_KEY"

Returns a lightweight status response without the full VP payload. Use this for polling when WebSocket is not available.

Cancel a Session

DELETE /v2/payment-sessions/{session_id}

GET //sandbox.perfectpay.ai/v2/payment-sessions/2mGFCDJt5RmJlk9WcPqN0123456
curl https://sandbox.perfectpay.ai/v2/payment-sessions/2mGFCDJt5RmJlk9WcPqN0123456 \
  -X DELETE \
  -H "api-key: YOUR_SECRET_API_KEY"

Only sessions in created or scanned status can be cancelled.

Confirm a Session Payment

POST /v2/payments/confirm-session

This endpoint is called by the customer's client (Web SDK or mobile app) after scanning and selecting a funding source. It uses customer JWT authentication.

GET //sandbox.perfectpay.ai/v2/payments/confirm-session
curl https://sandbox.perfectpay.ai/v2/payments/confirm-session \
  -X POST \
  -H "Authorization: Bearer CUSTOMER_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "session_id": "2mGFCDJt5RmJlk9WcPqN0123456",
    "funding_source_id": "fs_abc123",
    "rail": "fednow"
  }'

Capture a Payment

POST /v2/payments/{session_id}/capture

For sessions created with manual capture, use this to capture after authorization.

GET //sandbox.perfectpay.ai/v2/payments/2mGFCDJt5RmJlk9WcPqN0123456/capture
curl https://sandbox.perfectpay.ai/v2/payments/2mGFCDJt5RmJlk9WcPqN0123456/capture \
  -X POST \
  -H "api-key: YOUR_SECRET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 575
  }'

Void a Payment

POST /v2/payments/{session_id}/void

Cancel a pre-authorized payment before capture.

GET //sandbox.perfectpay.ai/v2/payments/2mGFCDJt5RmJlk9WcPqN0123456/void
curl https://sandbox.perfectpay.ai/v2/payments/2mGFCDJt5RmJlk9WcPqN0123456/void \
  -X POST \
  -H "api-key: YOUR_SECRET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "reason": "Customer changed mind"
  }'

Refund a Session Payment

POST /v2/payments/{session_id}/refund

GET //sandbox.perfectpay.ai/v2/payments/2mGFCDJt5RmJlk9WcPqN0123456/refund
curl https://sandbox.perfectpay.ai/v2/payments/2mGFCDJt5RmJlk9WcPqN0123456/refund \
  -X POST \
  -H "api-key: YOUR_SECRET_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "amount": 575,
    "reason": "Item returned"
  }'

Funding Sources

Customers link funding sources to pay via instant rails. These endpoints use customer JWT authentication. The {customer_id} in the path must match the JWT sub claim.

POST /v2/customers/{customer_id}/funding-sources

GET //sandbox.perfectpay.ai/v2/customers/cus_abc123/funding-sources
curl https://sandbox.perfectpay.ai/v2/customers/cus_abc123/funding-sources \
  -X POST \
  -H "Authorization: Bearer CUSTOMER_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "type": "bank_account",
    "plaid_public_token": "public-sandbox-abc123...",
    "plaid_account_id": "acct_abc123"
  }'

Funding source types:

TypeFieldsIntegration
bank_accountplaid_public_token, plaid_account_idPlaid for account verification
stablecoin_walletwallet_address, chain, stablecoin_typeDirect blockchain wallet
debit_cardcard_number, card_exp_month, card_exp_yearPush-to-Card rails

List Funding Sources

GET /v2/customers/{customer_id}/funding-sources

GET //sandbox.perfectpay.ai/v2/customers/cus_abc123/funding-sources
curl https://sandbox.perfectpay.ai/v2/customers/cus_abc123/funding-sources \
  -H "Authorization: Bearer CUSTOMER_JWT"

Delete a Funding Source

DELETE /v2/customers/{customer_id}/funding-sources/{funding_source_id}

GET //sandbox.perfectpay.ai/v2/customers/cus_abc123/funding-sources/fs_abc123
curl https://sandbox.perfectpay.ai/v2/customers/cus_abc123/funding-sources/fs_abc123 \
  -X DELETE \
  -H "Authorization: Bearer CUSTOMER_JWT"

Session Expiry

Sessions expire after 15 minutes by default. A background scheduler checks for expired sessions every 30 seconds, transitions them to expired status, broadcasts a WebSocket event, and fires a webhook.