Skip to main content

Changelog & Migration Notes

A short, action-oriented summary of recent API behavior changes. Read the Action required sections if you have an existing integration — the rest is informational.

TL;DR

  • The system-internal reference field is no longer returned on payment detail / list responses. Read clientReference instead. Webhooks still send both keys (same value) for backward compatibility.
  • The paidAt field is now omitted from the verify response when the payment hasn’t been received yet (previously: empty string).
  • The hosted-checkout redirect URL now always includes both paymentId (a real, verifiable payment ID) and reference (your clientReference).
  • Underpayment protection on testnets now matches mainnet behavior — underpaid testnet transactions reach you as failed with a populated deficitAmount instead of auto-completing.

Breaking changes

1. reference removed from payment detail and list responses

Internally we have two reference values per payment:
  • clientReference — the value you supplied (or one we generated for you if you didn’t pass one). Stable. Never mutated.
  • reference (system-internal) — used as the idempotency key with our exchange providers. May be regenerated mid-lifecycle if an exchange order expires.
Exposing the system-internal reference to merchants was misleading because it could change. We’ve removed it from outbound API surfaces. Affected endpoints:
  • GET /api/v1/payments/:paymentIdreference is no longer in the response.
  • GET /api/v1/payments (list) — each row no longer includes reference.
Action required Anywhere you read payment.reference from these responses, switch to payment.clientReference. The value is the same as what you passed (or the auto-generated one we use if you didn’t pass one).
- const ref = payment.reference;
+ const ref = payment.clientReference;

2. paidAt is omitted (not empty) when unpaid

GET /api/v1/payments/:paymentId/verify previously returned "paidAt": "" when the payment hadn’t been received. It now omits the field entirely. Action required If you parse paidAt directly as an ISO timestamp, handle the missing case:
- const ts = new Date(response.paidAt);
+ const ts = response.paidAt ? new Date(response.paidAt) : null;

3. List Payments response shape

The GET /api/v1/payments (list) response was previously documented as { items, page, pageSize, totalItems, totalPages }. The actual response shape is { data, currentPage, pageSize, totalItems, totalPages, hasNext, hasPrev }. If you coded to the documented shape, your integration was already broken — switch to the actual shape. If you coded to the actual response, nothing to do.
- response.data.items.forEach(...)
- response.data.page
+ response.data.data.forEach(...)
+ response.data.currentPage
See the updated List Payments docs.

Improvements (no action required)

Hosted-checkout redirect now always carries useful query params

When the hosted checkout redirects the customer back to your callbackURL (success) or failureURL (failure), it now reliably appends:
  • paymentId — the real ChainPal payment ID (a 24-character ObjectID hex string). You can pass this directly to GET /api/v1/payments/:paymentId/verify.
  • reference — your clientReference.
Previously: paymentId could be the 12-character payCode (which the verify endpoint rejects), and reference was sometimes empty if you hadn’t passed one at initialization. Now: both are guaranteed populated. If you weren’t using these query params before, you can rely on them now.

clientReference is now always populated

If you don’t supply a reference when calling POST /api/v1/payments, ChainPal generates one for you and stores it as the payment’s clientReference. You’ll see this same value in:
  • The clientReference field of every API response that surfaces it.
  • The reference and clientReference keys in webhook payloads.
  • The reference query param appended to your callbackURL / failureURL redirects.
This means your integrations can rely on clientReference as the canonical, stable, always-present payment identifier without having to fall back to paymentId for unsupplied-reference cases.

failureURL is now exposed in the checkout DTO

If you set failureURL on payment initialization, the checkout page can now read it back through the internal API. This means failure redirects will actually fire — previously the field was ignored on the checkout page even when you set it.

Webhooks: reference and clientReference carry the same value

Both keys in the webhook payload are now populated from your clientReference. The reference key is kept as a legacy alias — new integrations should prefer clientReference, but no urgent migration is needed.

Test and live keys can now be used side-by-side

Previously a merchant account had a single “active” environment and the API rejected requests sent with a key whose prefix didn’t match. That gate is gone. Each request is routed by the prefix on the key you send (cp_*_test_* → test, cp_*_live_* → live), so you can run a staging integration and a live integration concurrently without flipping a switch. The deprecated POST /users/public-api/toggle-environment endpoint now returns 410 Gone. See the Authentication page for the full picture.

Disabling the API in your dashboard preserves your keys

If a merchant disables the developer API in the dashboard, requests with existing keys start returning 401 Unauthorized (as expected), but the keys themselves are kept on file. Re-enabling resumes service without forcing a key rotation. Your integration code does not need to handle a “keys destroyed” case any more.

Testnet underpayment behavior now matches mainnet

Mainnet payments have always rejected materially-underpaid transactions via the exchange-provider settlement step. On testnets, the settlement step is skipped (no exchange provider in test mode), so underpaid testnet transactions previously auto-completed. Testnet now applies the same underpayment check as mainnet: an underpaid transaction is marked status: failed with a populated deficitAmount. If your test integration relied on the old behavior of “any non-zero crypto counts as paid”, update it to handle the realistic failed/deficitAmount case — this matches what your live integration would have seen all along.

Quick checklist for existing integrations

  • Replace any reads of payment.reference with payment.clientReference on GET /payments/:id and GET /payments.
  • Treat the verify endpoint’s paidAt as optional — guard before parsing as a date.
  • If you parsed list responses as { items, page }, switch to { data, currentPage }.
  • (Optional) Start using clientReference as your canonical payment identifier in webhook handlers and redirect-callback handlers.
If anything in your integration breaks, reach out at help@chainpal.xyz.