Bookly API (1.0.0)

Booking, payment and admin API for a photographer's booking site in Kigali.

The API behind the Bookly booking site. It serves three kinds of caller:

  • The public: anonymous visitors browsing services, checking availability, booking a slot and paying its booking fee.
  • A booking's client: no account; every client request carries a capability token in its path, emailed when the booking is confirmed.
  • The admin (the photographer): an Authorization: Bearer JWT from POST /api/admin/auth/login.

Read the API conventions first: error shapes, the 400-versus-422 rule, time zones and money are the same on every operation.

This file is written by hand. A backend test (src/openapi.test.ts) fails when a route exists without an operation here, or the other way round, and validates real responses against these schemas.

Health

Liveness check

Responses

Response Schema: application/json
ok
required
any
Value: true

Response samples

Content type
application/json
{
  • "ok": true
}

Catalogue & availability

Anonymous browsing and pricing.

List active services

Active services, each with their active packages and the add-ons they offer. Anonymous.

Responses

Response Schema: application/json
required
Array of objects (PublicService)

Response samples

Content type
application/json
{
  • "services": [
    ]
}

One service by slug

path Parameters
slug
required
string

Responses

Response Schema: application/json
required
object (PublicServiceDetail)

A service plus the booking-fee rate that applies to it, so a page can show a live quote.

Response samples

Content type
application/json
{
  • "service": {
    }
}

Price a basket

The API's authority over money. The price is always computed from the catalogue, never from the body.

Request Body schema: application/json
required
packageId
required
string <uuid> (Uuid)
addonIds
Array of strings <uuid> (Uuid) <= 50 items unique [ items <uuid > ]
Default: []

Responses

Response Schema: application/json
required
object (Quote)

The API's price for a basket. Only the booking fee is rounded (half-up, to a whole franc); the session fee is the remainder, so the two always add up to the total.

Request samples

Content type
application/json
{
  • "packageId": "471cc1d4-ec27-4504-b7c2-949af95662bc",
  • "addonIds": [ ]
}

Response samples

Content type
application/json
{
  • "quote": {
    }
}

Bookable starts for a package in a month

Every Kigali date of the month with the slot starts a visitor may book for this package right now. Cache-Control: no-store. Anonymous.

query Parameters
packageId
required
string <uuid> (Uuid)
month
required
string^\d{4}-(0[1-9]|1[0-2])$

YYYY-MM.

Responses

Response Schema: application/json
required
Array of objects

Every Kigali date of the month, in order, each with the starts still bookable for this package right now.

Response samples

Content type
application/json
{
  • "days": [
    ]
}

Bookings

Creating a booking and holding its slot.

Create a booking and hold its slot

Turns a chosen slot and a filled form into a pending_payment booking holding its slot for the hold window. Anonymous. The returned checkoutToken opens the booking's checkout.

Request Body schema: application/json
required
packageId
required
string <uuid> (Uuid)
addonIds
Array of strings <uuid> (Uuid) <= 50 items unique [ items <uuid > ]
Default: []
startsAt
required
string <date-time> (OffsetInstant)

Must be one of the starts that GET /api/availability offers this package right now; anything else is a 422 naming startsAt.

fullName
required
string [ 1 .. 200 ] characters
email
required
string <= 320 characters

Stored lowercased. A mistyped address is a 422, not a 400.

phone
required
string <= 40 characters
location
required
string [ 1 .. 500 ] characters
partySize
integer or null [ 1 .. 1000 ]
Default: null
specialRequests
string or null <= 2000 characters
Default: null
consent
required
boolean

Must be true. Recorded as consent_at (Law N° 058/2021). Omitted or false is a 422 naming consent.

Responses

Response Schema: application/json
required
object

Request samples

Content type
application/json
{
  • "packageId": "471cc1d4-ec27-4504-b7c2-949af95662bc",
  • "addonIds": [ ],
  • "startsAt": "2019-08-24T14:15:22Z",
  • "fullName": "string",
  • "email": "string",
  • "phone": "+250788123456",
  • "location": "string",
  • "partySize": null,
  • "specialRequests": null,
  • "consent": true
}

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Checkout & payments

Paying the booking fee while the hold lasts, and watching a payment settle.

