# Payment Hub project integration guide

Canonical API base URL: `https://payments.null2null.com`

Machine-readable API contract: `https://payments.null2null.com/openapi.json`

AI discovery file: `https://payments.null2null.com/llms.txt`

## 1. Responsibility boundary

Payment Hub is a private, multi-project payment orchestration service.

Payment Hub owns:

- Project-scoped payment orders
- Server-side product routes and fixed prices
- Provider adapters and provider account routing
- Provider reconciliation
- Signed outbound payment events and retries
- Payment audit records

The integrating project owns:

- Accounts and authentication
- Local merchant orders
- Customer identity mapping
- Subscriptions, credits, licenses, or other entitlements
- Refund and entitlement business rules
- User-facing purchase history and support

Payment Hub never grants a project entitlement. A project backend grants or revokes entitlement after it validates a Payment Hub result.

## 2. Non-negotiable security rules

1. Store the project API key only in the project backend secret manager.
2. Never include the API key in a browser, mobile app, desktop app, source repository, analytics event, crash report, or log.
3. Create a local merchant order before calling Payment Hub.
4. Send a unique `Idempotency-Key` on every create request.
5. Do not accept price, currency, provider, wallet, or payment method from the client. Payment Hub resolves them from `productId`.
6. Do not grant entitlement for `creating`, `waiting`, `partially_paid`, `processing`, `failed`, or `expired`.
7. Grant entitlement only for `succeeded`, and make that operation idempotent.
8. Treat `refunded` as a separate terminal event and apply the project's documented revocation policy.
9. Verify every webhook using the raw request body before parsing JSON.
10. Re-query the order after a valid webhook before applying high-value or irreversible business changes.

## 3. Information required to provision a project

Send the following to the Payment Hub operator through an approved secure channel:

- Project slug and display name
- Environment name, such as staging or production
- HTTPS checkout return URL
- HTTPS outbound payment webhook URL
- A secret-manager reference for the outbound HMAC secret
- Product IDs, display names, fixed prices, currencies, and allowed distribution channels
- A project-owned provider merchant account, store, or wallets
- Provider API credentials through an approved server-side secret channel

The operator provisions the project using an internal admin interface that is not reachable from the public internet. The project API key is returned once. Save it immediately in the project backend secret manager.

Provider accounts and products are staged inactive. Payment Hub validates the provider credential and checkout security settings before activation. Project checkout must remain disabled until controlled production transactions and refund/reconciliation tests pass.

Recommended project backend variables:

```text
PAYMENT_HUB_BASE_URL=https://payments.null2null.com
PAYMENT_HUB_API_KEY=pay_live_REDACTED
PAYMENT_HUB_WEBHOOK_SECRET=REDACTED
PAYMENT_HUB_EXPECTED_PROJECT_ID=UUID_FROM_OPERATOR
PAYMENT_HUB_TIMEOUT_SECONDS=15
```

## 4. Authentication and scopes

Project endpoints use a bearer API key:

```http
Authorization: Bearer pay_live_REDACTED
```

Available scopes:

- `orders:write` for creating orders
- `orders:read` for querying orders
- `*` only for explicitly approved service integrations

The default application-level limit is 120 authenticated requests per minute for each API key. The public reverse proxy can apply additional limits per client IP.

## 5. Local order model

At minimum, store these fields in the project database:

| Field | Purpose |
| --- | --- |
| `merchant_order_id` | Project-generated unique order reference |
| `customer_id` | Internal account or customer reference |
| `product_id` | Payment Hub product ID |
| `payment_hub_order_id` | Payment Hub order UUID |
| `payment_state` | Last accepted Payment Hub state |
| `payment_state_version` | Monotonic version used for stale-event rejection |
| `idempotency_key` | Stable key used to create the Payment Hub order |
| `price_amount` and `price_currency` | Server-verified commercial snapshot |
| `payment_method_ids_digest` | Optional pinned digest for configured payment methods |
| `checkout_url` | Temporary hosted checkout URL |
| `entitlement_applied_at` | Idempotency marker for successful delivery |
| `refunded_at` | Refund marker and support audit field |

Create a unique database constraint on `merchant_order_id`, `payment_hub_order_id`, and each processed webhook `eventId`.

## 6. Create an order

Endpoint:

```text
POST /v1/orders
```

Required headers:

