Payments
Zippex collects payment for every delivery created through the API. Your customer is charged once for items plus the delivery fee; Zippex remits your share to your Stripe account, keeps a volume-tiered service fee, and handles the dispatch and fulfillment of the delivery on its own side. There is a single payment flow: create a checkout → collect payment → create the delivery.
Universal: just open a URL
POST /v1/checkouts returns a checkout_url. Open it in a browser, WebView, Safari View Controller, or Chrome Custom Tab — Zippex hosts a Stripe-powered payment page that works identically on web, iOS, and Android. Pass success_url / cancel_url and the customer is redirected back to your app afterwards. No Stripe SDK, no PaymentSheet wiring, no platform-specific code.Prefer a native sheet inside your app? The same response also returns client_secret, customer, ephemeral_key, and publishable_key, so you can drop straight into Stripe's SDK — see Advanced: native SDK.
How money flows
Your customer is charged items_subtotal + delivery_fee. At delivery completion the funds settle automatically:
- • You — your items subtotal, minus the Zippex service fee, transferred to your connected Stripe account (your “net share”).
- • Zippex — keeps its own cut of the delivery fee and the service fee. Driver payout is handled internally by Zippex.
The service fee is taken from your share, not added on top of what the customer pays.
Service fee
The service fee is a percentage of your items subtotal and decreases as your monthly delivery volume grows. Higher volume, lower fee.
| Deliveries / 30 days | Service fee |
|---|---|
| 0 – 99 | 10% |
| 100 – 499 | 8% |
| 500 – 1,999 | 6% |
| 2,000+ | 4% |
High-volume accounts can negotiate a custom rate, contact support@zippex.com.
Prerequisite: connect Stripe
You must complete Stripe Connect onboarding before you can collect payments, this is the account your net share is paid out to.
- Go to Dashboard → Payouts → Connect Stripe.
- Complete the Stripe Express onboarding flow.
Connect required
403 stripe_not_connected until onboarding is complete.Step 1 — Create a checkout
/v1/checkoutsServer-side: creates a Stripe PaymentIntent for items + delivery and returns the credentials your client app needs to display Stripe's payment sheet.
What this endpoint actually does
client_secret— used by Stripe's SDK on your client to render the payment UIcustomer+ephemeral_key— required by mobile Stripe PaymentSheetpublishable_key— the public Stripe key your client SDK initializes withcheckout_id— pass this to/v1/deliveriesafter the customer pays
Zippex does not host a payment page — you render Stripe's sheet inside your own app using these values in Step 2.
Parameters
quote_idstringrequiredA valid, unused quote ID from POST /v1/quotes. The quote provides the authoritative delivery fee.
items_subtotalintegeroptionalGoods price in cents (e.g. 4500 = $45.00). Defaults to 0 for delivery-only. Your net payout is this amount minus the service fee.
success_urlstringoptionalAbsolute http(s) URL the hosted checkout redirects to after a successful payment. Recommended — gives you a clean handoff back to your app.
cancel_urlstringoptionalAbsolute http(s) URL the hosted checkout sends the customer to if they cancel.
customer.namestringoptionalCustomer name for the payment receipt.
customer.emailstringoptionalCustomer email for the receipt. Reuses an existing Stripe Customer when matched.
customer.phonestringoptionalCustomer phone for the receipt.
stripe_versionstringoptionalAdvanced — only needed if you render a native Stripe PaymentSheet yourself. Pass your mobile SDK apiVersion to receive an ephemeral_key.
curl -X POST https:"color:#6a9955">//developer.zippex.app/api/v1/checkouts \
-H "Authorization: Bearer zx_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"quote_id": "qt_dr_4f8a2b1c0d3e6f",
"items_subtotal": 4500,
"success_url": "https://yourapp.com/orders/123/confirmed",
"cancel_url": "https://yourapp.com/cart",
"customer": { "name": "Jane Smith", "email": "jane@example.com" }
}'{
"checkout_id": "chk_dr_abc123def456",
"checkout_url": "https://developer.zippex.app/checkout/chk_dr_abc123def456",
"client_secret": "pi_xxx_secret_yyy",
"payment_intent_id": "pi_xxx",
"customer": "cus_xxx",
"ephemeral_key": null,
"publishable_key": "pk_live_xxx",
"quote_id": "qt_dr_4f8a2b1c0d3e6f",
"currency": "cad",
"amount": 5749,
"delivery_fee": 1249,
"items_subtotal": 4500,
"expires_at": "2026-05-21T14:30:00Z"
}Step 2 — Open the hosted checkout
The checkout_url from Step 1 is a Zippex-hosted page that renders Stripe's Payment Element — cards, Apple Pay, Google Pay, Link, and every other method you've enabled. It works on any platform that can open a URL. Send the customer there, they pay, Stripe redirects them to your success_url. Done.
Web
"color:#6a9955">// Same-tab redirect
window.location.href = checkout.checkout_url;
"color:#6a9955">// Or a new tab
window.open(checkout.checkout_url, '_blank');iOS — SFSafariViewController
import SafariServices
"color:#6a9955">// The recommended way to open URLs on iOS — full Stripe UI, Apple Pay,
"color:#6a9955">// and your success_url is delivered back to your app universal link.
let vc = SFSafariViewController(url: URL(string: checkout.checkoutUrl)!)
present(vc, animated: true)Android — Chrome Custom Tabs
import androidx.browser.customtabs.CustomTabsIntent
"color:#6a9955">// Renders the checkout in an in-app browser tab with shared cookies,
"color:#6a9955">// Google Pay support, and a smooth return to your app via the success_url
"color:#6a9955">// (use an app link / deep link for the cleanest handoff).
CustomTabsIntent.Builder().build()
.launchUrl(context, Uri.parse(checkout.checkoutUrl))React Native / Expo
import * as WebBrowser from 'expo-web-browser';
"color:#6a9955">// openAuthSessionAsync auto-closes the browser once it hits success_url,
"color:#6a9955">// then returns control to your app. Make success_url a deep link / universal
"color:#6a9955">// link into your app (e.g. yourapp://orders/123/confirmed).
const result = await WebBrowser.openAuthSessionAsync(
checkout.checkout_url,
'yourapp://orders/123/confirmed', // matches success_url
);
if (result.type === 'success') {
"color:#6a9955">// Customer paid — call POST /v1/deliveries with checkout.checkout_id
}What the customer sees
cancel_url. The page already handles 3-D Secure, Link, wallet sheets, error states, and expiry. There is nothing else to build.Advanced — Render Stripe's sheet natively
You only need this if you want the sheet inside your own app shell without ever leaving it (typical for high-volume merchants who care about every conversion percent). The same /v1/checkouts response gives you client_secret, customer, ephemeral_key, and publishable_key. Hand those to Stripe's official SDK on each platform.
Common reasons the native sheet doesn't appear
- You called
initPaymentSheetbut never calledpresentPaymentSheet()/confirmPayment(). - You passed
payment_intent_idinstead ofclient_secret. - You forgot to send
stripe_versionin the checkout body, soephemeral_keycame backnulland the init failed silently. - You initialized Stripe with your own publishable key instead of the
publishable_keyreturned by Zippex (the PaymentIntent lives on the Zippex platform account).
Web — Stripe.js + PaymentElement
Install @stripe/stripe-js (or load https://js.stripe.com/v3/):
npm install @stripe/stripe-jsimport { loadStripe } from '@stripe/stripe-js';
"color:#6a9955">// 1. Create the checkout on your server, return values to the browser
const checkout = await fetch('/your-server/create-checkout', { method: 'POST' }).then(r => r.json());
"color:#6a9955">// 2. Initialize Stripe with the publishable key Zippex returned
const stripe = await loadStripe(checkout.publishable_key);
"color:#6a9955">// 3. Mount the Payment Element
const elements = stripe.elements({ clientSecret: checkout.client_secret });
const paymentElement = elements.create('payment');
paymentElement.mount('#payment-element'); // <div id="payment-element"></div>
"color:#6a9955">// 4. Confirm the payment when the customer clicks "Pay"
const { error, paymentIntent } = await stripe.confirmPayment({
elements,
confirmParams: { return_url: window.location.origin + '/order-confirmed' },
redirect: 'if_required',
});
if (!error && paymentIntent.status === 'succeeded') {
"color:#6a9955">// 5. Now call POST /v1/deliveries with checkout.checkout_id
}iOS — Stripe iOS SDK (Swift)
Add via Swift Package Manager: https://github.com/stripe/stripe-ios (StripePaymentSheet product).
import StripePaymentSheet
"color:#6a9955">// 1. Create the checkout from your server, passing the iOS SDK apiVersion
"color:#6a9955">// The current SDK exposes STPAPIClient.STPSDKVersion, but Stripe pins
"color:#6a9955">// apiVersion to "2020-08-27" for ephemeral keys — pass that.
let checkout = try await yourServer.createCheckout(
quoteId: quoteId,
itemsSubtotal: 4500,
stripeVersion: "2020-08-27"
)
"color:#6a9955">// 2. Configure the PaymentSheet with the Zippex publishable key
STPAPIClient.shared.publishableKey = checkout.publishableKey
var config = PaymentSheet.Configuration()
config.merchantDisplayName = "Your Store"
config.customer = .init(id: checkout.customer, ephemeralKeySecret: checkout.ephemeralKey)
config.applePay = .init(merchantId: "merchant.com.yourapp", merchantCountryCode: "CA")
config.allowsDelayedPaymentMethods = true
let paymentSheet = PaymentSheet(
paymentIntentClientSecret: checkout.clientSecret,
configuration: config
)
"color:#6a9955">// 3. Present from your view controller
paymentSheet.present(from: self) { result in
switch result {
case .completed:
"color:#6a9955">// Call POST /v1/deliveries with checkout.checkoutId
case .canceled: break
case .failed(let error): print(error)
}
}Android — Stripe Android SDK (Kotlin)
Add to build.gradle:
dependencies {
implementation 'com.stripe:stripe-android:21.+'
}import com.stripe.android.PaymentConfiguration
import com.stripe.android.paymentsheet.PaymentSheet
import com.stripe.android.paymentsheet.PaymentSheetResult
class CheckoutActivity : AppCompatActivity() {
private lateinit var paymentSheet: PaymentSheet
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
paymentSheet = PaymentSheet(this, ::onPaymentResult)
lifecycleScope.launch {
"color:#6a9955">// 1. Create checkout via your server, send the Android SDK apiVersion
val checkout = yourServer.createCheckout(
quoteId = quoteId,
itemsSubtotal = 4500,
stripeVersion = "2020-08-27"
)
"color:#6a9955">// 2. Initialize Stripe with the Zippex publishable key
PaymentConfiguration.init(applicationContext, checkout.publishableKey)
"color:#6a9955">// 3. Present the sheet
paymentSheet.presentWithPaymentIntent(
checkout.clientSecret,
PaymentSheet.Configuration(
merchantDisplayName = "Your Store",
customer = PaymentSheet.CustomerConfiguration(
id = checkout.customer,
ephemeralKeySecret = checkout.ephemeralKey
),
googlePay = PaymentSheet.GooglePayConfiguration(
environment = PaymentSheet.GooglePayConfiguration.Environment.Production,
countryCode = "CA",
currencyCode = "CAD"
)
)
)
}
}
private fun onPaymentResult(result: PaymentSheetResult) {
when (result) {
is PaymentSheetResult.Completed -> { /* call POST /v1/deliveries */ }
is PaymentSheetResult.Canceled -> {}
is PaymentSheetResult.Failed -> Log.e("Stripe", "Error", result.error)
}
}
}React Native — @stripe/stripe-react-native
Cross-platform sheet that renders the native iOS / Android UI under the hood.
npm install @stripe/stripe-react-native
cd ios && pod installimport { StripeProvider, useStripe } from '@stripe/stripe-react-native';
"color:#6a9955">// Wrap your root component once
export default function App() {
const [publishableKey, setPublishableKey] = useState('');
return (
<StripeProvider publishableKey={publishableKey} merchantIdentifier="merchant.com.yourapp">
<Checkout onPublishableKey={setPublishableKey} />
</StripeProvider>
);
}
function Checkout({ onPublishableKey }) {
const { initPaymentSheet, presentPaymentSheet } = useStripe();
async function openSheet() {
"color:#6a9955">// 1. Create the checkout — send the SDK's apiVersion
const checkout = await fetch('https:"color:#6a9955">//your-server/create-checkout', {
method: 'POST',
body: JSON.stringify({
quote_id, items_subtotal: 4500,
stripe_version: '2020-08-27', // matches @stripe/stripe-react-native
}),
}).then(r => r.json());
// 2. Hand Stripe the publishable key from Zippex
onPublishableKey(checkout.publishable_key);
// 3. Init + present the sheet (this renders the iOS / Android native UI)
const init = await initPaymentSheet({
merchantDisplayName: 'Your Store',
customerId: checkout.customer,
customerEphemeralKeySecret: checkout.ephemeral_key,
paymentIntentClientSecret: checkout.client_secret,
allowsDelayedPaymentMethods: true,
applePay: { merchantCountryCode: 'CA' },
googlePay: { merchantCountryCode: 'CA', testEnv: __DEV__, currencyCode: 'CAD' },
});
if (init.error) { console.error(init.error); return; }
const { error } = await presentPaymentSheet();
if (!error) {
"color:#6a9955">// 4. Call POST /v1/deliveries with checkout.checkout_id
}
}
return <Button onPress={openSheet} title="Pay" />;
}Flutter — flutter_stripe
import 'package:flutter_stripe/flutter_stripe.dart';
Future<void> openSheet() async {
final checkout = await yourServer.createCheckout(
quoteId: quoteId, itemsSubtotal: 4500, stripeVersion: '2020-08-27',
);
Stripe.publishableKey = checkout.publishableKey;
await Stripe.instance.initPaymentSheet(
paymentSheetParameters: SetupPaymentSheetParameters(
merchantDisplayName: 'Your Store',
paymentIntentClientSecret: checkout.clientSecret,
customerId: checkout.customer,
customerEphemeralKeySecret: checkout.ephemeralKey,
applePay: const PaymentSheetApplePay(merchantCountryCode: 'CA'),
googlePay: const PaymentSheetGooglePay(merchantCountryCode: 'CA', currencyCode: 'CAD'),
),
);
await Stripe.instance.presentPaymentSheet();
"color:#6a9955">// Then POST /v1/deliveries with checkout.checkoutId
}Apple Pay & Google Pay
Test mode
zx_test_ key, /v1/checkouts creates a real test-mode PaymentIntent — the sheet renders normally, but no money moves. Use card 4242 4242 4242 4242 (any future expiry, any CVC). Stripe Connect onboarding is not required in test mode. Pass the resulting checkout_id to a test-mode /v1/deliveries call to exercise the full pay → deliver loop.Step 3 — Create the delivery
After the customer pays, create the delivery with the checkout_id. Zippex verifies the payment succeeded, then dispatches a driver. See Deliveries for the full reference.
curl -X POST https:"color:#6a9955">//developer.zippex.app/api/v1/deliveries \
-H "Authorization: Bearer zx_live_your_api_key_here" \
-H "Content-Type: application/json" \
-d '{
"quote_id": "qt_dr_4f8a2b1c0d3e6f",
"checkout_id": "chk_dr_abc123def456",
"customer": { "name": "Jane Smith", "phone": "+16045550456" }
}'Pay before you dispatch
checkout_id. Calling /v1/deliveries without one returns 400 missing_checkout; with an unpaid checkout it returns 402 checkout_not_paid.Payouts
Your net share is transferred to your connected Stripe Express account when the delivery completes, then paid out on your Stripe schedule (typically 2 business days).
- View payout history in Dashboard → Payouts.
- Open your Stripe Dashboard from the Payouts page for detailed transaction reports.