What the active provider collects

The active provider and the methods its pay page may offer. Cache-Control: no-store. Anonymous.

Responses

Response Schema: application/json
provider
required
string
methods
required
Array of strings (PaymentMethod)
Items Enum: "momo_mtn" "momo_airtel" "card"

Response samples

Content type
application/json
{
  • "provider": "mtn_momo_direct",
  • "methods": [
    ]
}

The checkout for a held booking

What the pay page shows. Addressed by the booking reference and its checkout token; a wrong token is a 404, as an unknown reference is. Cache-Control: no-store.

path Parameters
reference
required
string (BookingReference) ^BKY-\d{4}-[0-9A-HJKMNP-TV-Z]{5}$
Examples: BKY-2610-7MAR5

The booking's quotable reference: BKY-, the Kigali year and month it was made, and five Crockford base-32 characters. Not a secret.

token
required
string^[A-Za-z0-9_-]{43}$

The checkout token returned when the booking was created: an HMAC of the reference, 43 base64url characters. Good only for paying the booking fee while the hold lasts.

Responses

Response Schema: application/json
required
object

Response samples

Content type
application/json
{
  • "checkout": {
    }
}

Pay the booking fee

Starts a booking-fee payment; the payer's phone is prompted. Cache-Control: no-store.

path Parameters
reference
required
string (BookingReference) ^BKY-\d{4}-[0-9A-HJKMNP-TV-Z]{5}$
Examples: BKY-2610-7MAR5

The booking's quotable reference: BKY-, the Kigali year and month it was made, and five Crockford base-32 characters. Not a secret.

token
required
string^[A-Za-z0-9_-]{43}$

The checkout token returned when the booking was created: an HMAC of the reference, 43 base64url characters. Good only for paying the booking fee while the hold lasts.

Request Body schema: application/json
required
method
required
string
Enum: "momo_mtn" "momo_airtel" "card"

One of the active provider's methods (GET /api/payment-methods). Anything else is a 422.

phone
required
string <= 40 characters

The number to prompt. Rwandan numbers may be local (0788…) or international (+250788…); an unusable number is a 422.

Responses

Response Schema: application/json
required
object

Request samples

Content type
application/json
{
  • "method": "momo_mtn",
  • "phone": "+250788123456"
}

Response samples

Content type
application/json
{
  • "payment": {
    }
}

Where a payment stands

The confirmation page polls this while a Mobile Money payment settles — often 30 seconds or more. Addressed by the random our_ref the pay page was handed; shows no contact details. Cache-Control: no-store.

path Parameters
ourRef
required
string <uuid> (Uuid)

Our id for one payment attempt, returned when it was started.

Responses

Response Schema: application/json
required
object
required
object

Response samples

Content type
application/json
{
  • "payment": {
    },
  • "booking": {
    }
}

Client booking

A client's own booking, addressed by its access token.

View my booking

The booking the token addresses, and records the token's use. Any token that does not resolve is a 404.

path Parameters
token
required
string^[A-Za-z0-9_-]{16,256}$

The booking's access token, from its confirmation email or a resend: 256 random bits, base64url. It is the client's whole identity, so treat the URL as a secret. Unknown, expired (365 days) and superseded tokens are all a 404.

Responses

Response Schema: application/json
required
object (ClientBooking)

A booking as its own client sees it. Neither the booking's id nor the token that reached it appears anywhere in it, and the contact email is masked (a•••••@example.com).

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Cancel my booking

Cancellable up to the shoot, and only on a confirmed booking. The booking fee is not refunded (it is non-refundable on a client cancellation).

path Parameters
token
required
string^[A-Za-z0-9_-]{16,256}$

The booking's access token, from its confirmation email or a resend: 256 random bits, base64url. It is the client's whole identity, so treat the URL as a secret. Unknown, expired (365 days) and superseded tokens are all a 404.

Responses

Response Schema: application/json
required
object (ClientBooking)

A booking as its own client sees it. Neither the booking's id nor the token that reached it appears anywhere in it, and the contact email is masked (a•••••@example.com).

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Change my contact email

Stores the new address as pending and emails a confirmation link to it; nothing changes until that link is followed, so a link-holder who is not the client cannot redirect the booking's email without also owning the new inbox. At most three requests a day per booking.

