Payment Flow Launchpad
Flows built here can run locally (Stripe) or be exported to the NoblePort Payment Gateway (Stripe + PayPal) that serves all NoblePort dapps. Direct mode uses this app's own live Stripe integration; Gateway mode produces a deployment package — a JSON flow definition plus a client snippet that calls POST https://gateway.nobleport.app/v1/checkout.
Gateway target
Gateway mode: nothing executes here. The builder emits a deployment package for the central NoblePort Payment Gateway.
Processor
Staged — connect PayPal credentials in the NoblePort Payment Gateway. PayPal is generated as exportable configuration only — order create + capture for one-time payments, a billing plan for subscriptions, and a partial capture with balance due for deposits. No PayPal transaction is executed by this app.
Flow configuration
Each currency exports its own amount. Stripe gets currency_options; PayPal gets one order/plan per currency. Leave an amount blank to reuse the base amount.
Deployment package
- additionalCurrencies PayPal cannot mix currencies in one plan — the export generates one per currency.
Copy or download the JSON definition and the client snippet below, then deploy them to the NoblePort Payment Gateway. The gateway holds the Stripe and PayPal credentials for every NoblePort dapp.
Flow definition (JSON)
Schema changelog (embedded in every export)
- v1.3.0 · 2026-08-01
- Added `schema` block with schema/builder versions, changelog and template provenance.
- v1.2.0 · 2026-07-01
- Added multi-currency `pricing[]` with per-currency minor units and PayPal support flags.
- Stripe exports use `currency_options`; PayPal exports one order/plan per currency.
- v1.1.0 · 2026-06-01
- Added `webhooks` configuration block and signed handler snippet generation.
- v1.0.0 · 2026-05-01
- Initial payment_flow document: one-time, subscription and deposit flows.
{
"version": "1.0",
"kind": "nobleport.payment_flow",
"id": "flow_sea_esta_host_membership",
"name": "Host Membership",
"type": "subscription",
"processor": "both",
"target": "gateway",
"amount": {
"value": 499,
"minor_units": 49900,
"currency": "USD"
},
"charge_now": {
"value": 499,
"minor_units": 49900,
"currency": "USD"
},
"currencies": [
"USD",
"EUR"
],
"pricing": [
{
"currency": "USD",
"decimals": 2,
"amount": {
"value": 499,
"minor_units": 49900,
"display": "499.00 USD"
},
"charge_now": {
"value": 499,
"minor_units": 49900,
"display": "499.00 USD"
},
"paypal_supported": true
},
{
"currency": "EUR",
"decimals": 2,
"amount": {
"value": 459,
"minor_units": 45900,
"display": "459.00 EUR"
},
"charge_now": {
"value": 459,
"minor_units": 45900,
"display": "459.00 EUR"
},
"paypal_supported": true
}
],
"schedule": {
"interval": "year",
"interval_count": 1
},
"urls": {
"success_url": "https://seaesta.lovable.app/checkout/return",
"cancel_url": "https://seaesta.lovable.app/"
},
"metadata": {
"product": "host_membership",
"dapp": "Sea-Esta",
"platform": "NoblePort",
"currencies": "USD,EUR"
},
"processors": {
"stripe": {
"mode": "subscription",
"ui_mode": "embedded_page",
"currency": "usd",
"currencies": [
"USD",
"EUR"
],
"line_items": [
{
"quantity": 1,
"price_data": {
"currency": "usd",
"unit_amount": 49900,
"product_data": {
"name": "Host Membership"
},
"recurring": {
"interval": "year"
},
"currency_options": {
"eur": {
"unit_amount": 45900
}
}
}
}
],
"return_url": "https://seaesta.lovable.app/checkout/return",
"cancel_url": "https://seaesta.lovable.app/",
"metadata": {
"product": "host_membership",
"dapp": "Sea-Esta",
"platform": "NoblePort",
"flow_type": "subscription",
"currencies": "USD,EUR"
},
"execution": "live"
},
"paypal": {
"intent": "SUBSCRIPTION",
"execution": "staged",
"execution_note": "Staged — connect PayPal credentials in the NoblePort Payment Gateway",
"currencies": [
"USD",
"EUR"
],
"plan": {
"product_id": "PROD-HOST_MEMBERSHIP",
"name": "Host Membership (USD)",
"status": "ACTIVE",
"billing_cycles": [
{
"frequency": {
"interval_unit": "YEAR",
"interval_count": 1
},
"tenure_type": "REGULAR",
"sequence": 1,
"total_cycles": 0,
"pricing_scheme": {
"fixed_price": {
"value": "499.00",
"currency_code": "USD"
}
}
}
],
"payment_preferences": {
"auto_bill_outstanding": true,
"setup_fee_failure_action": "CANCEL",
"payment_failure_threshold": 2
}
},
"by_currency": {
"USD": {
"product_id": "PROD-HOST_MEMBERSHIP",
"name": "Host Membership (USD)",
"status": "ACTIVE",
"billing_cycles": [
{
"frequency": {
"interval_unit": "YEAR",
"interval_count": 1
},
"tenure_type": "REGULAR",
"sequence": 1,
"total_cycles": 0,
"pricing_scheme": {
"fixed_price": {
"value": "499.00",
"currency_code": "USD"
}
}
}
],
"payment_preferences": {
"auto_bill_outstanding": true,
"setup_fee_failure_action": "CANCEL",
"payment_failure_threshold": 2
}
},
"EUR": {
"product_id": "PROD-HOST_MEMBERSHIP",
"name": "Host Membership (EUR)",
"status": "ACTIVE",
"billing_cycles": [
{
"frequency": {
"interval_unit": "YEAR",
"interval_count": 1
},
"tenure_type": "REGULAR",
"sequence": 1,
"total_cycles": 0,
"pricing_scheme": {
"fixed_price": {
"value": "459.00",
"currency_code": "EUR"
}
}
}
],
"payment_preferences": {
"auto_bill_outstanding": true,
"setup_fee_failure_action": "CANCEL",
"payment_failure_threshold": 2
}
}
},
"application_context": {
"brand_name": "Sea-Esta",
"user_action": "SUBSCRIBE_NOW",
"return_url": "https://seaesta.lovable.app/checkout/return",
"cancel_url": "https://seaesta.lovable.app/"
}
}
},
"webhooks": {
"endpoint": "https://seaesta.lovable.app/api/public/gateway/events",
"events": [
"checkout.completed",
"checkout.failed",
"checkout.canceled",
"subscription.activated",
"subscription.renewed",
"subscription.canceled",
"payment.refunded"
],
"signature": {
"algorithm": "hmac-sha256",
"header": "x-nobleport-signature",
"timestamp_header": "x-nobleport-timestamp",
"signed_payload": "{timestamp}.{raw_body}",
"tolerance_seconds": 300,
"secret_name": "NOBLEPORT_GATEWAY_WEBHOOK_SECRET"
},
"retry": {
"max_attempts": 5,
"backoff": "exponential"
},
"idempotency": {
"key": "event_id",
"scope": "flow"
},
"handling": "ui_status_only",
"note": "Staged — the gateway endpoint only records events and updates flow UI status. It never touches Stripe billing state, which stays owned by the existing Stripe webhook."
},
"generated_at": "2026-08-03T16:43:02.793Z",
"schema": {
"schema_version": "1.3.0",
"builder_version": "1.3.0",
"kind_version": "1.0",
"released_at": "2026-08-01",
"source": "gateway_launchpad",
"changelog": [
{
"version": "1.3.0",
"date": "2026-08-01",
"changes": [
"Added `schema` block with schema/builder versions, changelog and template provenance."
]
},
{
"version": "1.2.0",
"date": "2026-07-01",
"changes": [
"Added multi-currency `pricing[]` with per-currency minor units and PayPal support flags.",
"Stripe exports use `currency_options`; PayPal exports one order/plan per currency."
]
},
{
"version": "1.1.0",
"date": "2026-06-01",
"changes": [
"Added `webhooks` configuration block and signed handler snippet generation."
]
},
{
"version": "1.0.0",
"date": "2026-05-01",
"changes": [
"Initial payment_flow document: one-time, subscription and deposit flows."
]
}
]
}
}Gateway API snippet
// Sea-Esta → NoblePort Payment Gateway
// POST https://gateway.nobleport.app/v1/checkout with your app API key.
// Flow: flow_sea_esta_host_membership (Subscription, Both)
// Currencies: USD, EUR
// Schema 1.3.0 · builder 1.3.0 · generated 2026-08-03T16:43:02.793Z
const GATEWAY_URL = "https://gateway.nobleport.app/v1/checkout";
const APP_API_KEY = process.env.NOBLEPORT_APP_API_KEY!; // server-side only
// Charge-now amounts in minor units, per presentment currency.
export const PRICES = {
USD: { amount: 49900, currency: "USD" },
EUR: { amount: 45900, currency: "EUR" },
} as const;
export type FlowCurrency = keyof typeof PRICES;
export const DEFAULT_CURRENCY: FlowCurrency = "USD";
export async function startCheckout(
opts: { customerEmail?: string; processor?: "stripe" | "paypal"; currency?: FlowCurrency } = {},
) {
const price = PRICES[opts.currency ?? DEFAULT_CURRENCY];
if (!price) throw new Error(`Unsupported currency: ${opts.currency}`);
const res = await fetch(GATEWAY_URL, {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": `Bearer ${APP_API_KEY}`,
"X-NoblePort-Dapp": "Sea-Esta",
},
body: JSON.stringify({
flow_id: "flow_sea_esta_host_membership",
processor: opts.processor ?? "stripe",
amount: price.amount, // minor units for the selected currency
currency: price.currency,
type: "subscription",
schedule: { interval: "year", interval_count: 1 },
success_url: "https://seaesta.lovable.app/checkout/return",
cancel_url: "https://seaesta.lovable.app/",
customer_email: opts.customerEmail,
metadata: {"product":"host_membership","dapp":"Sea-Esta","platform":"NoblePort","currencies":"USD,EUR"},
}),
});
if (!res.ok) throw new Error(`Gateway error ${res.status}: ${await res.text()}`);
// { checkout_url, client_secret?, session_id, processor }
return res.json();
}
// NOTE: PayPal leg is "Staged — connect PayPal credentials in the NoblePort Payment Gateway".
// The gateway executes it once PayPal credentials are connected there.
Example templates
One worked example for every flow type × processor × mode combination. Download the JSON definition or the client snippet, or load a template into the builder above to tweak it. PayPal blocks stay staged configuration until credentials are connected in the gateway.
Webhook configuration
Staged — the gateway endpoint only records events and updates flow UI status. It never touches Stripe billing state, which stays owned by the existing Stripe webhook.
- Register the endpoint in the NoblePort GatewayIn the gateway dashboard for “Sea-Esta”, add a webhook destination pointing at https://seaesta.lovable.app/api/public/gateway/events (POST, JSON).
- Subscribe to this flow's eventscheckout.completed, checkout.failed, checkout.canceled, subscription.activated, subscription.renewed, subscription.canceled, payment.refunded
- Generate a signing secret and store it as NOBLEPORT_GATEWAY_WEBHOOK_SECRETThe gateway signs HMAC-SHA256 over "{timestamp}.{raw_body}" and sends it in x-nobleport-signature, with the unix timestamp in x-nobleport-timestamp. Add the same secret to this project's backend secrets.
- Scope the handler to UI status onlyThe bundled handler verifies the signature, de-duplicates by event_id and records status for the UI. Stripe billing state stays owned by the existing Stripe webhook at /api/public/payments/webhook — do not write membership entitlements from gateway events.
- Send a test event and confirm a 200Use the gateway's “Send test event” action with checkout.completed. A 401 means the secret differs; a 400 means the timestamp is outside the 5-minute tolerance.
// Sea-Esta → NoblePort Gateway webhook receiver (PLACEHOLDER)
// Route: POST /api/public/gateway/events Flow: flow_sea_esta_host_membership
//
// Staged — the gateway endpoint only records events and updates flow UI status. It never touches Stripe billing state, which stays owned by the existing Stripe webhook.
import { createFileRoute } from "@tanstack/react-router";
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 300;
const HANDLED_EVENTS = [
"checkout.completed",
"checkout.failed",
"checkout.canceled",
"subscription.activated",
"subscription.renewed",
"subscription.canceled",
"payment.refunded"
] as const;
function verify(raw: string, timestamp: string, signature: string, secret: string) {
const age = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(age) || age > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${timestamp}.${raw}`).digest("hex");
const a = Buffer.from(signature);
const b = Buffer.from(expected);
return a.length === b.length && timingSafeEqual(a, b);
}
export const Route = createFileRoute("/api/public/gateway/events")({
server: {
handlers: {
POST: async ({ request }) => {
const secret = process.env["NOBLEPORT_GATEWAY_WEBHOOK_SECRET"];
if (!secret) return Response.json({ received: true, staged: true });
const raw = await request.text();
const signature = request.headers.get("x-nobleport-signature") ?? "";
const timestamp = request.headers.get("x-nobleport-timestamp") ?? "";
if (!verify(raw, timestamp, signature, secret)) {
return new Response("Invalid signature", { status: 401 });
}
const event = JSON.parse(raw) as { id: string; type: string; data?: unknown };
if (!HANDLED_EVENTS.includes(event.type as never)) {
return Response.json({ received: true, ignored: event.type });
}
// TODO: persist UI status for event.id (idempotent upsert on event_id).
// Do NOT grant entitlements here — Stripe billing state is owned by
// the existing Stripe webhook.
console.log("[nobleport] gateway event", event.id, event.type);
return Response.json({ received: true });
},
},
},
});
How the Launchpad and the Gateway relate
This app remains the live Stripe implementation for Sea-Esta memberships. The Launchpad is the flow-builder companion to the central NoblePort Payment Gateway: you design a flow once, run it locally on Stripe, or ship it to the gateway where Stripe and PayPal credentials live and where every NoblePort dapp calls the same REST API with its own app key.