Client Booking Flow
This guide describes the complete flow for a client application to book a resource for a user.
Overview
1. Browse Resources → 2. Check Availability → 3. Create Booking → 4. Pay → 5. Confirmation
Authentication Modes
The API supports two authentication modes:
Direct User Authentication
For end-user apps where users have their own accounts:
Authorization: Token USER_TOKEN
The authenticated user is the booking owner.
Client API Authentication (B2B)
For client applications acting on behalf of users:
Authorization: Token CLIENT_API_TOKEN
X-User-External-ID: your-user-123
Or using our internal user ID:
Authorization: Token CLIENT_API_TOKEN
X-User-ID: 550e8400-e29b-41d4-a716-446655440000
Client apps must first register/map users before creating bookings.
Client API: User Registration
Before a client app can create bookings on behalf of users, it must register them:
curl -X POST "https://api.matchengine.de/api/v1/client/users/register/" \
-H "Authorization: Token CLIENT_API_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"external_id": "your-user-123",
"email": "user@example.com",
"first_name": "John",
"last_name": "Doe"
}'
Response (201 Created):
{
"user_id": "550e8400-e29b-41d4-a716-446655440000",
"external_id": "your-user-123",
"email": "user@example.com",
"created": true,
"mapping_created": true
}
If the user already exists (by email or external_id), the existing mapping is returned with created: false.
Lookup Existing User
curl -X GET "https://api.matchengine.de/api/v1/client/users/lookup/?external_id=your-user-123" \
-H "Authorization: Token CLIENT_API_TOKEN"
List All Mapped Users
curl -X GET "https://api.matchengine.de/api/v1/client/users/mappings/" \
-H "Authorization: Token CLIENT_API_TOKEN"
Step 1: Browse Available Resources
First, fetch the list of bookable resources. You can filter by venue, activity type, or other attributes.
# List all resources
curl -X GET "https://api.matchengine.de/api/v1/resources/"
# Filter by venue
curl -X GET "https://api.matchengine.de/api/v1/resources/?venue=1"
# Filter by activity type
curl -X GET "https://api.matchengine.de/api/v1/resources/?activity=5"
Get detailed information about a specific resource:
curl -X GET "https://api.matchengine.de/api/v1/resources/1/"
Step 2: Check Availability
Once the user selects a resource, fetch availability data for their desired date range.
Direct API Request
curl -X GET "https://api.matchengine.de/api/v1/resources/availability/?resource_id=1&start_date=2025-01-15&end_date=2025-01-16"
Client API Request
curl -X GET "https://api.matchengine.de/api/v1/resources/availability/?resource_external_id=your-court-123&start_date=2025-01-15&end_date=2025-01-16" \
-H "Authorization: Token CLIENT_API_TOKEN"
Response:
{
"resource_id": 1,
"resource_name": "Court 1",
"start_date": "2025-01-15",
"end_date": "2025-01-16",
"timezone": "Europe/Berlin",
"booking_interval_minutes": 30,
"min_duration_minutes": 60,
"max_duration_minutes": 180,
"prevent_unbookable_gaps": true,
"min_advance_booking_minutes": 60,
"max_advance_booking_days": 30,
"available_ranges": [
{
"date": "2025-01-15",
"start_time": "08:00:00",
"end_time": "14:00:00",
"price_per_hour": "25.00",
"currency": "EUR",
"label": "",
"slot_prices": null
},
{
"date": "2025-01-15",
"start_time": "15:30:00",
"end_time": "22:00:00",
"price_per_hour": "25.00",
"currency": "EUR",
"label": "",
"slot_prices": {"16:00": "35.00", "17:00": "35.00"}
}
]
}
Response Fields
| Field | Description |
|---|---|
available_ranges |
Pre-calculated time ranges that are actually available (bookings and unavailability already subtracted) |
booking_interval_minutes |
Start times must align to this interval (e.g., 30 = :00, :30) |
min_duration_minutes |
Minimum booking duration |
max_duration_minutes |
Maximum booking duration (null = no limit) |
prevent_unbookable_gaps |
If true, bookings that would leave unbookable gaps are rejected |
slot_prices |
Per-slot prices when dynamic pricing applies (null if prices are uniform) |
Using Available Ranges
The available_ranges are pre-calculated - no frontend calculation needed. Each range represents a continuous block of time that is available for booking.
Example: If 14:00-15:30 is booked, you'll see two separate ranges: 08:00-14:00 and 15:30-22:00.
Key points:
-
Start times must align to
booking_interval_minutes(e.g., 08:00, 08:30, 09:00 for 30-min intervals) -
Users can book flexible durations between
min_duration_minutesandmax_duration_minutes -
The backend validates
prevent_unbookable_gapswhen creating bookings -
When
slot_pricesis present, use those prices for specific time slots; otherwise useprice_per_hour
Step 3: Create Booking
After the user selects their desired time, create a booking. This requires authentication.
Direct User Request
curl -X POST "https://api.matchengine.de/api/v1/bookings/" \
-H "Authorization: Token USER_AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"resource": 1,
"start_datetime": "2025-01-15T10:00:00+01:00",
"end_datetime": "2025-01-15T11:30:00+01:00",
"participant_count": 2,
"notes": "Optional booking notes"
}'
Client API Request (on behalf of user)
Client apps can use resource_external_id instead of resource:
curl -X POST "https://api.matchengine.de/api/v1/bookings/" \
-H "Authorization: Token CLIENT_API_TOKEN" \
-H "X-User-External-ID: your-user-123" \
-H "Content-Type: application/json" \
-d '{
"resource_external_id": "your-court-123",
"start_datetime": "2025-01-15T10:00:00+01:00",
"end_datetime": "2025-01-15T11:30:00+01:00",
"participant_count": 2,
"notes": "Optional booking notes"
}'
When using Client API authentication, the booking is automatically linked to the client for commission tracking.
Request Fields
| Field | Required | Description |
|---|---|---|
resource |
Yes* | MatchEngine resource ID (for direct API) |
resource_external_id |
Yes* | Your external ID for the resource (for client API) |
start_datetime |
Yes | ISO 8601 datetime with timezone |
end_datetime |
Yes | ISO 8601 datetime with timezone |
participant_count |
No | Number of participants (default: 1) |
notes |
No | Optional booking notes |
*Either resource or resource_external_id is required.
Validation Rules
-
start_datetimemust align tobooking_interval_minutes -
start_datetimemust be in the future -
Duration must be between
min_duration_minutesandmax_duration_minutes -
If
prevent_unbookable_gapsis true, booking must not create gaps <min_duration_minutes
Response (201 Created)
{
"id": "550e8400-e29b-41d4-a716-446655440000",
"reference_code": "BK-250115-A7F3",
"resource": 1,
"resource_name": "Court 1",
"venue_name": "Sports Center",
"venue_address": "123 Main St, Munich",
"start_datetime": "2025-01-15T10:00:00+01:00",
"end_datetime": "2025-01-15T11:30:00+01:00",
"duration_minutes": 90,
"participant_count": 2,
"price_per_unit": "25.00",
"pricing_unit": "hour",
"total_price": "37.50",
"currency": "EUR",
"status": "pending",
"can_cancel": true,
"is_upcoming": true,
"notes": "",
"confirmed_at": null,
"paid_at": null,
"cancelled_at": null,
"created_at": "2025-01-14T12:00:00Z",
"updated_at": "2025-01-14T12:00:00Z"
}
The booking is created with status pending. Store the id for the next step.
Step 4: Process Payment
Create a Stripe PaymentIntent and complete the payment using Stripe.js on the frontend.
4.1 Create Payment Intent
Direct User Request:
curl -X POST "https://api.matchengine.de/api/v1/bookings/550e8400-e29b-41d4-a716-446655440000/create-payment-intent/" \
-H "Authorization: Token USER_AUTH_TOKEN"
Client API Request:
curl -X POST "https://api.matchengine.de/api/v1/bookings/550e8400-e29b-41d4-a716-446655440000/create-payment-intent/" \
-H "Authorization: Token CLIENT_API_TOKEN" \
-H "X-User-External-ID: your-user-123"
Response:
{
"client_secret": "pi_3ABC123_secret_XYZ789",
"payment_intent_id": "pi_3ABC123",
"amount": "37.50",
"currency": "EUR",
"publishable_key": "pk_live_xxxxx",
"stripe_account_id": "acct_1ABC123"
}
| Field | Description |
|---|---|
client_secret |
Secret for Stripe.js to confirm payment |
payment_intent_id |
Stripe PaymentIntent ID |
amount |
Total amount to charge |
currency |
Currency code (e.g., EUR) |
publishable_key |
Stripe publishable key for Stripe.js initialization |
stripe_account_id |
Connected account ID (only present when venue uses Stripe Connect) |
4.2 Complete Payment (Frontend)
Use Stripe.js to confirm the payment with the client_secret:
// Initialize Stripe with the publishable key from the API response
// For venues using Stripe Connect, pass the stripe_account_id option
const stripeOptions = paymentResponse.stripe_account_id
? { stripeAccount: paymentResponse.stripe_account_id }
: {};
const stripe = Stripe(paymentResponse.publishable_key, stripeOptions);
// Confirm payment
const { error, paymentIntent } = await stripe.confirmPayment({
clientSecret: paymentResponse.client_secret,
confirmParams: {
return_url: 'https://yourapp.com/booking/success',
},
});
if (error) {
// Handle error - show message to user
console.error(error.message);
} else if (paymentIntent.status === 'succeeded') {
// Payment successful - redirect or show success
}
Step 5: Webhook & Confirmation
After successful payment, Stripe sends a webhook to our server:
POST /webhooks/stripe/
The webhook handler:
-
Verifies the Stripe signature
-
Marks the booking as
paid -
Creates client commission record (if booking was made via Client API)
-
Sends confirmation emails to:
-
User: Booking confirmation with venue details
-
Venue Operator: New booking notification with user contact info
Client Commission Tracking
When a booking is made through a Client API:
-
A
ClientCommissionrecord is created automatically -
Commission is calculated based on the client's
commission_percentsetting -
Commissions are aggregated monthly for payout via bank transfer
-
This avoids per-transaction Stripe fees on referral payments
Complete Flow Diagram
Direct User Flow
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ User │ │ API │ │ Stripe │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ GET /resources/ │ │
│──────────────────>│ │
│ [resources list] │ │
│<──────────────────│ │
│ │ │
│ GET /resources/availability/?resource_id=1
│──────────────────>│ │
│ [available_ranges] │
│<──────────────────│ │
│ │ │
│ POST /bookings/ │ │
│ Token: USER_TOKEN│ │
│──────────────────>│ │
│ {booking pending}│ │
│<──────────────────│ │
│ │ │
│ POST /bookings/{id}/create-payment-intent/
│──────────────────>│ Create PI │
│ │──────────────────>│
│ {client_secret} │<──────────────────│
│<──────────────────│ │
│ │ │
│ stripe.confirmPayment(client_secret) │
│──────────────────────────────────────>│
│ │ Webhook │
│ │<──────────────────│
│ │ [Mark paid, send emails]
Client API Flow (B2B)
┌─────────────┐ ┌─────────────┐ ┌─────────────┐
│ Client App │ │ API │ │ Stripe │
└──────┬──────┘ └──────┬──────┘ └──────┬──────┘
│ │ │
│ POST /client/users/register/ │
│ Token: CLIENT_TOKEN │
│ {"external_id": "usr-123", ...} │
│──────────────────>│ │
│ {user_id, external_id} │
│<──────────────────│ │
│ │ │
│ GET /resources/availability/?resource_external_id=court-1
│ Token: CLIENT_TOKEN │
│──────────────────>│ │
│ [available_ranges] │
│<──────────────────│ │
│ │ │
│ POST /bookings/ │ │
│ Token: CLIENT_TOKEN │
│ X-User-External-ID: usr-123 │
│ {"resource_external_id": "court-1"} │
│──────────────────>│ │
│ {booking pending, client tracked} │
│<──────────────────│ │
│ │ │
│ POST /bookings/{id}/create-payment-intent/
│ X-User-External-ID: usr-123 │
│──────────────────>│ Create PI │
│ │──────────────────>│
│ {client_secret} │<──────────────────│
│<──────────────────│ │
│ │ │
│ stripe.confirmPayment(client_secret) │
│──────────────────────────────────────>│
│ │ Webhook │
│ │<──────────────────│
│ │ [Mark paid, create commission, send emails]
Error Handling
User Identification Missing (Client API)
When using Client API authentication without specifying the user:
{
"error": "User identification required. Provide X-User-ID or X-User-External-ID header."
}
Solution: Include X-User-ID or X-User-External-ID header with the request.
User Not Found (Client API)
When the external_id has no mapping:
{
"error": "No user found with external_id: your-user-123"
}
Solution: First register the user via POST /api/v1/client/users/register/.
Not a Client API User
When trying to access client-only endpoints with a regular user token:
{
"detail": "You do not have permission to perform this action."
}
Solution: Use a token from a Client's api_user service account.
Resource Not Found (Client API)
When resource_external_id has no mapping for this client:
{
"resource_external_id": ["No resource found with external_id: your-court-123"]
}
Solution: First register the resource mapping via POST /api/v1/client/resources/register/.
Slot No Longer Available
If the slot was booked by another user between availability check and booking creation:
{
"non_field_errors": ["The requested time slot is not available"]
}
Solution: Fetch availability again and ask user to select another slot.
Invalid Time Interval
If the start time doesn't align to the booking interval:
{
"non_field_errors": ["Booking start time must align to 30-minute intervals (e.g., :00, :30 for 30-minute intervals)"]
}
Solution: Ensure start times are calculated from the booking_interval_minutes constraint.
Unbookable Gap
If the booking would create a gap that's too short for anyone else to book:
{
"non_field_errors": ["This would leave a 30-minute gap that cannot be booked (minimum booking is 60 minutes)"]
}
Solution: Adjust the booking duration to either:
-
End exactly where the next booking starts, or
-
Leave at least
min_duration_minutesof free time
Payment Failed
If payment fails, the booking remains in pending status. The user can retry payment:
# Create a new payment intent and try again
POST /api/v1/bookings/{id}/create-payment-intent/
Booking Already Paid
{
"error": "This booking has already been paid"
}
Environment Variables
Ensure these are configured on the server:
# Stripe
STRIPE_SECRET_KEY=sk_live_xxxxx
STRIPE_WEBHOOK_SECRET=whsec_xxxxx
# Mailgun (for confirmation emails)
MAILGUN_API_KEY=key-xxxxx
Related Documentation
-
Resources API - Resource endpoints reference
-
Bookings API - Booking endpoints reference
-
Authentication - How to authenticate API requests
-
Client API - Client integration and user mapping