path Parameters
token
required
string^[A-Za-z0-9_-]{16,256}$

The booking's access token, from its confirmation email or a resend: 256 random bits, base64url. It is the client's whole identity, so treat the URL as a secret. Unknown, expired (365 days) and superseded tokens are all a 404.

Request Body schema: application/json
required
email
required
string <= 254 characters

A mistyped address is a 422.

Responses

Response Schema: application/json
required
object (ClientBooking)

A booking as its own client sees it. Neither the booking's id nor the token that reached it appears anywhere in it, and the contact email is masked (a•••••@example.com).

Request samples

Content type
application/json
{
  • "email": "string"
}

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Pay my session fee

Starts a session-fee payment for what the booking owes. Only mounted when a payment provider is configured. The amount is computed server-side from the booking, never sent by the client.

path Parameters
token
required
string^[A-Za-z0-9_-]{16,256}$

The booking's access token, from its confirmation email or a resend: 256 random bits, base64url. It is the client's whole identity, so treat the URL as a secret. Unknown, expired (365 days) and superseded tokens are all a 404.

Request Body schema: application/json
required
method
required
string
Enum: "momo_mtn" "momo_airtel" "card"

One of the active provider's methods (GET /api/payment-methods). Anything else is a 422.

phone
required
string <= 40 characters

The number to prompt. Rwandan numbers may be local (0788…) or international (+250788…); an unusable number is a 422.

Responses

Response Schema: application/json
required
object

Request samples

Content type
application/json
{
  • "method": "momo_mtn",
  • "phone": "+250788123456"
}

Response samples

Content type
application/json
{
  • "payment": {
    }
}

Email confirmations

Confirming a booking's change of contact email from the new address.

Confirm a change of contact email

Follows the link sent to the new address when a client asked to change their booking's email. A POST, not a GET, so a mail scanner opening the link cannot trigger the change. Unknown, expired, replaced and already-used tokens are all a 404.

path Parameters
token
required
string^[A-Za-z0-9_-]{16,256}$

The booking's access token, from its confirmation email or a resend: 256 random bits, base64url. It is the client's whole identity, so treat the URL as a secret. Unknown, expired (365 days) and superseded tokens are all a 404.

Responses

Response Schema: application/json
confirmed
required
any
Value: true

Response samples

Content type
application/json
{
  • "confirmed": true
}

Admin auth

Login and password reset for the one admin account.

Log in

Email and password for the one admin account. Returns a bearer JWT valid 8 hours. Unknown email, wrong password and a locked account are one answer — 401 invalid_credentials — so none is disclosed; repeated failures lock the account and email the photographer. Cache-Control: no-store.

Request Body schema: application/json
required
email
required
string [ 1 .. 320 ] characters
password
required
string [ 1 .. 1024 ] characters

Responses

Response Schema: application/json
required
object (PublicAdmin)

The only shape of the admin that leaves the API. No hash, reset or lockout state.

token
required
string

Send as Authorization: Bearer <token>. Keep it in memory or sessionStorage, never localStorage.

tokenType
required
any
Value: "Bearer"
expiresAt
required
string <date-time> (Instant)

An ISO-8601 instant, always UTC in responses (2026-10-14T07:00:00.000Z).

Request samples

Content type
application/json
{
  • "email": "string",
  • "password": "string"
}

Response samples

Content type
application/json
{
  • "admin": {
    },
  • "token": "string",
  • "tokenType": "Bearer",
  • "expiresAt": "2019-08-24T14:15:22Z"
}

Request a password reset

If the address is the admin's, a single-use, time-limited reset link is emailed. The answer is always 202, whether or not it is, so the endpoint does not reveal the admin's address.

Request Body schema: application/json
required
email
required
string [ 1 .. 320 ] characters

Responses

Request samples

Content type
application/json
{
  • "email": "string"
}

Response samples

Content type
application/json
{
  • "ok": true
}

Set a new password from a reset link

Consumes the emailed token and sets the new password, which revokes every token already issued. The token works once.

Request Body schema: application/json
required
token
required
string [ 1 .. 512 ] characters
newPassword
required
string [ 12 .. 1024 ] characters

Responses

Request samples

