Stripe · Merchant

Completion and refunds

Confirm the booking from Stripe server-side state, not from the browser return. Refund from PayShare so application fees reverse correctly.

How a session ends

OutcomeWhat happens to the moneySession status
Everyone paysPayShare captures every authorised share together on your Stripe account.completed
Time runs outAuthorised shares are released. Nobody is charged.expired
The host cancelsAuthorised shares are released. Nobody is charged.cancelled

While people are still joining and paying, the session is pending or active.

The return to your site is not proof of payment

After a completed session PayShare sends the customer to your successReturnUrl. Use that page to show a confirmation, not to mark the order paid. A browser can open that URL without paying, and a customer who closes the tab never arrives.

Confirm the order from Stripe

Every share is a PaymentIntent on your Stripe account, so your existing Stripe webhook endpoint already receives its events. Each one carries this metadata:

payshare_session_idstring
The PayShare session ID.
payshare_participant_idstring
The person who paid this share.
payshare_order_refstring
Your order reference from the order proof.

Shares are captured together, so they reach succeeded together. Mark the order paid when the succeeded shares for one session add up to the order total in the order currency.

Node.js
// Your existing Stripe webhook handler
case "payment_intent.succeeded": {
  const pi = event.data.object;
  const sessionId = pi.metadata.payshare_session_id;
  const participantId = pi.metadata.payshare_participant_id;
  const orderRef = pi.metadata.payshare_order_ref;
  if (!sessionId || !participantId || !orderRef) break; // Not a PayShare share

  const booking = await getBookingByReference(orderRef);
  if (!booking || pi.currency !== booking.currency.toLowerCase()) break;

  // Record the share once, keyed by PaymentIntent ID.
  await upsertShare({ paymentIntentId: pi.id, sessionId, bookingId: booking.id, amount: pi.amount_received });

  // Paid when this session's recorded shares cover the order total.
  const received = await sumShares({ bookingId: booking.id, sessionId });
  if (received >= booking.totalMinor) {
    await markBookingPaid(booking.id, { payshareSessionId: sessionId }); // idempotent
  }
  break;
}
  • Make the update idempotent. Stripe can deliver an event more than once.
  • Ignore the cancelled order proof. It has payshare_bootstrap set to consumed and no payshare_participant_id, so it is not a share.
  • An order can have more than one session if an earlier one expired. Count shares per session.

You can also see every session, its status and each person’s share in the PayShare app in your Stripe Dashboard.

Refunds

From the PayShare app

Open the session in PayShare in your Stripe Dashboard. You can refund the whole booking or one person’s share. PayShare refunds its fee with it. Refund through PayShare rather than selecting the individual payments in Stripe, so nothing is refunded twice.

From your server

To refund a whole session from your own admin, use the refundToken you stored when you started it.

POSThttps://app.payshare.nz/api/v1/bootstrap/session-refund

Request
POST /api/v1/bootstrap/session-refund
Content-Type: application/json

{
  "stripeAccountId": "acct_123",
  "sessionId": "5f0c2d6e-…",
  "refundToken": "…"
}
stripeAccountIdstringRequired
The Stripe account that installed PayShare.
sessionIduuidRequired
From the start-split response.
refundTokenstringRequired
From the start-split response.
200 OK
{
  "success": true,
  "sessionId": "5f0c2d6e-…",
  "refundedCount": 3,
  "ledgerRefunds": 3
}
CodeHTTPMeaning
SESSION_NOT_FOUND404No session with that ID for this install.
REFUND_TOKEN_INVALID403The token does not belong to this session.
MERCHANT_NOT_INSTALLED403PayShare is not installed on this Stripe account.
REFUND_FAILED400Stripe did not accept the refund. The message says why.

Next: test the whole flow.