Payment Integration
This guide explains the complete payment flow for accepting testimony offers using Stripe integration.
Overview
The payment process involves three main steps:
- Checkout Initiation - Client initiates payment and receives a Stripe checkout URL
- Payment Processing - Client completes payment on Stripe's secure checkout page
- Payment Confirmation - Application confirms payment and accepts the offer
Prerequisites
Before You Start
Ensure these requirements are met before initiating payment:
- Client is authenticated and has a verified email
- Testimony exists in
offeredstate (see Testimony Workflow) - Expert has created an offer (see Create Offer)
- Offer includes
price,commissionAmount,commissionPercent, andexpertAmount(see Offer Object)
Step-by-Step Flow
Step 1: Initiate Checkout
Endpoint: Checkout Offer
POST /api/v1/testimonies/offers/{offer}/checkoutRequired Data:
phone- Phone number in international format, not used by another userfirstName- Client's first namelastName- Client's last namebillingAddress- Billing address object (all fields required,line2included)shippingAddress- Delivery address (optional; saved to the client's profile, not sent to Stripe)company- Company details (optional, for business purchases)
The submitted details are also saved to the client's profile.
Request Example:
{
"phone": "+421902123456",
"firstName": "Peter",
"lastName": "Novák",
"billingAddress": {
"line1": "Hlavná 123",
"line2": "Apartment 4B",
"city": "Bratislava",
"postalCode": "811 01",
"country": "SK"
},
"shippingAddress": {
"line1": "Trieda SNP",
"line2": "1",
"city": "Košice",
"postalCode": "040 01",
"country": "SK"
},
"company": {
"businessId": "12345678",
"taxId": "2023456789",
"vatId": "SK2023456789"
}
}Response:
{
"data": {
"url": "https://checkout.stripe.com/c/pay/cs_test_a1..."
}
}Step 2: Complete Payment on Stripe
- Redirect User - Redirect the client to the
urlreturned in Step 1 - Stripe Checkout - Client completes payment on Stripe's secure page
- Stripe Redirect - After payment (or cancellation) Stripe redirects the client to the success or cancel URL configured on the backend (
BRUNAGO_FRONTEND_SUCCESS_URL/BRUNAGO_FRONTEND_CANCEL_URL). The backend appends?sessionId={CHECKOUT_SESSION_ID}to both, so your page receives the Stripe session id as thesessionIdquery parameter
Example Redirect URLs:
Success: https://your-app.com/payment/success?sessionId=cs_test_a1...
Cancel: https://your-app.com/payment/cancel?sessionId=cs_test_a1...Calling Checkout Offer again for the same testimony returns a new URL and expires the previous, still-open Stripe session — always send the client to the URL from the latest response.
Step 3: Confirm Payment
Endpoint: Accept Offer
POST /api/v1/testimonies/offers/acceptRequired Data:
sessionId- The session ID returned from Stripe redirect
Request Example:
{
"sessionId": "cs_test_a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0"
}Response:
204 No ContentWebhook race
The backend also accepts the offer when Stripe's checkout.session.completed webhook arrives, which can happen before the client reaches your success page. Then this call returns 409 Conflict with Pre tento posudok už bola prijatá ponuka. — treat it as success on the payment-success page and confirm with Step 4.
Step 4: Verify Acceptance
After successful payment confirmation, the testimony transitions to accepted state.
Verify State: Show Testimony
GET /api/v1/testimonies/{testimonyId}Check for:
stateshould be"accepted"acceptedOffershould contain the offer detailsexpertshould be populated
State Transitions
The payment flow triggers the following state transitions:
pending → offered → acceptedSee Testimony Workflow for complete state transition diagram.
Commission Calculation
Each offer includes a full financial breakdown:
price- Total amount set by the expertcommissionPercent- Platform fee percentage (default 21%)commissionAmount- Calculated platform feeexpertAmount- Amount the expert receives after commission
Example:
Price: €500
Commission (21%): €105
Expert Amount: €395The breakdown is calculated automatically when the offer is created. See Offer Object for field details.
Error Handling
Checkout Errors
If checkout initiation fails:
422 Unprocessable Content
{
"message": "Telefónne číslo už existuje.",
"errors": {
"phone": [
"Telefónne číslo už existuje."
]
}
}Common Issues:
- Phone number used by another user
- Invalid address data (
line2must be a non-empty string) - Invalid country code
422 Unprocessable Content with a plain message (no errors object):
Platbu je možné začať len pri posudku s aktívnou ponukou.Znalec ešte nie je overený.— the expert's account is not approved yetPri spracovaní platby nastala chyba. Skúste to znova.
429 Too Many Requests — more than 3 checkout attempts per minute.
500 Internal Server Error
{
"message": "An unexpected error occurred."
}Payment Confirmation Errors
If payment confirmation fails:
422 Unprocessable Content
{
"message": "Platba ešte nebola dokončená. Stav: unpaid."
}(A validation error with an errors object is returned when sessionId is missing or does not start with cs_.)
403 Forbidden — This testimony does not belong to the authenticated client.
409 Conflict — one of:
Pre tento posudok už bola prijatá ponuka.— the webhook got there first, or another offer was paid; verify with Step 4Táto ponuka už nie je dostupná.Prechod zo stavu [cancelled] do stavu [accepted] nie je možný.— the testimony left theofferedstateZáznam bol medzitým zmenený. Skúste to znova.
502 Bad Gateway — Could not retrieve Stripe session. (unknown session id or Stripe unreachable)
See Accept Offer for details.
Security Notes
Security Best Practices
- All payment processing is handled by Stripe's PCI-compliant infrastructure
- No credit card data is stored in the application
- Session IDs are single-use and expire after confirmation
- Phone number uniqueness prevents duplicate accounts
Testing
Testing Payments
For testing the payment flow:
- Use Stripe test mode credentials
- Use test card numbers from Stripe Testing Docs
- Common test cards:
- Success:
4242 4242 4242 4242 - Decline:
4000 0000 0000 0002
- Success:
See Email Testing for viewing payment notification emails.
Integration Example
Frontend Flow (Pseudocode)
// Step 1: Initiate checkout
async function initiateCheckout(offerId, checkoutData) {
const response = await fetch(`/api/v1/testimonies/offers/${offerId}/checkout`, {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify(checkoutData)
});
const { data } = await response.json();
// Step 2: Redirect to Stripe
window.location.href = data.url;
}
// Step 3: Handle Stripe redirect (on your success page)
async function handlePaymentSuccess() {
const urlParams = new URLSearchParams(window.location.search);
const sessionId = urlParams.get('sessionId');
if (!sessionId) {
throw new Error('No session ID found');
}
// Confirm payment
const response = await fetch('/api/v1/testimonies/offers/accept', {
method: 'POST',
headers: {
'Authorization': `Bearer ${accessToken}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ sessionId })
});
// 204 = accepted now; 409 = the Stripe webhook already accepted it — both are fine here
if (!response.ok && response.status !== 409) {
throw new Error('Payment confirmation failed');
}
// Redirect to testimony details
window.location.href = `/testimonies/${testimonyId}`;
}