Content type
application/json
{
  • "token": "string",
  • "newPassword": "stringstring"
}

Response samples

Content type
application/json
Example
{
  • "error": "invalid_request"
}

The signed-in admin

Authorizations:
adminBearer

Responses

Response Schema: application/json
required
object (PublicAdmin)

The only shape of the admin that leaves the API. No hash, reset or lockout state.

Response samples

Content type
application/json
{
  • "admin": {
    }
}

Admin availability

Working hours and blocked time.

List working hours

Authorizations:
adminBearer

Responses

Response Schema: application/json
required
Array of objects (WorkingHours)

Response samples

Content type
application/json
{
  • "workingHours": [
    ]
}

Add a weekly rule or dated override

Authorizations:
adminBearer
Request Body schema: application/json
required
One of
weekday
required
integer [ 0 .. 6 ]
opensMinute
integer or null [ 0 .. 1440 ]
Default: null
closesMinute
integer or null [ 0 .. 1440 ]
Default: null
isOpen
boolean
Default: true
note
string or null <= 500 characters
Default: null

Responses

Response Schema: application/json
required
object (WorkingHours)

A weekly rule (weekday set) or a one-date override (effectiveDate set), never both.

Request samples

Content type
application/json
Example
{
  • "weekday": 0,
  • "opensMinute": null,
  • "closesMinute": null,
  • "isOpen": true,
  • "note": null
}

Response samples

Content type
application/json
{
  • "workingHours": {
    }
}

Replace a working-hours row

Replaces the whole row, so a rule can move between a weekday and a date.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
One of
weekday
required
integer [ 0 .. 6 ]
opensMinute
integer or null [ 0 .. 1440 ]
Default: null
closesMinute
integer or null [ 0 .. 1440 ]
Default: null
isOpen
boolean
Default: true
note
string or null <= 500 characters
Default: null

Responses

Response Schema: application/json
required
object (WorkingHours)

A weekly rule (weekday set) or a one-date override (effectiveDate set), never both.

Request samples

Content type
application/json
Example
{
  • "weekday": 0,
  • "opensMinute": null,
  • "closesMinute": null,
  • "isOpen": true,
  • "note": null
}

Response samples

Content type
application/json
{
  • "workingHours": {
    }
}

Delete a working-hours row

Deleting a weekday rule closes that weekday; deleting an override restores the weekly rule for its date.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)

Responses

Response samples

Content type
application/json
{
  • "error": "unauthenticated"
}

List blocks

Authorizations:
adminBearer

Responses

Response Schema: application/json
required
Array of objects (Block)

Response samples

Content type
application/json
{
  • "blocks": [
    ]
}

Block time

Authorizations:
adminBearer
Request Body schema: application/json
required
One of
isAllDay
required
any
Value: true
startDate
required
string <date> (KigaliDate)

A calendar date in Africa/Kigali (UTC+2, no DST), YYYY-MM-DD. Years 0000–0099 are refused.

endDate
required
string <date> (KigaliDate)

Inclusive; not before startDate.

reason
string or null <= 500 characters
Default: null
confirm
boolean
Default: false

Responses

Response Schema: application/json
required
object (Block)

Request samples

Content type
application/json
Example
{
  • "isAllDay": true,
  • "startDate": "2019-08-24",
  • "endDate": "2019-08-24",
  • "reason": null,
  • "confirm": false
}

Response samples

Content type
application/json
{
  • "block": {
    }
}

Change a block

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
One of
isAllDay
required
any
Value: true
startDate
required
string <date> (KigaliDate)

A calendar date in Africa/Kigali (UTC+2, no DST), YYYY-MM-DD. Years 0000–0099 are refused.

endDate
required
string <date> (KigaliDate)

Inclusive; not before startDate.

reason
string or null <= 500 characters
Default: null
confirm
boolean
Default: false

Responses

Response Schema: application/json
required
object (Block)

Request samples

Content type
application/json
Example
{
  • "isAllDay": true,
  • "startDate": "2019-08-24",
  • "endDate": "2019-08-24",
  • "reason": null,
  • "confirm": false
}

Response samples

Content type
application/json
{
  • "block": {
    }
}

Delete a block

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)

Responses

Response samples

