Skip to navigation

Wallet Pay Funding

View as Markdown

NinjaTrader Connect supports deposits funded with Apple Pay or Google Pay. The deposit does not save a card. For deposits from a saved card, see Debit-card funding.

Apple Pay can only be tested on Apple devices. Neither Apple Pay nor Google Pay can work end to end with Stripe sandbox (test cards) in NinjaTrader.
These endpoints are Live-only. Do not send them to the Demo base URL.

Prerequisite: register your hostnames with NinjaTrader

Before implementing or testing Apple Pay or Google Pay, provide NinjaTrader with every hostname that will host the integration so it can be registered as a Stripe Payment Method Domain. Submit hostnames only, without https://, paths, or ports. Wait for confirmation that the domains are registered and that Apple Pay and Google Pay are active before continuing. Each preview or temporary hostname used for testing must also be registered.

This requirement is separate from the Live-only warning above. The hostname must be registered before you initialize or test a wallet. NinjaTrader’s funding endpoints are Live-only and must not be sent to the Demo API base URL.

Stripe shows Apple Pay only when its requirements are met, including an active payment-method domain, a supported browser and device, a configured wallet, and a supported currency and region. See Stripe’s Express Checkout Element and Payment Method Domains documentation.

For end-to-end testing, serve the integration from one of the registered HTTPS hostnames, and test on every supported target device and browser.

Prerequisites

Your integration needs:

  • a bearer access token for the user
  • the user’s eligible live accountId and the USD currencyId
  • Stripe.js or an applicable Stripe client integration with Apple Pay or Google Pay available on the user’s device and browser

Complete the funding disclosures

Before the wallet flow, the user must review and sign both funding disclosures:

  1. Payment Processor Disclosure and Authorization
  2. Form of Risk Disclosure and Customer Authorization

Both disclosures must be signed, but the signing order is not enforced. Previously signed copies satisfy the requirement. startStripeFunding rejects requests when either disclosure is missing. Use the published signUserDocument operation by following Funding disclosures. If you prefer a hosted signing and funding flow, see Embedding the Funding Page with an iframe.

Get the Stripe configuration

Call getStripeConfiguration to obtain the Stripe publishable key. Never send Stripe secret keys to the browser.

Initialize Stripe.js

Call loadStripe(publishableKey) with the key from getStripeConfiguration. It can return null, so check the result before you continue.

Check eligibility and limits

Call checkStripeFundingEligibility before the wallet flow:

{
"accountId": 123456,
"currencyId": 1
}

Use the returned minimumAmount and maximumAmount for the amount UI. Do not hard-code them. To validate a specific deposit, repeat the request with amount.

Stop when errorText is non-empty. While a deposit is still Pending, Suspended, Locked, or InTransit, a check without amount returns a maximumAmount lowered by that deposit’s amount, and an amount-specific check that is otherwise within limits returns a non-empty errorText and no maximumAmount. Reconcile that deposit and wait for it to settle instead of retrying.

Wallet funding flow

The wallet flow does not create a saved card and does not use a NinjaTrader funding-method ID. NinjaTrader performs the server-side step with the ConfirmationToken, so you don’t create or confirm a PaymentIntent. Stripe’s documentation covers the client-side details: see the Stripe Express Checkout Element and the Stripe ConfirmationToken flow.

  1. The customer selects Apple Pay or Google Pay in Stripe’s UI, and Stripe produces a ConfirmationToken. NinjaTrader calls this value stripeConfirmationToken. If Stripe doesn’t produce a token, nothing was sent to NinjaTrader, so don’t call it.
  2. Once you have the token, call startStripeFunding as described in Submit the NinjaTrader funding request.

Submit the NinjaTrader funding request

Before you send the request, capture the baseline described in Reconcile the funding outcome.

Guard against duplicates

Your client keeps two in-memory guards:

  • confirmationInFlight prevents concurrent handling while the token is being created.
  • fundingRequestSent becomes true for the life of that flow after your durable marker is written and confirmed, and immediately before you call startStripeFunding. Don’t reset it. Create new guards with a new flow instead.

The order is fixed: once you have the ConfirmationToken, write your marker and wait until the write is confirmed, set fundingRequestSent to true, and only then call startStripeFunding.

If the marker write fails or can’t be confirmed, don’t send the request. Fail the Stripe interaction (the example does this in its catch block), and tell the user to retry or use the NinjaTrader-hosted funding flow. Nothing was sent to NinjaTrader and the token is unused.

A ConfirmationToken is single-use client output. Pass it to NinjaTrader immediately. Never log it, persist it, place it in a URL, resend it, or use it in a different funding request. Creating a ConfirmationToken doesn’t prove that funding succeeded.

