Skip to main content

Payments API

Sessiq takes online payments through two providers:

  • Stripe for cards and wallets. The customer pays in your page with Stripe Elements.
  • Paytrail for Finnish online banking and other local methods. The customer is redirected to Paytrail's hosted page.

Each venue uses its own Stripe and Paytrail accounts. Staff connect them in the admin. Bookings can also be paid on arrival, with a deposit, or at the counter.

This page covers taking payment for a booking hold, refunds, and no-show fees. Provider callbacks (Stripe webhooks, Paytrail notifications) are handled by Sessiq; see Webhooks. For gift card payments, see Gift Cards API.

Public endpoints​

All of these need the x-tenant-id header and no login. They work on a booking that is hold (or confirmed with an unpaid balance).

MethodPathPurpose
GET/api/v1/payments/configWhich providers are on, and the Stripe publishable key
POST/api/v1/payments/create-intentCreate a Stripe PaymentIntent for a booking
POST/api/v1/payments/{bookingId}/confirmTell Sessiq a Stripe payment succeeded; confirms the booking
POST/api/v1/payments/paytrail/createCreate a Paytrail payment and get the redirect URL
GET/api/v1/payments/paytrail/returnSettle a Paytrail payment when the customer comes back
GET/api/v1/payments/{bookingId}/statusCurrent payment status

Payment configuration​

GET /api/v1/payments/config?bookingId={bookingId}

{
"publishableKey": "pk_live_xxx",
"providers": {
"stripe": { "enabled": true, "publishableKey": "pk_live_xxx" },
"paytrail": { "enabled": true, "merchantId": "123456" }
}
}

Pass bookingId so the answer reflects the booking's site, which can have its own payment accounts. Initialise Stripe.js with this key, not one of your own.

Pay with Stripe​

  1. Create a PaymentIntent:

    curl -X POST https://api.sessiq.com/api/v1/payments/create-intent \
    -H "Content-Type: application/json" \
    -H "x-tenant-id: {tenantId}" \
    -d '{
    "bookingId": "bk_abc123",
    "customerEmail": "customer@example.com",
    "customerName": "Maija Meikäläinen",
    "customerPhone": "+358401234567"
    }'
    {
    "success": true,
    "clientSecret": "pi_xxx_secret_yyy",
    "paymentIntentId": "pi_xxx",
    "amount": 8000,
    "currency": "eur"
    }
    FieldRequiredDescription
    bookingIdYesThe hold to pay for
    paymentAmountNoAmount in cents (a positive integer), to pay a deposit only. Defaults to the full total
    customerEmail, customerName, customerPhoneNoSaved on the booking, so it has contact details even if the customer never returns to your page
    savedPaymentMethodId, stripeCustomerIdNoPay with a card the logged-in customer saved earlier

    amount in the response is in cents. Calling this again for the same booking reuses the earlier intent only if it is for the same amount; otherwise the old intent is cancelled and a new one created.

    The server decides how much may be charged, from the product's current deposit settings:

    BookingAllowed paymentAmountNo amount charges
    Deposit product, nothing paid yetthe deposit (1 cent rounding tolerance) up to the full totalthe full total
    No deposit, nothing paid yetexactly the full totalthe full total
    part_paid (deposit already taken)1 cent up to the outstanding balancethe outstanding balance

    Anything else gets 400 before Stripe is called. Out-of-range amounts return code PAYMENT_AMOUNT_OUT_OF_RANGE with the allowed min and max. Show the deposit from booking.depositInfo.depositAmount (cents) on GET /api/v1/bookings/{bookingId}: it includes add-ons, while the slot-level deposit in availability responses is only a preview.

  2. Mount Stripe Elements with clientSecret and call stripe.confirmPayment() in the browser.

  3. When Stripe reports succeeded, call:

    curl -X POST https://api.sessiq.com/api/v1/payments/{bookingId}/confirm \
    -H "Content-Type: application/json" \
    -H "x-tenant-id: {tenantId}" \
    -d '{ "paymentIntentId": "pi_xxx" }'
    {
    "success": true,
    "bookingStatus": "confirmed",
    "guestAccessToken": "eyJhbGciOi..."
    }

    This records the payment and confirms the booking. Do not also call POST /api/v1/bookings/{id}/confirm afterwards: the booking is no longer a hold and that call fails.

If the browser closes before step 3, Stripe's webhook still confirms the booking. Your page can check GET /api/v1/payments/{bookingId}/status when the customer comes back.

Pay with Paytrail​

  1. Create the payment:

    POST /api/v1/payments/paytrail/create
    {
    "bookingId": "bk_abc123",
    "successUrl": "https://example.com/booking/done",
    "cancelUrl": "https://example.com/booking/cancelled",
    "customerEmail": "customer@example.com",
    "customerName": "Maija Meikäläinen",
    "customerPhone": "+358401234567"
    }

    The response has redirectUrl, paymentId, amount and currency.

  2. Send the browser to redirectUrl.

  3. Paytrail sends the customer back to successUrl or cancelUrl with checkout-* query parameters. Pass them on to settle the payment straight away:

    GET /api/v1/payments/paytrail/return?checkout-reference={bookingId}&checkout-status=ok&checkout-transaction-id={transactionId}

    Forward all the checkout-* parameters you received, including the signature. The response has success, bookingStatus and message.