Content type
application/json
{
  • "error": "unauthenticated"
}

Admin settings

The five operating values.

The five operating values

Authorizations:
adminBearer

Responses

Response Schema: application/json
required
object (Settings)

Response samples

Content type
application/json
{
  • "settings": {
    }
}

Change settings

Any subset of the five. Each is range-checked here, so a value out of range is a 422 rather than a database error.

Authorizations:
adminBearer
Request Body schema: application/json
required
bookingFeeRate
number [ 0 .. 1 ]
minLeadTimeMinutes
integer [ 0 .. 2147483647 ]
holdMinutes
integer [ 1 .. 2147483647 ]
bufferMinutes
integer [ 0 .. 2147483647 ]
deliveryExpiryDays
integer [ 1 .. 2147483647 ]

Responses

Response Schema: application/json
required
object (Settings)

Request samples

Content type
application/json
{
  • "bookingFeeRate": 0,
  • "minLeadTimeMinutes": 0,
  • "holdMinutes": 1,
  • "bufferMinutes": 0,
  • "deliveryExpiryDays": 1
}

Response samples

Content type
application/json
{
  • "settings": {
    }
}

Admin calendar

Bookings, holds and blocks for a range

Authorizations:
adminBearer
query Parameters
from
required
string <date> (KigaliDate)

A calendar date in Africa/Kigali (UTC+2, no DST), YYYY-MM-DD. Years 0000–0099 are refused.

to
required
string <date> (KigaliDate)

Inclusive. At most 62 days after from.

Responses

Response Schema: application/json
required
Array of objects

What occupies time. pending_payment appears only while its hold is live; cancelled and expired bookings are left out.

required
Array of objects

Response samples

Content type
application/json
{
  • "bookings": [
    ],
  • "blocks": [
    ]
}

Admin bookings

The bookings list, one booking, and everything done to it.

List bookings

Filtered by stage, status and shoot date, searched by reference, name, email or phone, and paged by number with a total.

Authorizations:
adminBearer
query Parameters
BookingStage (string) or Array of BookingStage (strings)

Repeatable. Display stages (see BookingStage).

BookingStatus (string) or Array of BookingStatus (strings)

Repeatable. Kept for links made before stages existed.

from
string <date> (KigaliDate)

A calendar date in Africa/Kigali (UTC+2, no DST), YYYY-MM-DD. Years 0000–0099 are refused.

to
string <date> (KigaliDate)

A calendar date in Africa/Kigali (UTC+2, no DST), YYYY-MM-DD. Years 0000–0099 are refused.

search
string <= 200 characters
page
integer >= 1

1-based. Past the end, the last page is answered.

pageSize
integer [ 1 .. 100 ]
Default: 25

Responses

Response Schema: application/json
required
Array of objects (BookingListRow)

Newest shoot first (start, then id, descending).

total
required
integer >= 0

Every booking the filter matches, across all pages.

page
required
integer >= 1

The page answered; a page past the end answers the last one.

pageSize
required
integer [ 1 .. 100 ]
pageCount
required
integer >= 1

Response samples

Content type
application/json
{
  • "bookings": [
    ],
  • "total": 0,
  • "page": 1,
  • "pageSize": 1,
  • "pageCount": 1
}

One booking

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)

Responses

Response Schema: application/json
required
object (AdminBooking)

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Reschedule a booking

Keeps the booking, its reference, token and payments; moves the slot and its buffer to a free slot.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
startsAt
required
string <date-time> (OffsetInstant)

An ISO-8601 instant with an explicit offset (2026-10-14T09:00:00+02:00 or …Z). A bare local time is refused: it would be read in the server's zone.

Responses

Response Schema: application/json
required
object (AdminBooking)

Request samples

Content type
application/json
{
  • "startsAt": "2019-08-24T14:15:22Z"
}

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Cancel a booking

Releases the slot and marks the booking fee refund_due for a full refund. An optional reason is shown to the client.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
optional
reason
string or null [ 1 .. 1000 ] characters

Responses

Response Schema: application/json
required
object (AdminBooking)

Request samples

Content type
application/json
{
  • "reason": "string"
}

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Mark a booking completed

Completing means paid. A booking that still owes money is a 409 balance_due.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)

Responses