```http
Authorization: Bearer pay_live_REDACTED
Content-Type: application/json
Idempotency-Key: 0123456789abcdef0123456789abcdef
```

The idempotency key must contain 16 to 80 ASCII letters, digits, underscores, or hyphens. Generate it once and store it with the local order. Do not generate a new key during an automatic retry.

Request:

```json
{
  "productId": "premium_monthly",
  "merchantOrderId": "project-order-0001",
  "customerRef": "opaque-customer-0001",
  "distributionChannel": "web"
}
```

Field constraints:

- `productId`: 2 to 64 characters, starts with an ASCII letter or digit, then letters, digits, `_`, `.`, or `-`
- `merchantOrderId`: 1 to 128 characters, starts with an ASCII letter or digit, then letters, digits, `_`, `.`, `:`, or `-`
- `customerRef`: same syntax as `merchantOrderId`; use an opaque internal ID, not an email address
- `distributionChannel`: 2 to 40 lowercase letters, digits, or underscores, starting with a lowercase letter

Example request:

```sh
curl --request POST \
  --url https://payments.null2null.com/v1/orders \
  --header "Authorization: Bearer $PAYMENT_HUB_API_KEY" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: 0123456789abcdef0123456789abcdef" \
  --data '{
    "productId": "premium_monthly",
    "merchantOrderId": "project-order-0001",
    "customerRef": "opaque-customer-0001",
    "distributionChannel": "web"
  }'
```

The response contains an `order` object. Persist its ID, state, state version, price snapshot, payment method digest, checkout URL, and expiry before returning checkout information to the client.

```json
{
  "order": {
    "id": "58f93f31-0c02-4a13-9f9f-74d71fc014f1",
    "merchantOrderId": "project-order-0001",
    "customerRef": "opaque-customer-0001",
    "productId": "premium_monthly",
    "provider": "bitcart",
    "providerPaymentId": "provider-invoice-id",
    "state": "waiting",
    "stateVersion": 1,
    "priceAmount": 9.99,
    "priceCurrency": "USD",
    "paymentMethodIdsDigest": "sha256-hex-digest",
    "payAmount": 9.98,
    "payCurrency": "USDT_TRON",
    "checkoutUrl": "https://checkout.example/invoice/example",
    "paymentExpiresAt": "2026-08-25T12:20:00Z",
    "createdAt": "2026-08-25T12:00:00Z",
    "updatedAt": "2026-08-25T12:00:02Z"
  }
}
```

All values in this example are illustrative. Do not hardcode them.

## 7. Idempotency behavior

- Same API key project, same `Idempotency-Key`, same request: returns the existing order.
- Same API key project, same `Idempotency-Key`, different request: returns `409 idempotency_conflict`.
- Different idempotency key, same `merchantOrderId`: returns `409 merchant_order_conflict`.
- A timed-out create request must be retried with the original idempotency key.
- A client must not create a second local order merely because the first network request timed out.

## 8. Open hosted checkout

Return only the required display data and `checkoutUrl` to the client. The client opens the HTTPS hosted checkout in an approved browser or secure web view according to the platform policy.

The client must not decide that payment succeeded based on:

- A return URL
- A success page
- A closed browser
- A provider notification visible in the client
- A local timer

The project backend decides payment state from verified Payment Hub data.

## 9. Order states

| State | Terminal | Entitlement action |
| --- | --- | --- |
| `creating` | No | None |
| `waiting` | No | None |
| `partially_paid` | No | None |
| `processing` | No | None |
| `succeeded` | Yes for fulfillment | Grant once |
| `failed` | Yes | Do not grant |
| `expired` | Yes | Do not grant; a new order may be created |
| `refunded` | Yes | Apply documented revocation policy once |

`stateVersion` increases when the authoritative state changes. Ignore an incoming event if its state version is older than the version already stored for that Payment Hub order.

## 10. Receive outbound webhooks

Payment Hub sends these event types:

- `payment.succeeded`
- `payment.refunded`
- `payment.failed`
- `payment.expired`

Headers:

```http
Content-Type: application/json
X-Payment-Event-Id: EVENT_UUID
X-Payment-Timestamp: UNIX_SECONDS
X-Payment-Signature: v1=HEX_HMAC_SHA256
```

Payload shape:

```json
{
  "eventId": "cc24d888-d19e-4500-88d7-ed9a59f28752",
  "type": "payment.succeeded",
  "createdAt": "2026-08-25T12:05:00Z",
  "projectId": "PROJECT_UUID",
  "data": {
    "order": {
      "id": "58f93f31-0c02-4a13-9f9f-74d71fc014f1",
      "merchantOrderId": "project-order-0001",
      "customerRef": "opaque-customer-0001",
      "productId": "premium_monthly",
      "provider": "bitcart",
      "providerPaymentId": "provider-invoice-id",
      "state": "succeeded",
      "stateVersion": 2,
      "priceAmount": 9.99,
      "priceCurrency": "USD",
      "paymentMethodIdsDigest": "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa",
      "payAmount": 9.98,
      "payCurrency": "USDT_TRON",
      "checkoutUrl": "https://checkout.example/invoice/example",
      "paymentExpiresAt": "2026-08-25T12:20:00Z",
      "paidAt": "2026-08-25T12:04:40Z",
      "createdAt": "2026-08-25T12:00:00Z",
      "updatedAt": "2026-08-25T12:04:41Z"
    }
  }
}
```

The full order object follows the OpenAPI `PaymentOrder` schema. Example IDs, amounts, and timestamps are illustrative.

### Signature algorithm

Build the signed message from the exact raw request bytes decoded as UTF-8:

```text
timestamp + "." + eventId + "." + rawBody
```

Compute:

```text
expected = "v1=" + lowercase_hex(HMAC_SHA256(webhookSecret, signedMessage))
```

Compare `expected` and `X-Payment-Signature` in constant time.

### Verification sequence

1. Read and retain the raw request body.
2. Require all three signature headers.
3. Parse the timestamp as Unix seconds.
4. Reject timestamps outside the project's allowed replay window. Five minutes is the recommended default.
5. Compute the expected signature from the raw body.
6. Compare signatures in constant time.
7. Parse JSON only after signature verification.
8. Require payload `eventId` to equal `X-Payment-Event-Id`.
9. Require payload `projectId` to equal the provisioned project ID.
10. Insert the event ID into a table with a unique constraint. If it already exists, return HTTP 2xx without applying business changes again.
11. Match order ID, merchant order ID, customer reference, product, amount, currency, and optional payment method digest with the local order.
12. Re-query the Payment Hub order when the event would grant or revoke valuable entitlement.
13. Update the local order and entitlement in one database transaction.
14. Return HTTP 2xx only after durable processing, or after durably enqueueing the event for internal processing.

Node.js verification example:

```js
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyPaymentWebhook(rawBody, headers, secret) {
  const eventId = headers["x-payment-event-id"];
  const timestamp = headers["x-payment-timestamp"];
  const received = headers["x-payment-signature"];
  if (!eventId || !timestamp || !received) return false;

  const signed = `${timestamp}.${eventId}.${rawBody}`;
  const expected = `v1=${createHmac("sha256", secret)
    .update(signed, "utf8")
    .digest("hex")}`;
  const actualBytes = Buffer.from(received, "utf8");
  const expectedBytes = Buffer.from(expected, "utf8");
  return actualBytes.length === expectedBytes.length
    && timingSafeEqual(actualBytes, expectedBytes);
}
```

Framework warning: configure the webhook route to expose the raw body. A JSON body parser that consumes and re-serializes the request before verification will break signatures.

## 11. Query and reconcile

Query by Payment Hub order ID:

```http
GET /v1/orders/{orderId}?refresh=true
Authorization: Bearer pay_live_REDACTED
```

Query by project merchant order ID:

```http
GET /v1/orders/by-merchant-order/{merchantOrderId}?refresh=true
Authorization: Bearer pay_live_REDACTED
```

`refresh` defaults to `true`. Payment Hub refreshes non-terminal or recently successful orders from the authenticated provider API when needed. If a provider is temporarily unavailable, Payment Hub returns the last authenticated stored state when possible.

Recommended project recovery loop:

1. Select local payment orders that are not terminal and have not been checked recently.
2. Query by `merchantOrderId` with `refresh=true`.
3. Validate the returned order identity and commercial fields.
4. Apply a newer state transactionally.
5. Use exponential backoff with jitter on transport errors, HTTP 429, and HTTP 5xx.
6. Alert support when an order remains unresolved beyond the configured payment expiry and reconciliation window.

