Skip to content

Add checkout to the channel merchant experience

The channel builds and operates the checkout page for its merchant. The merchant does not mount anything and never receives the channel key. The channel’s server creates the scoped session, and the page receives only its session identifier.

Mount checkout inside the channel-managed merchant page on an allowed origin, initialize it on every page load, and treat approval, cancellation, and error callbacks as interface signals. Keep merchant attribution with the session and reconcile final state on the server. Look up the checked browser contract in the shared maxanapay.js reference.

Point the page at the same base your server calls: the base URL issued with your credential, the one step one had you store from the activation screen. Pass it as apiBase. The one shorthand is environment, which resolves the two published hosts on Environments and nothing else: when your credential’s base is the live host, environment: 'live' names it and apiBase is not needed. Every other base — including the one a test credential is issued on, which is not a host environment can name — is reached only through apiBase, so that is the option to reach for while you build against a test credential. apiBase takes precedence when both are supplied. Supplying neither throws config_error before anything mounts, and a mount pointed at the wrong deployment fails on the session instead of on the option, because the session it is looking for does not exist there.

Mount the session your server created, and post the capture identifier back to your own server

// The channel's own page mounts the session its server created. The page
// receives the session identifier and nothing else; the channel key stays on
// the server.
const channelCheckoutScript = document.createElement('script');
channelCheckoutScript.src = 'https://js.maxanapay.com/v1/maxanapay.js';
channelCheckoutScript.addEventListener('load', () => {
  const checkout = MaxanaPay.checkout({
    sessionId: 'cs_00000000000000000000000000000000',
    container: '#maxana-checkout',
    // Which deployment this page calls: the base URL issued with your
    // credential — the one on your activation screen, the same base your server
    // calls. Substitute yours below. When that base is the published live host,
    // `environment: 'live'` is the shorthand and apiBase is not needed;
    // otherwise pass apiBase, which is the case while you build against a test
    // credential.
    apiBase: 'https://staging.api.maxanapay.com',
    onApproved(payment) {
      // payment.captureId is the value POST /api/payments/captures/{capture_id}/refund takes.
      // It is null until payment.status is 'captured'.
      void fetch('/internal/orders/order_1001/paid', {
        method: 'POST',
        headers: { 'Content-Type': 'application/json' },
        body: JSON.stringify({
          orderId: payment.orderId,
          captureId: payment.captureId,
          status: payment.status,
          source: payment.source,
        }),
      });
    },
    onError(error) {
      console.error(error.code, error.retryable);
    },
  });

  window.addEventListener('pagehide', () => checkout.unmount());
});
document.head.append(channelCheckoutScript);

The page above holds the session identifier and no credential. It has to be served from one of the allowed_origins the session was created with, or the SDK reports origin_not_allowed instead of mounting.

onApproved receives one object, and it is the only place the browser learns the identifier a refund needs. payment.captureId is that value: it is the same provider_capture_id the transaction carries and the same capture_id that POST /api/payments/captures/{capture_id}/refund takes in its path. It is null until payment.status is captured, so read the status before storing it.

The complete field list is on the shared maxanapay.js reference. Treat what arrives there as interface feedback and confirm the payment from payment.captured and the transaction read; the callback tells you what to show the buyer, not what to bill.

When the buyer approved and no capture followed

Section titled “When the buyer approved and no capture followed”

An approved order that was never captured does not reach you through onApproved. The SDK attempts the capture itself once the buyer authorizes, and when PayPal returns the order still merely approved it reports this as an onError with code capture_pending and stops before onApproved — so onApproved only ever runs for a payment that was actually captured. The buyer authorized at PayPal and the money was not taken; nothing is owed to you yet, so do not fulfil on capture_pending.

The channel recovers that payment from its own server. POST /api/payments/orders/{order_id}/capture accepts a channel key paired with X-Merchant-Id naming the merchant the order belongs to, and captures the approved order for that merchant and for no other. The same operation still accepts a merchant’s own key, and still accepts no credential at all — the buyer page’s form, which on top of the checkout headers needs the browser’s own Origin header, without which it answers 403 and Browser Origin header is required for public checkout-session requests. The operation reference lists Origin as optional because a call carrying a merchant or channel key needs none; the credential-less call does.

Recovery is safe to retry. Re-capturing an order that is already captured returns the existing capture — 200, no second charge — and a terminal order, one that has failed, refunded, or voided, is refused 409. Because the channel call is authenticated, a checkout session that has since expired does not block it: session expiry refuses only the anonymous buyer path. Confirm what happened from payment.captured and from GET /api/transactions for that merchant, and treat the SDK callback as interface feedback rather than as the record. Letting the buyer approve again also still works — a payment that was not taken does not close the session — but the server-side capture is the recovery that does not depend on the buyer coming back.

The published maxanapay.js major channel mounts the PayPal wallet rail, and that is the whole of the channel integration. The channel builds and operates that checkout for its merchant; the merchant mounts nothing and holds no credential of its own.

The checked v1 SDK contract excludes card fields, 3-D Secure orchestration, Apple Pay, Google Pay, Pay Later messaging, App Switch, SDK-controlled shipping and billing-address collection, save-payment-method sessions, and sessions routed to Gr4vy or NMI. Pay Later buttons are separate from Pay Later messaging: an eligible button can appear, while promotional messaging remains excluded.

Continue to Refunds, statements, and fees.