Response Schema: application/json
required
object (AdminBooking)

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Mark a no-show

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)

Responses

Response Schema: application/json
required
object (AdminBooking)

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Add a post-shoot add-on

Adds a catalogue add-on to a booking whose shoot has begun. The price is read from the catalogue, never sent.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
addonId
required
string <uuid> (Uuid)
quantity
integer [ 1 .. 99 ]
Default: 1

Responses

Response Schema: application/json
required
object (AdminBooking)

Request samples

Content type
application/json
{
  • "addonId": "ed930881-3f27-4e25-9365-ff53ffbcfa14",
  • "quantity": 1
}

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Remove a post-shoot add-on

Removes an add-on still removable (post-shoot, not yet paid for).

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
addonId
required
string <uuid> (Uuid)

Responses

Response Schema: application/json
required
object (AdminBooking)

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Ask the client for the session fee

Opens a session-fee request for what the booking owes and emails the client. Only mounted when a payment provider is configured; without one the route is a 404.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)

Responses

Response Schema: application/json
required
object (AdminBooking)

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Record a cash payment

Records cash the photographer was handed, up to what is owed. Refused while an online payment may be in flight; it voids an open online request so the same money cannot be taken twice, and emails nobody.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
amountRwf
required
integer [ 1 .. 2147483647 ]
note
string or null [ 1 .. 500 ] characters

Responses

Response Schema: application/json
required
object (AdminBooking)

Request samples

Content type
application/json
{
  • "amountRwf": 1,
  • "note": "string"
}

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Save the photo-delivery link

Sets the external photo link and its expiry on a completed booking. The link must be https.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
url
required
string <uri> <= 2000 characters

Must be https.

expiresOn
string <date> (KigaliDate)

Defaults to today plus the delivery-expiry setting.

note
string or null [ 1 .. 2000 ] characters

Responses

Response Schema: application/json
required
object (AdminBooking)

Request samples

Content type
application/json
{}

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Send the photos

Emails the saved delivery link, optionally to another address for this one email. The booking's own contact email is not changed.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
optional
recipient
string <= 254 characters

A one-off address; a mistyped one is a 422.

Responses

Response Schema: application/json
required
object (AdminBooking)

Request samples

Content type
application/json
{
  • "recipient": "string"
}

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Add a note to the client

A plain-text note, emailed to the client unless email is false.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
body
required
string [ 1 .. 1000 ] characters
email
boolean
Default: true

Responses

Response Schema: application/json
required
object (AdminBooking)

Request samples

Content type
application/json
{
  • "body": "string",
  • "email": true
}

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Delete a note

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
noteId
required
string <uuid> (Uuid)

Responses

Response Schema: application/json
required
object (AdminBooking)

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Admin payments

Record a refund

Records that the photographer refunded a payment by hand (the system never moves money). Needs the MoMo or bank reference it was sent with.

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
reference
required
string [ 1 .. 100 ] characters
refundedAt
string <date-time> (OffsetInstant)

An ISO-8601 instant with an explicit offset (2026-10-14T09:00:00+02:00 or …Z). A bare local time is refused: it would be read in the server's zone.

Responses

Response Schema: application/json
required
object (AdminBooking)

Request samples

Content type
application/json
{
  • "reference": "string",
  • "refundedAt": "2019-08-24T14:15:22Z"
}

Response samples

Content type
application/json
{
  • "booking": {
    }
}

Admin catalogue

Services, packages and add-ons.

The whole catalogue

Every service (active or not) with its packages and own add-ons, and the add-ons offered on every service.

Authorizations:
adminBearer

Responses

Response Schema: application/json
required
Array of objects

Every service, active or not, with all its packages and its own add-ons.

required
Array of objects (AdminAddon)

Add-ons offered on every service (serviceId null).

Response samples

Content type
application/json
{
  • "services": [
    ],
  • "sharedAddons": [
    ]
}

Create a service

Authorizations:
adminBearer
Request Body schema: application/json
required
slug
string <= 100 characters ^[a-z0-9]+(?:-[a-z0-9]+)*$

Lowercase words joined by single hyphens. Unique; a taken slug is 409 slug_taken.