Paytrail also notifies Sessiq server to server, so the booking is confirmed even if the customer never comes back.

Other ways to pay​

Pay on arrival. If the product allows it, confirm the hold with payment_mode: "poa" (see Confirm a booking). The booking is confirmed with payment pending. Staff take the money at check-in (mark-arrived with collectPayment) or record it with mark-paid. Details: Payment on arrival guide.

Deposit. A product can require a deposit: a fixed amount or a percentage, with the balance due some hours before the event or after booking. The deposit amount appears as depositInfo on availability slots and on GET /api/v1/bookings/{bookingId}. To pay only the deposit, send paymentAmount (in cents) to create-intent. The booking becomes confirmed + part_paid, and Sessiq emails the customer a reminder with a payment link shortly before the balance is due. The customer can also pay the rest at the venue. If the product's deposit is set to collect the balance on arrival (balanceDueMode: "on_arrival"), no reminder is sent and the rest is paid at the venue. A deposit product cannot be confirmed without paying at least the deposit online: pay on arrival returns 409 DEPOSIT_REQUIRED.

Zero total. When a discount or voucher covers the whole price, confirm the hold with POST /api/v1/bookings/{id}/confirm. It becomes confirmed and paid with no payment step.

Payment status​

GET /api/v1/payments/{bookingId}/status:

{
"success": true,
"status": "succeeded",
"paymentIntentId": "pi_xxx",
"amount": 8000,
"currency": "eur",
"paidAt": "2026-10-15T09:06:12.000Z",
"bookingStatus": "confirmed"
}

status is one of pending, processing, succeeded, failed, canceled or requires_action. The booking's own statuses are described in Bookings API.

Refunds​

When a customer or staff member cancels a paid booking, Sessiq applies the cancellation policy and refunds through the original provider. Use GET /api/v1/bookings/{bookingId}/cancel-preview to see the refund before cancelling.

Staff can also refund without cancelling. These endpoints need a staff JWT.

MethodPathPermissionPurpose
POST/api/v1/admin/refunds/processadmin:refunds:processRefund all or part of a booking
POST/api/v1/admin/bookings/{bookingId}/record-manual-refundadmin:refunds:processRecord cash or external card money handed back
GET/api/v1/admin/bookings/{bookingId}/refundsadmin:refunds:viewA booking's refunds
GET/api/v1/admin/refunds/{refundRequestId}admin:refunds:viewOne refund
GET/api/v1/admin/refundsadmin:refunds:viewSearch refunds (status, bookingId, requestedBy, dateFrom, dateTo, minAmount, maxAmount, page)

Refund a booking​

curl -X POST https://api.sessiq.com/api/v1/admin/refunds/process \
-H "Authorization: Bearer {token}" \
-H "x-tenant-id: {tenantId}" \
-H "Content-Type: application/json" \
-d '{
"bookingId": "bk_abc123",
"refundType": "partial",
"amount": 20,
"reason": "Room was too cold",
"adminNotes": "Approved by manager"
}'
FieldRequiredDescription
bookingIdYesBooking to refund
refundTypeYesfull, partial or adjustment
amountFor partialAmount in euros
reasonYesShown in the refund record
adminNotesNoInternal only

The response has refundRequestId, status (pending, processing, completed or failed), refundAmount, feeAmount, netRefundAmount and a paymentBreakdown with one line per original payment it refunded.

Manual refunds​

For walk-in or phone bookings paid in cash or on an external terminal, the money goes back at the counter. Record it with POST /api/v1/admin/bookings/{bookingId}/record-manual-refund and { "amount": 40, "reason": "..." }. The booking's payment status becomes refunded for a full refund.

No-show fees​

A product's (or site's) cancellation policy can set a fee for customers who do not turn up. After staff mark a booking as no-show, they can charge the fee.

MethodPathPurpose
POST/api/v1/admin/no-show/booking/{bookingId}/calculate-feePreview the fee without charging
POST/api/v1/admin/no-show/booking/{bookingId}/process-feeCharge the fee and notify the customer
GET/api/v1/admin/no-show/booking/{bookingId}No-show record for a booking
GET/api/v1/admin/no-show/pendingFees not yet handled
PATCH/api/v1/admin/no-show/{trackingId}/resolveClose a no-show record by hand
GET/api/v1/admin/no-show/statisticsNo-show figures

These need a staff JWT with admin:write.

process-fee body:

{ "auto_charge": true, "notify_customer": true }

auto_charge and notify_customer default to true. The acting admin is taken from the token; a marked_by_user_id (or, on resolve, resolved_by_user_id) in the body is ignored. resolve takes an optional resolution_notes. The response has success and data with fee_amount (cents), fee_amount_formatted, payment_intent_id, charged_at and notification_sent.