When a customer selects Split with others, your start-split route runs three steps on your server.
Create an order proof in Stripe
An unpaid PaymentIntent for the order’s trusted total, tagged for PayShare.
Ask PayShare to start the session
Send PayShare the PaymentIntent ID and where to return the customer.
Send the customer to the session
Return the session URL to the button, which moves the current tab there.
Why an order proof
PayShare retrieves the PaymentIntent from Stripe on your account. Only a server holding your Stripe secret key can create one, so the PaymentIntent proves the request came from you. PayShare takes the amount, currency and order reference from the PaymentIntent. Anything the browser sends is ignored for money.
1. Create the order proof
Create a PaymentIntent on the Stripe account that installed PayShare, using the secret key your server already has. Do not confirm it.
amountintegerRequired- The order total in minor units, from your records. At least 1.
currencystringRequired- The order currency.
metadata.payshare_bootstrapstringRequired"1". Marks the PaymentIntent as a PayShare order proof.metadata.payshare_order_refstringRequired- Your booking or order reference, up to 256 characters. It is copied onto every share payment.
The PaymentIntent must still be requires_payment_method or requires_confirmation when PayShare reads it. A test-mode PaymentIntent works only with a test-mode install, and live with live.
2. Start the session
POSThttps://app.payshare.nz/api/v1/bootstrap/stripe-order-proof
Call this from your server with a JSON body. The order proof is what authenticates the request. Unknown fields are rejected.
POST /api/v1/bootstrap/stripe-order-proof
Host: app.payshare.nz
Content-Type: application/json
{
"stripeAccountId": "acct_123",
"paymentIntentId": "pi_3Q…",
"successReturnUrl": "https://www.example.com/bookings/BK-10482/confirmed",
"cancelReturnUrl": "https://www.example.com/bookings/BK-10482/checkout",
"checkoutExpiresAt": "2026-10-01T14:30:00+13:00",
"orderSummary": {
"merchantDisplayName": "Example Lodge",
"itemDescription": "Lake view suite, 2 nights",
"bookingStart": "2026-11-20T15:00:00+13:00",
"bookingEnd": "2026-11-22T10:00:00+13:00"
}
}Request body
stripeAccountIdstringRequired- The Stripe account that installed PayShare, starting
acct_. Hard-code it or store it in server config. Never read it from the browser. paymentIntentIdstringRequired- The order proof from step 1.
successReturnUrlURLRequired- Where PayShare sends the customer after the session completes. HTTPS. Your query string is kept, and PayShare may add its own parameters.
cancelReturnUrlURL- Where PayShare sends the customer if they leave the session. HTTPS. Recommended.
checkoutExpiresAtISO 8601 with offsetConditional- When your checkout releases this order. Send this or
checkoutHold, not both. See checkout hold. checkoutHold"none"Conditional- Send "none" if your checkout does not hold the order. Send this or checkoutExpiresAt.
orderSummaryobjectRequired- What the group sees in the session. See the fields below.
platformContextIdstring- Optional. If Pay now for this order settles on one of your Stripe Connect connected accounts, pass that
acct_here so shares settle there too. Still create the order proof on the install account. hostContactEmailstring- Optional. The booking customer’s email, if you have it.
orderSummary
These fields describe the order to the group. They never set the amount, currency or order reference.
merchantDisplayNamestringRequired- Your business name as guests should see it. Up to 256 characters.
itemDescriptionstringRequired- What is being bought, for example the room or ticket. Up to 512 characters.
bookingStart, bookingEndISO 8601- Start and end of a stay or event. Include an offset, or send a date as YYYY-MM-DD.
orderBlurbstring- A short line shown with the order. Up to 120 characters.
orderImageUrlURL- An HTTPS thumbnail of the item. Up to 2048 characters.
productContextstring- Background about the product for PayShare. Not shown to guests. Up to 1000 characters.
Send the photo your checkout summary already shows as orderImageUrl. PayShare displays it at the top of the session for the host and every guest, so the group can see what they are paying for. Use an absolute, public https:// URL (expand relative paths with your site origin). Leave it out when there is no real photo, and PayShare shows your brand instead.
Response
A successful request returns HTTP 200 with the header x-payshare-bootstrap: stripe-order-proof-v1.
{
"success": true,
"bootstrapVersion": "stripe-order-proof-v1",
"sessionId": "5f0c2d6e-…",
"sessionUrl": "https://app.payshare.nz/session/5f0c2d6e-…",
"hostUrl": "https://app.payshare.nz/session/5f0c2d6e-…",
"amountMinor": 42000,
"currency": "nzd",
"merchantOrderRef": "BK-10482",
"proofPaymentIntentId": "pi_3Q…",
"replayed": false,
"refundToken": "…"
}sessionIdstring- The PayShare session ID.
sessionUrlURL- Send the customer here. They become the host.
hostUrlURL- The same host link.
amountMinor, currencyinteger, string- Read from the order proof.
merchantOrderRefstring- Your order reference from the proof metadata.
proofPaymentIntentIdstring- The order proof you sent.
replayedboolean- true when this is a repeat of a request that already created the session.
refundTokenstring- Lets your server refund this session later. Store it with the order and never send it to the browser. See refunds.
Errors use { "success": false, "error": { "code", "message", "details" } }. See Errors.
Retries and duplicates
- One order proof creates at most one session. Duplicate requests with the same proof return that session with
replayed: true. - Once the session exists, the proof is cancelled. Sending it again later returns
PROOF_INELIGIBLE. To send a customer back to their session, use thesessionUrlyou stored. - For a genuinely new split attempt, create a new order proof.
Checkout hold
A split must finish before your checkout gives the order away.
checkoutExpiresAt: the session ends 30 seconds before this time. It must be in the future and within PayShare’s maximum, which the error message states if you go over.checkoutHold: "none": your checkout holds nothing, and the session lasts 30 minutes.
A hold that has passed returns CHECKOUT_EXPIRED. One with 30 seconds or less left returns CHECKOUT_EXPIRY_TOO_CLOSE.
3. Send the customer to the session
Return { redirectUrl, sessionUrl } from your route. The official button moves the current tab to the session. If you build your own redirect, use the same tab.
Complete examples
Node.js with @payshare/platform-sdk, which sends the same request as above:
// app/api/payshare/start-split/route.ts
import Stripe from "stripe";
import {
bootstrapSessionFromOrderProof,
payshareBootstrapPaymentIntentMetadata,
PayShareBootstrapError,
} from "@payshare/platform-sdk";
// The secret key your server already uses for this Stripe account.
const stripe = new Stripe(process.env.STRIPE_SECRET_KEY!);
// The Stripe account that installed PayShare (the same account as the key above).
const STRIPE_ACCOUNT_ID = "acct_123";
export async function POST(req: Request) {
const { bookingId } = await req.json();
// Look the order up in your own database. Never trust amounts from the browser.
const booking = await getBooking(bookingId);
if (!booking) {
return Response.json({ error: { message: "Booking not found" } }, { status: 404 });
}
// 1. Order proof: an unpaid PaymentIntent for the trusted total.
const proof = await stripe.paymentIntents.create({
amount: booking.totalMinor,
currency: booking.currency.toLowerCase(),
metadata: payshareBootstrapPaymentIntentMetadata(booking.reference),
});
try {
// 2. Start the PayShare session.
const session = await bootstrapSessionFromOrderProof(
{ apiBase: "https://app.payshare.nz" },
{
stripeAccountId: STRIPE_ACCOUNT_ID,
paymentIntentId: proof.id,
successReturnUrl: `https://www.example.com/bookings/${booking.reference}/confirmed`,
cancelReturnUrl: `https://www.example.com/bookings/${booking.reference}/checkout`,
checkoutExpiresAt: booking.holdExpiresAt.toISOString(),
orderSummary: {
merchantDisplayName: "Example Lodge",
itemDescription: booking.roomName,
bookingStart: booking.checkIn,
bookingEnd: booking.checkOut,
},
},
);
// Keep refundToken server-side. Store sessionUrl to resume the same session.
await saveSplitSession(booking.id, session);
// 3. The button moves the browser to the session.
return Response.json({ redirectUrl: session.sessionUrl, sessionUrl: session.sessionUrl });
} catch (error) {
if (error instanceof PayShareBootstrapError) {
return Response.json({ error: { code: error.code, message: error.message } }, { status: error.httpStatus });
}
throw error;
}
}PHP with the Stripe PHP library:
<?php
// start-split.php
require __DIR__ . '/vendor/autoload.php';
$stripe = new \Stripe\StripeClient(getenv('STRIPE_SECRET_KEY'));
$stripeAccountId = 'acct_123'; // The Stripe account that installed PayShare
$input = json_decode(file_get_contents('php://input'), true);
$booking = get_booking($input['bookingId'] ?? ''); // Your database, your trusted total
header('Content-Type: application/json');
if (!$booking) {
http_response_code(404);
echo json_encode(['error' => ['message' => 'Booking not found']]);
exit;
}
// 1. Order proof
$proof = $stripe->paymentIntents->create([
'amount' => $booking['total_minor'],
'currency' => strtolower($booking['currency']),
'metadata' => [
'payshare_bootstrap' => '1',
'payshare_order_ref' => $booking['reference'],
],
]);
// 2. Start the session
$ch = curl_init('https://app.payshare.nz/api/v1/bootstrap/stripe-order-proof');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => ['Content-Type: application/json', 'Accept: application/json'],
CURLOPT_POSTFIELDS => json_encode([
'stripeAccountId' => $stripeAccountId,
'paymentIntentId' => $proof->id,
'successReturnUrl' => 'https://www.example.com/bookings/' . $booking['reference'] . '/confirmed',
'cancelReturnUrl' => 'https://www.example.com/bookings/' . $booking['reference'] . '/checkout',
'checkoutHold' => 'none',
'orderSummary' => [
'merchantDisplayName' => 'Example Lodge',
'itemDescription' => $booking['room_name'],
],
]),
]);
$body = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status !== 200 || empty($body['success'])) {
http_response_code($status ?: 502);
echo json_encode(['error' => $body['error'] ?? ['message' => 'Could not start the split']]);
exit;
}
// Keep refundToken server-side. Store sessionUrl to resume the same session.
save_split_session($booking['id'], $body);
// 3. The button moves the browser to the session
echo json_encode(['redirectUrl' => $body['sessionUrl'], 'sessionUrl' => $body['sessionUrl']]);