Send the request

Call startStripeFunding with stripeConfirmationToken set to the token ID, flowType set to WalletFunding, and amount in major units:

{
"accountId": 123456,
"amount": 250,
"currencyId": 1,
"stripeConfirmationToken": "ctoken_example",
"flowType": "WalletFunding"
}

Convert the amount

startStripeFunding takes amount in major units (250 for $250.00), while Stripe uses minor units (25000).

  • Reject amounts with more than two decimal places.
  • Round when you derive one amount from the other, as the example does.
  • Send NinjaTrader the same amount you gave Stripe. If it changes before any request is sent, update it in Stripe and capture a fresh baseline, or start a new flow. After fundingRequestSent is true, it can’t change.

Handle the response

While the flow is locked (fundingRequestSent is true), keep the wallet actions unavailable and show your own “request in progress, check status” state.

  • Error, empty, missing, or timed out: the deposit may have been submitted. Keep the flow locked, don’t resend the token or send a second request, and reconcile.
  • HTTP 429 or p-ticket: stop, don’t retry with the ticket, and discard the old token.
  • Anything else: a response never proves that NinjaTrader created the deposit. Follow Reconcile the funding outcome for the outcome, rate limits and CAPTCHA, and the durable marker.

Example

The following example shows the flow in a single block. It omits UI state, error display, the FundTransaction baseline and reconciliation, penalty-ticket handling, and your server-side marker implementation, which Submit the NinjaTrader funding request and Reconcile the funding outcome describe. Write and confirm the marker between creating the token and setting fundingRequestSent.

import { loadStripe } from "@stripe/stripe-js";
const stripe = await loadStripe(publishableKey);
if (!stripe) {
throw new Error("Stripe.js did not initialize.");
}
const amount = 250; // NinjaTrader amount, major units
const amountInCents = Math.round(amount * 100); // Stripe Elements amount, minor units
const elements = stripe.elements({
mode: "payment",
amount: amountInCents,
currency: "usd",
});
const expressCheckout = elements.create("expressCheckout", {
paymentMethodOrder: ["apple_pay", "google_pay"],
});
// Show or enable a wallet action only while Stripe reports that wallet available.
function setWalletAvailable(wallet, available) {
// Update your UI.
}
expressCheckout.on("ready", ({ availablePaymentMethods }) => {
setWalletAvailable("applePay", Boolean(availablePaymentMethods?.applePay));
setWalletAvailable("googlePay", Boolean(availablePaymentMethods?.googlePay));
});
expressCheckout.on("availablepaymentmethodschange", ({ paymentMethods }) => {
setWalletAvailable("applePay", Boolean(paymentMethods?.applePay?.available));
setWalletAvailable("googlePay", Boolean(paymentMethods?.googlePay?.available));
});
expressCheckout.on("loaderror", () => {
setWalletAvailable("applePay", false);
setWalletAvailable("googlePay", false);
});
let confirmationInFlight = false;
let fundingRequestSent = false;
expressCheckout.on("confirm", async (event) => {
if (confirmationInFlight || fundingRequestSent) {
return;
}
if (event.expressPaymentType !== "apple_pay" && event.expressPaymentType !== "google_pay") {
event.paymentFailed({ reason: "fail" });
return;
}
confirmationInFlight = true;
try {
const submitted = await elements.submit();
if (submitted.error) {
confirmationInFlight = false;
event.paymentFailed({
reason: "invalid_payment_data",
message: submitted.error.message,
});
return;
}
const result = await stripe.createConfirmationToken({ elements });
if (result.error || !result.confirmationToken) {
confirmationInFlight = false;
event.paymentFailed({ reason: "fail" });
return;
}
// Your marker write goes here (omitted). If it fails, throw before setting
// fundingRequestSent so nothing is sent.
fundingRequestSent = true;
// Your function that calls NinjaTrader's startStripeFunding endpoint.
await startStripeFunding({
accountId: 123456,
amount,
currencyId: 1,
stripeConfirmationToken: result.confirmationToken.id,
flowType: "WalletFunding",
});
// The response does not prove NinjaTrader created the deposit: inspect
// errorText and p-ticket, then reconcile with FundTransaction records.
} catch (error) {
// The error is intentionally not logged here.
if (!fundingRequestSent) {
confirmationInFlight = false;
event.paymentFailed({ reason: "fail" });
}
// If the funding request might have been sent, keep the flow locked and
// reconcile against FundTransaction records.
}
});
// Start unavailable until Stripe reports a wallet available.
setWalletAvailable("applePay", false);
setWalletAvailable("googlePay", false);
// Mount Express Checkout into your page container.
expressCheckout.mount("#express-checkout");

Next steps