Stripe · Merchant

Start a split

Create an unpaid Stripe PaymentIntent as the order proof, send it to PayShare from your server, and redirect the customer to the session URL.

When a customer selects Split with others, your start-split route runs three steps on your server.

  1. Create an order proof in Stripe

    An unpaid PaymentIntent for the order’s trusted total, tagged for PayShare.

  2. Ask PayShare to start the session

    Send PayShare the PaymentIntent ID and where to return the customer.

  3. 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.

Request
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.

200 OK
{
  "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 the sessionUrl you 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:

Next.js route handler
// 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
<?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']]);