nameEn
string [ 1 .. 200 ] characters
nameFr
string or null <= 200 characters
descriptionEn
string or null <= 5000 characters
descriptionFr
string or null <= 5000 characters
coverImageUrl
string or null <uri>

A pasted link to an image hosted elsewhere. Must be https:; http: is a 422.

bookingFeeRateOverride
number or null [ 0 .. 1 ]

At most three decimal places. Null uses the global rate.

isActive
boolean
sortOrder
integer >= 0

Responses

Response Schema: application/json
required
object (AdminService)

Request samples

Content type
application/json
{
  • "slug": "string",
  • "nameEn": "string",
  • "nameFr": "string",
  • "descriptionEn": "string",
  • "descriptionFr": "string",
  • "coverImageUrl": "http://example.com",
  • "bookingFeeRateOverride": 0,
  • "isActive": true,
  • "sortOrder": 0
}

Response samples

Content type
application/json
{
  • "service": {
    }
}

Update a service

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
slug
string <= 100 characters ^[a-z0-9]+(?:-[a-z0-9]+)*$

Lowercase words joined by single hyphens. Unique; a taken slug is 409 slug_taken.

nameEn
string [ 1 .. 200 ] characters
nameFr
string or null <= 200 characters
descriptionEn
string or null <= 5000 characters
descriptionFr
string or null <= 5000 characters
coverImageUrl
string or null <uri>

A pasted link to an image hosted elsewhere. Must be https:; http: is a 422.

bookingFeeRateOverride
number or null [ 0 .. 1 ]

At most three decimal places. Null uses the global rate.

isActive
boolean
sortOrder
integer >= 0

Responses

Response Schema: application/json
required
object (AdminService)

Request samples

Content type
application/json
{
  • "slug": "string",
  • "nameEn": "string",
  • "nameFr": "string",
  • "descriptionEn": "string",
  • "descriptionFr": "string",
  • "coverImageUrl": "http://example.com",
  • "bookingFeeRateOverride": 0,
  • "isActive": true,
  • "sortOrder": 0
}

Response samples

Content type
application/json
{
  • "service": {
    }
}

Delete a service

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)

Responses

Response samples

Content type
application/json
{
  • "error": "unauthenticated"
}

Create a package

Authorizations:
adminBearer
Request Body schema: application/json
required
serviceId
string <uuid> (Uuid)

Create only. A service that does not exist is a 422 naming serviceId.

nameEn
string [ 1 .. 200 ] characters
nameFr
string or null <= 200 characters
descriptionEn
string or null <= 5000 characters
descriptionFr
string or null <= 5000 characters
priceRwf
integer (Rwf) >= 0

Whole Rwandan francs. Money is never a float.

photoCount
integer >= 0
durationMinutes
integer >= 1
isActive
boolean
sortOrder
integer >= 0

Responses

Response Schema: application/json
required
object (AdminPackage)
required
null or object

Request samples

Content type
application/json
{
  • "serviceId": "8f8bb40f-b96b-40fe-9064-5031fbe483f9",
  • "nameEn": "string",
  • "nameFr": "string",
  • "descriptionEn": "string",
  • "descriptionFr": "string",
  • "priceRwf": 0,
  • "photoCount": 0,
  • "durationMinutes": 1,
  • "isActive": true,
  • "sortOrder": 0
}

Response samples

Content type
application/json
{
  • "package": {
    },
  • "warning": { }
}

Update a package

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
serviceId
string <uuid> (Uuid)

Create only. A service that does not exist is a 422 naming serviceId.

nameEn
string [ 1 .. 200 ] characters
nameFr
string or null <= 200 characters
descriptionEn
string or null <= 5000 characters
descriptionFr
string or null <= 5000 characters
priceRwf
integer (Rwf) >= 0

Whole Rwandan francs. Money is never a float.

photoCount
integer >= 0
durationMinutes
integer >= 1
isActive
boolean
sortOrder
integer >= 0

Responses

Response Schema: application/json
required
object (AdminPackage)
required
null or object

Request samples

Content type
application/json
{
  • "serviceId": "8f8bb40f-b96b-40fe-9064-5031fbe483f9",
  • "nameEn": "string",
  • "nameFr": "string",
  • "descriptionEn": "string",
  • "descriptionFr": "string",
  • "priceRwf": 0,
  • "photoCount": 0,
  • "durationMinutes": 1,
  • "isActive": true,
  • "sortOrder": 0
}