## 12. Error contract

```json
{
  "timestamp": "2026-08-25T12:00:00Z",
  "status": 401,
  "code": "authentication_required",
  "message": "Bearer authentication is required",
  "path": "/v1/orders"
}
```

Branch on `code`, not `message`.

Important codes:

| HTTP | Code | Required handling |
| --- | --- | --- |
| 400 | `invalid_request` | Fix request validation |
| 400 | `idempotency_key_invalid` | Generate a compliant stable key |
| 400 | `merchant_order_invalid` | Fix the local order reference |
| 401 | `authentication_required` | Add bearer authentication |
| 401 | `invalid_api_key` | Rotate or correct the project key |
| 403 | `insufficient_scope` | Use a key with the required scope |
| 403 | `checkout_channel_not_allowed` | Do not bypass the channel policy |
| 404 | `product_not_found` | Check project and environment configuration |
| 404 | `payment_order_not_found` | Check order ownership and identifier |
| 409 | `idempotency_conflict` | Reuse a key only with the original request |
| 409 | `merchant_order_conflict` | Reconcile the existing local order |
| 429 | `rate_limited` | Retry with exponential backoff and jitter |
| 502 | `provider_unavailable` | Preserve pending state and retry |
| 502 | `provider_response_invalid` | Preserve state and alert if repeated |
| 502 | `provider_identity_mismatch` | Stop fulfillment and alert immediately |
| 503 | `provider_account_unavailable` | Stop new checkout and alert operations |
| 500 | `internal_error` | Preserve state and retry safely |

## 13. Distribution channel policy

External checkout may be configured for approved direct-sale channels such as:

- `web`
- `standard_android`
- `windows`
- `macos`
- `linux`

Do not expose Payment Hub checkout inside an App Store iOS build or Google Play Android build. Those builds must use Apple In-App Purchase or Google Play Billing according to store policy. The Payment Hub operator must reject external-checkout product routes for store-distributed channel names.

## 14. Provider independence

Project code integrates with the Payment Hub order contract, not a specific provider contract. Do not branch project entitlement logic on `provider` unless an audited business requirement exists.

Adding a provider requires a server-side adapter that validates:

- Provider account identity
- Order identity
- Requested amount and currency
- Allowed payment method set
- Authenticated terminal state
- Callback replay behavior
- Refund behavior
- Reconciliation behavior
- Hosted checkout handling for card providers

Provider callbacks are wake-up hints only. Payment Hub always re-queries the authenticated provider API before accepting a state transition.

## 15. Production acceptance tests

Do not enable real checkout until all applicable cases pass and evidence is retained:

1. Valid order creation
2. Same-key idempotent retry
3. Same-key conflicting request
4. Duplicate merchant order ID
5. Successful payment
6. Partial or underpayment
7. Processing delay
8. Expired payment
9. Provider failure
10. Duplicate provider callback
11. Forged provider callback
12. Valid project webhook signature
13. Forged project webhook signature
14. Expired project webhook timestamp
15. Duplicate project event
16. Project callback outage and later retry recovery
17. Active reconciliation without a callback
18. Refund and entitlement revocation
19. Provider API outage without false fulfillment
20. Low-value real payment into the project's independent receiving account

## 16. Implementation completion criteria for coding AI

A coding AI must not claim integration is complete until it has produced and verified:

- A server-only Payment Hub client
- Secret configuration with no client exposure
- A durable local order model
- Create-order idempotency
- Hosted-checkout handoff
- Raw-body webhook verification
- Timestamp and replay protection
- Event and entitlement idempotency
- Order identity and amount validation
- Active reconciliation
- Refund handling
- Channel restrictions
- Automated tests for success, duplicate delivery, signature failure, conflict, and retry
- An operations runbook for key rotation, provider outage, reconciliation, and refund support

Recommended coding AI prompt:

```text
Read https://payments.null2null.com/docs/integration.md and
https://payments.null2null.com/openapi.json completely.
Implement a server-side Payment Hub integration for this project.
Keep the API key out of every client. Create a local order first.
Use stable idempotency keys, verify webhook signatures from the raw body,
re-query valuable terminal events, and grant entitlement only for succeeded.
Make fulfillment and refund handling transactional and idempotent.
Add recovery reconciliation and tests before reporting completion.
```
