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_minutes and max_duration_minutes

  • The backend validates prevent_unbookable_gaps when creating bookings

  • When slot_prices is present, use those prices for specific time slots; otherwise use price_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_datetime must align to booking_interval_minutes

  • start_datetime must be in the future

  • Duration must be between min_duration_minutes and max_duration_minutes

  • If prevent_unbookable_gaps is 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:

  1. Verifies the Stripe signature

  2. Marks the booking as paid

  3. Creates client commission record (if booking was made via Client API)

  4. Sends confirmation emails to:

  5. User: Booking confirmation with venue details

  6. Venue Operator: New booking notification with user contact info

Client Commission Tracking

When a booking is made through a Client API:

  • A ClientCommission record is created automatically

  • Commission is calculated based on the client's commission_percent setting

  • 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_minutes of 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