Skip to content

Payment Integration ​

This guide explains the complete payment flow for accepting testimony offers using Stripe integration.

Overview ​

The payment process involves three main steps:

  1. Checkout Initiation - Client initiates payment and receives a Stripe checkout URL
  2. Payment Processing - Client completes payment on Stripe's secure checkout page
  3. Payment Confirmation - Application confirms payment and accepts the offer

Prerequisites ​

Before You Start

Ensure these requirements are met before initiating payment:

Step-by-Step Flow ​

Step 1: Initiate Checkout ​

Endpoint: Checkout Offer

http
POST /api/v1/testimonies/offers/{offer}/checkout

Required Data:

  • phone - Phone number in international format, not used by another user
  • firstName - Client's first name
  • lastName - Client's last name
  • billingAddress - Billing address object (all fields required, line2 included)
  • 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:

json
{
  "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:

json
{
  "data": {
    "url": "https://checkout.stripe.com/c/pay/cs_test_a1..."
  }
}

Step 2: Complete Payment on Stripe ​

  1. Redirect User - Redirect the client to the url returned in Step 1
  2. Stripe Checkout - Client completes payment on Stripe's secure page
  3. 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 the sessionId query 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

http
POST /api/v1/testimonies/offers/accept

Required Data:

  • sessionId - The session ID returned from Stripe redirect

Request Example:

json
{
  "sessionId": "cs_test_a1B2c3D4e5F6g7H8i9J0k1L2m3N4o5P6q7R8s9T0"
}

Response:

204 No Content

Webhook 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

http
GET /api/v1/testimonies/{testimonyId}

Check for:

  • state should be "accepted"
  • acceptedOffer should contain the offer details
  • expert should be populated

State Transitions ​

The payment flow triggers the following state transitions:

pending → offered → accepted

See Testimony Workflow for complete state transition diagram.

Commission Calculation ​

Each offer includes a full financial breakdown:

  • price - Total amount set by the expert
  • commissionPercent - Platform fee percentage (default 21%)
  • commissionAmount - Calculated platform fee
  • expertAmount - Amount the expert receives after commission

Example:

Price:              €500
Commission (21%):   €105
Expert Amount:      €395

The 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

json
{
  "message": "Telefónne číslo už existuje.",
  "errors": {
    "phone": [
      "Telefónne číslo už existuje."
    ]
  }
}

Common Issues:

  • Phone number used by another user
  • Invalid address data (line2 must 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 yet
  • Pri spracovaní platby nastala chyba. Skúste to znova.

429 Too Many Requests — more than 3 checkout attempts per minute.

500 Internal Server Error

json
{
  "message": "An unexpected error occurred."
}

Payment Confirmation Errors ​

If payment confirmation fails:

422 Unprocessable Content

json
{
  "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 4
  • Táto ponuka už nie je dostupná.
  • Prechod zo stavu [cancelled] do stavu [accepted] nie je možný. — the testimony left the offered state
  • Zá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:

  1. Use Stripe test mode credentials
  2. Use test card numbers from Stripe Testing Docs
  3. Common test cards:
    • Success: 4242 4242 4242 4242
    • Decline: 4000 0000 0000 0002

See Email Testing for viewing payment notification emails.

Integration Example ​

Frontend Flow (Pseudocode) ​

javascript
// 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}`;
}