How a session ends
| Outcome | What happens to the money | Session status |
|---|---|---|
| Everyone pays | PayShare captures every authorised share together on your Stripe account. | completed |
| Time runs out | Authorised shares are released. Nobody is charged. | expired |
| The host cancels | Authorised 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.
// 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_bootstrapset toconsumedand nopayshare_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
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.
{
"success": true,
"sessionId": "5f0c2d6e-…",
"refundedCount": 3,
"ledgerRefunds": 3
}| Code | HTTP | Meaning |
|---|---|---|
SESSION_NOT_FOUND | 404 | No session with that ID for this install. |
REFUND_TOKEN_INVALID | 403 | The token does not belong to this session. |
MERCHANT_NOT_INSTALLED | 403 | PayShare is not installed on this Stripe account. |
REFUND_FAILED | 400 | Stripe did not accept the refund. The message says why. |
Next: test the whole flow.