Response samples

Content type
application/json
{
  • "package": {
    },
  • "warning": { }
}

Delete a package

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)

Responses

Response samples

Content type
application/json
{
  • "error": "unauthenticated"
}

Create an add-on

Authorizations:
adminBearer
Request Body schema: application/json
required
serviceId
string or null <uuid>

Create only.

nameEn
string [ 1 .. 200 ] characters
nameFr
string or null <= 200 characters
priceRwf
integer (Rwf) >= 0

Whole Rwandan francs. Money is never a float.

isActive
boolean
sortOrder
integer >= 0

Responses

Response Schema: application/json
required
object (AdminAddon)

Request samples

Content type
application/json
{
  • "serviceId": "8f8bb40f-b96b-40fe-9064-5031fbe483f9",
  • "nameEn": "string",
  • "nameFr": "string",
  • "priceRwf": 0,
  • "isActive": true,
  • "sortOrder": 0
}

Response samples

Content type
application/json
{
  • "addon": {
    }
}

Update an add-on

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)
Request Body schema: application/json
required
serviceId
string or null <uuid>

Create only.

nameEn
string [ 1 .. 200 ] characters
nameFr
string or null <= 200 characters
priceRwf
integer (Rwf) >= 0

Whole Rwandan francs. Money is never a float.

isActive
boolean
sortOrder
integer >= 0

Responses

Response Schema: application/json
required
object (AdminAddon)

Request samples

Content type
application/json
{
  • "serviceId": "8f8bb40f-b96b-40fe-9064-5031fbe483f9",
  • "nameEn": "string",
  • "nameFr": "string",
  • "priceRwf": 0,
  • "isActive": true,
  • "sortOrder": 0
}

Response samples

Content type
application/json
{
  • "addon": {
    }
}

Delete an add-on

Authorizations:
adminBearer
path Parameters
id
required
string <uuid> (Uuid)

Responses

Response samples

Content type
application/json
{
  • "error": "unauthenticated"
}

Webhooks

Inbound payment-provider callbacks. Called by providers, never by browsers.

MTN MoMo callback

MTN signs nothing it sends, so the proof is in the URL: the path carries our reference and HMAC(our_ref) keyed from SESSION_SECRET, and the body's externalId must name the same payment. Only MTN is ever given this URL. A body failing either check is stored and applied to nothing.

path Parameters
ourRef
required
string <uuid> (Uuid)

Our id for one payment attempt, returned when it was started.

signature
required
string

HMAC(our_ref), base64url.

Request Body schema: application/json
required
externalId
string
status
string
amount
string
financialTransactionId
string

Responses

Response Schema: application/json
received
required
any
Value: true

Request samples

Content type
application/json
{
  • "externalId": "string",
  • "status": "SUCCESSFUL",
  • "amount": "string",
  • "financialTransactionId": "string"
}

Response samples

Content type
application/json
{
  • "received": true
}

MTN MoMo callback (PUT)

Same as the POST callback; MTN may deliver either verb.

path Parameters
ourRef
required
string <uuid> (Uuid)

Our id for one payment attempt, returned when it was started.

signature
required
string
Request Body schema: application/json
required
externalId
string
status
string
amount
string
financialTransactionId
string

Responses

Response Schema: application/json
received
required
any
Value: true

Request samples

Content type
application/json
{
  • "externalId": "string",
  • "status": "string",
  • "amount": "string",
  • "financialTransactionId": "string"
}

Response samples

Content type
application/json
{
  • "received": true
}

Flutterwave webhook

Verified by the flutterwave-signature header, an HMAC-SHA256 of the raw body keyed by the dashboard's secret hash. A body whose signature does not match is stored unverified and applied to nothing.

header Parameters
flutterwave-signature
required
string

Base64 HMAC-SHA256 of the raw body.

Request Body schema: application/json
required
type
string
data
object

Responses

Response Schema: application/json
received
required
any
Value: true

Request samples

Content type
application/json
{
  • "type": "charge.completed",
  • "data": { }
}

Response samples

Content type
application/json
{
  • "received": true
}