Mojo logo Developers
Back to Mojo ↗

Mojo API

Connect your platform to a studio’s schedule and bookings.

In development. Endpoints and access credentials are not available yet.

Overview

Read a studio’s group classes, find or create clients, and manage bookings created by your integration.

Requests and responses use JSON.

Clients
Find by email or phone, view one client, or create a client.
Events
Read group classes, instructors, dates, status, and remaining spots.
Attendances
Create, view, cancel, or reactivate your integration’s bookings.
Webhooks
Subscribe to attendance.created and attendance.updated for your own bookings.
Base URL
https://www.usemojo.app/api

Authentication

A token is bound to one business and one integration source. Keep it on your server, out of browser code, query strings, and logs.

The studio owner can generate, rotate, or revoke the token. Rotation invalidates the previous token. API tokens and webhook signing secrets are separate credentials.

  • Missing, invalid, or revoked tokens return 401. Resources outside your business or booking source return 404.
  • Every resource uses a public string ID: cli_ for clients, evt_ for events, att_ for attendances, usr_ for instructors, and whk_ for webhook registrations. Each prefix is followed by eight lowercase hexadecimal characters. Numeric IDs are never accepted.
Authorization
Required on every request. Use Bearer followed by your studio API token.
Content-Type
Required for JSON writes: application/json.
Idempotency-Key
Optional on POST; recommended for safe retries.
Set up your environment
export MOJO_API_URL="https://www.usemojo.app/api"
export MOJO_API_TOKEN="YOUR_STUDIO_API_TOKEN"

Book your first class

Use curl and jq in the same shell, with MOJO_API_URL and MOJO_API_TOKEN set as shown above. Stop if a request fails.

1. Find a class

Set the date range and choose a future class with available places from data. Copy its id into EVENT_ID.

Read the schedule
curl --fail-with-body --get "$MOJO_API_URL/events" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  --data-urlencode 'status=active' \
  --data-urlencode 'starts_at_from=2026-10-01T00:00:00Z' \
  --data-urlencode 'starts_at_before=2026-10-08T00:00:00Z'
Save the selected class ID
EVENT_ID="evt_5f2d810a"

2. Find the client

Search using the client’s email. If a matching client is returned, copy its id into CLIENT_ID and skip to step 4. If data is empty, create the client in step 3. Resolve multiple matches before continuing.

Look up the client
CLIENT_EMAIL="alex@example.com"

curl --fail-with-body --get "$MOJO_API_URL/clients" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  --data-urlencode "email=$CLIENT_EMAIL"
Save the matching client ID
CLIENT_ID="cli_99aa630c"

3. Create the client if needed

Run this only when lookup returned no matches. The response’s data.id is saved in CLIENT_ID. Use a new Idempotency-Key for each intended create; keep it when retrying.

Create and save the client ID
CLIENT_BODY=$(jq -n --arg email "$CLIENT_EMAIL" \
  '{client: {name: "Alex Example", email: $email}}')

CLIENT_RESPONSE=$(curl --fail-with-body -X POST "$MOJO_API_URL/clients" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: client-example-001" \
  --data "$CLIENT_BODY") &&
  CLIENT_ID=$(printf '%s' "$CLIENT_RESPONSE" | jq -er '.data.id')

4. Book the class

Use the event and client IDs from the previous steps. A successful response is 201 with data.status set to confirmed. Mojo rechecks availability when booking.

Create the booking
BOOKING_BODY=$(jq -n --arg client_id "$CLIENT_ID" \
  '{attendance: {client_id: $client_id}}')

curl --fail-with-body -X POST \
  "$MOJO_API_URL/events/$EVENT_ID/attendances" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: booking-example-001" \
  --data "$BOOKING_BODY"
201 Created
{
  "data": {
    "id": "att_123abc0c",
    "event_id": "evt_5f2d810a",
    "client_id": "cli_99aa630c",
    "source": "partner_app",
    "status": "confirmed",
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T10:00:00.000000Z"
  }
}

5. Cancel the booking

When you need to cancel, PATCH using the same event and client IDs. A successful response is 200 with data.status set to cancelled. Cancel before the class’s calendar day in the studio’s timezone.

Cancel using the client ID
curl --fail-with-body -X PATCH \
  "$MOJO_API_URL/events/$EVENT_ID/attendances/$CLIENT_ID" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{"attendance":{"status":"cancelled"}}'

Clients

Client discovery is an exact contact lookup. An email or phone number is required; there is no full client directory or pagination.

Emails are trimmed and lowercased before searching. Dots and +tags are preserved. Send phones in E.164 format, including + and country code. Mojo also removes harmless spaces, parentheses, and hyphens before validation and lookup: “ +40 (722) 123-456 ” becomes “+40722123456”. Local numbers, letters, and extensions are rejected.

Look up a client

GET /api/clients

If both email and phone_number are supplied, both must match the same client. Multiple clients may share a phone. URL-encode + as %2B; curl --data-urlencode does this for you.

Query parameters

email string One of these required
Email address. Trimmed and lowercased before matching.
phone_number string One of these required
International E.164 phone number with + and country code.
archived boolean Optional
Default: false. Selects archived clients when true.

Returns

200 · data is an array of all exact matches, or []. More than 100 matches returns 400 lookup_too_broad; add email to narrow the search. Missing contacts return 400 lookup_required; malformed contacts or unsupported filters return 400 invalid_filter.

cURL · request
curl --get \
  "https://www.usemojo.app/api/clients" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  --data-urlencode 'phone_number=+40722123456'
200 OK
{
  "data": [
    {
      "id": "cli_99aa630c",
      "name": "Alex Example",
      "email": "alex@example.com",
      "phone_number": "+40722123456",
      "phone_country_code": "RO",
      "archived_at": null,
      "created_at": "2026-09-16T10:00:00.000000Z",
      "updated_at": "2026-09-16T10:00:00.000000Z"
    }
  ]
}

Retrieve a client

GET /api/clients/:id

Archived clients remain readable.

Path parameters

id string Required
A cli_ client ID.

Returns

200 · client object. Missing or inaccessible client: 404.

cURL · request
curl \
  "https://www.usemojo.app/api/clients/cli_99aa630c" \
  -H "Authorization: Bearer $MOJO_API_TOKEN"
200 OK
{
  "data": {
    "id": "cli_99aa630c",
    "name": "Alex Example",
    "email": "alex@example.com",
    "phone_number": "+40722123456",
    "phone_country_code": "RO",
    "archived_at": null,
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T10:00:00.000000Z"
  }
}

Create a client

POST /api/clients

Existing clients are not overwritten or restored.

Request body

Send these fields inside the required client JSON object.

name string Required
Nonblank client name, up to 200 characters.
email string One of these required
Valid email address, up to 254 characters. Trimmed and lowercased.
phone_number string One of these required
Valid E.164 phone, such as +40722123456.

Returns

201 · client object. Duplicate email: 409. Invalid fields or contacts: 422. Location: /api/clients/cli_99aa630c.

cURL · request
curl -X POST \
  "https://www.usemojo.app/api/clients" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: client-example-001" \
  --data '{
  "client": {
    "name": "Alex Example",
    "email": "alex@example.com",
    "phone_number": "+40722123456"
  }
}'
201 Created
{
  "data": {
    "id": "cli_99aa630c",
    "name": "Alex Example",
    "email": "alex@example.com",
    "phone_number": "+40722123456",
    "phone_country_code": "RO",
    "archived_at": null,
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T10:00:00.000000Z"
  }
}

Client response fields

id
String · cli_ public ID.
name
String · client name.
email
String or null · normalized email address.
phone_number
String or null · international E.164 number.
phone_country_code
String or null · derived country code, such as RO.
archived_at
String or null · archive time in ISO 8601 UTC, or null for an active client. Archived clients cannot make new bookings.
created_at
String · creation time in ISO 8601 UTC with fractional seconds.
updated_at
String · last update time in ISO 8601 UTC with fractional seconds.

Events

Events are read-only group classes. Class creation, updates, and cancellations are managed in Mojo.

Remaining spots includes confirmed and in-review reservations from every source. It never goes below zero and is zero for cancelled, completed, or ended classes. Reading availability does not reserve a place. Refresh events to pick up schedule changes; schedule and availability webhooks are not part of this API.

Read the schedule

GET /api/events

Returns individual group-class occurrences, ordered by start time. With no date filters, all dates are eligible.

Query parameters

starts_at_from timestamp Optional
Inclusive start bound. ISO 8601 with Z or a timezone offset.
starts_at_before timestamp Optional
Exclusive start bound. ISO 8601 with Z or a timezone offset; must be later than starts_at_from.
status string Optional
Filter by active, cancelled, or completed.
type string Optional
Only group_session is supported.
page integer Optional
Page number, starting at 1. Default: 1. Must be positive.
per_page integer Optional
Number of records per page. Default: 50. Allowed range: 1–100.

Returns

200 · array of event objects and pagination meta. Unsupported type or malformed/inverted date range: 400 invalid_filter.

cURL · request
curl --get \
  "https://www.usemojo.app/api/events" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  --data-urlencode 'starts_at_from=2026-10-01T00:00:00Z' \
  --data-urlencode 'starts_at_before=2026-10-08T00:00:00Z' \
  --data-urlencode 'status=active'
200 OK
{
  "data": [
    {
      "id": "evt_5f2d810a",
      "type": "group_session",
      "title": "Reformer Pilates",
      "instructor": {
        "id": "usr_07bc9e12",
        "name": "Ana Example"
      },
      "start_at": "2026-10-01T15:00:00.000000Z",
      "end_at": "2026-10-01T16:00:00.000000Z",
      "duration_minutes": 60,
      "status": "active",
      "max_attendees": 8,
      "remaining_spots": 6,
      "publicly_bookable": true,
      "uses_waitlists": false,
      "max_waitlist": 0,
      "created_at": "2026-09-16T10:00:00.000000Z",
      "updated_at": "2026-09-16T10:00:00.000000Z"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 50,
    "next_page": null
  }
}

Retrieve a class

GET /api/events/:id

Returns current class details and availability.

Path parameters

id string Required
An evt_ group-class ID.

Returns

200 · event object. Missing, inaccessible, or unsupported event type: 404.

cURL · request
curl \
  "https://www.usemojo.app/api/events/evt_5f2d810a" \
  -H "Authorization: Bearer $MOJO_API_TOKEN"
200 OK
{
  "data": {
    "id": "evt_5f2d810a",
    "type": "group_session",
    "title": "Reformer Pilates",
    "instructor": {
      "id": "usr_07bc9e12",
      "name": "Ana Example"
    },
    "start_at": "2026-10-01T15:00:00.000000Z",
    "end_at": "2026-10-01T16:00:00.000000Z",
    "duration_minutes": 60,
    "status": "active",
    "max_attendees": 8,
    "remaining_spots": 6,
    "publicly_bookable": true,
    "uses_waitlists": false,
    "max_waitlist": 0,
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T10:00:00.000000Z"
  }
}

Event response fields

id
String · evt_ public ID.
type
String · always group_session.
title
String · class name.
instructor
Object · the class instructor, with id and name fields.
instructor.id
String · usr_ public ID.
instructor.name
String · instructor’s display name.
start_at
String · class start time in ISO 8601 UTC with fractional seconds.
end_at
String · class end time in ISO 8601 UTC with fractional seconds.
duration_minutes
Integer · class duration in minutes.
status
String · active, cancelled, or completed.
max_attendees
Integer · class capacity.
remaining_spots
Integer · unreserved places, counting confirmed and in_review bookings from all sources. Zero for cancelled, completed, or ended classes.
publicly_bookable
Boolean · whether public booking is enabled.
uses_waitlists
Boolean · whether the studio enables a waitlist for direct bookings. External API bookings never enter a waitlist.
max_waitlist
Integer · configured waitlist capacity for direct bookings.
created_at
String · creation time in ISO 8601 UTC with fractional seconds.
updated_at
String · last update time in ISO 8601 UTC with fractional seconds.

Attendances

An attendance is a booking for one client at one event. Your integration can only read or change its own bookings, including when you reference them by client ID. The source is assigned by Mojo and cannot be changed through the API.

External bookings are confirmed immediately when eligible. They never attach, reserve, or consume a Mojo membership. Full classes return 409 event_full; there is no external waitlisting.

  • Booking and reactivation honor the studio’s configured booking lead time. The class must be eligible and the client active; a closed booking window returns 422 booking_window_closed.
  • Cancel before the class’s calendar day in the studio’s timezone. Changing a booking to cancelled on or after that local date returns 422 cancellation_window_closed.
  • Cancel from confirmed, in_review, or waitlisted. Reactivate only from cancelled to confirmed; capacity and booking rules are checked again. The same attendance ID is retained.
  • Repeating confirmed → confirmed or cancelled → cancelled returns 200 without another change or notification, including a repeated cancellation after its cutoff. Other transitions, attendance marking, and no-show marking are not writable.
  • If a studio rebooks a cancelled partner booking directly in Mojo, it can become a Mojo booking. It is then outside your integration’s access, even with a previously known attendance ID.

List your bookings for a class

GET /api/events/:event_id/attendances

Returns your integration’s attendances for this group class.

Path parameters

event_id string Required
An evt_ group-class ID.

Query parameters

client_id string Optional
Filter by a cli_ client ID.
status string Optional
Filter by in_review, confirmed, waitlisted, cancelled, attended, no_show, or expired.
page integer Optional
Page number, starting at 1. Default: 1. Must be positive.
per_page integer Optional
Number of records per page. Default: 50. Allowed range: 1–100.

Returns

200 · array of attendance objects and pagination meta.

cURL · request
curl \
  "https://www.usemojo.app/api/events/evt_5f2d810a/attendances" \
  -H "Authorization: Bearer $MOJO_API_TOKEN"
200 OK
{
  "data": [
    {
      "id": "att_123abc0c",
      "event_id": "evt_5f2d810a",
      "client_id": "cli_99aa630c",
      "source": "partner_app",
      "status": "confirmed",
      "created_at": "2026-09-16T10:00:00.000000Z",
      "updated_at": "2026-09-16T10:00:00.000000Z"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 50,
    "next_page": null
  }
}

Retrieve by attendance ID

GET /api/events/:event_id/attendances/:id

Retrieves your integration’s booking using its attendance ID.

Path parameters

event_id string Required
An evt_ group-class ID.
id string Required
An att_ attendance ID.

Returns

200 · attendance object. Missing or inaccessible booking: 404.

cURL · request
curl \
  "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c" \
  -H "Authorization: Bearer $MOJO_API_TOKEN"
200 OK
{
  "data": {
    "id": "att_123abc0c",
    "event_id": "evt_5f2d810a",
    "client_id": "cli_99aa630c",
    "source": "partner_app",
    "status": "confirmed",
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T10:00:00.000000Z"
  }
}

Retrieve by client ID

GET /api/events/:event_id/attendances/:client_id

Retrieves your integration’s booking using the client ID. There is at most one attendance per event and client.

Path parameters

event_id string Required
An evt_ group-class ID.
client_id string Required
A cli_ client ID.

Returns

200 · attendance object. Missing or inaccessible booking: 404.

cURL · request
curl \
  "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/cli_99aa630c" \
  -H "Authorization: Bearer $MOJO_API_TOKEN"
200 OK
{
  "data": {
    "id": "att_123abc0c",
    "event_id": "evt_5f2d810a",
    "client_id": "cli_99aa630c",
    "source": "partner_app",
    "status": "confirmed",
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T10:00:00.000000Z"
  }
}

Create a booking

POST /api/events/:event_id/attendances

Creates a confirmed booking. Mojo assigns source and status; do not send them in the request.

Path parameters

event_id string Required
An evt_ group-class ID.

Request body

Send these fields inside the required attendance JSON object.

client_id string Required
The cli_ ID of an active client in the same business.

Returns

201 · attendance object. Full class: 409 event_full and no attendance is created. An existing event/client attendance, including a cancelled one: 409; use PATCH to reactivate. Location: /api/events/evt_5f2d810a/attendances/att_123abc0c.

cURL · request
curl -X POST \
  "https://www.usemojo.app/api/events/evt_5f2d810a/attendances" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: booking-example-001" \
  --data '{
  "attendance": {
    "client_id": "cli_99aa630c"
  }
}'
201 Created
{
  "data": {
    "id": "att_123abc0c",
    "event_id": "evt_5f2d810a",
    "client_id": "cli_99aa630c",
    "source": "partner_app",
    "status": "confirmed",
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T10:00:00.000000Z"
  }
}

Update by attendance ID

PATCH /api/events/:event_id/attendances/:id

Cancel or reactivate a booking created by your integration.

Path parameters

event_id string Required
An evt_ group-class ID.
id string Required
An att_ attendance ID.

Request body

Send these fields inside the required attendance JSON object.

status string Required
Use cancelled to cancel; use confirmed to reactivate a cancelled booking. No other values can be written.

Returns

200 · updated attendance object. Sending its current status is a no-op. Missing or foreign-source booking: 404. Disallowed transition or cutoff: 422. A full class prevents reactivation with 409 event_full.

cURL · request
curl -X PATCH \
  "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "attendance": {
    "status": "cancelled"
  }
}'
200 OK
{
  "data": {
    "id": "att_123abc0c",
    "event_id": "evt_5f2d810a",
    "client_id": "cli_99aa630c",
    "source": "partner_app",
    "status": "cancelled",
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T11:00:00.000000Z"
  }
}
cURL · reactivate
curl -X PATCH \
  "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "attendance": {
    "status": "confirmed"
  }
}'
200 OK
{
  "data": {
    "id": "att_123abc0c",
    "event_id": "evt_5f2d810a",
    "client_id": "cli_99aa630c",
    "source": "partner_app",
    "status": "confirmed",
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T11:05:00.000000Z"
  }
}

Update by client ID

PATCH /api/events/:event_id/attendances/:client_id

Cancel or reactivate a booking created by your integration.

Path parameters

event_id string Required
An evt_ group-class ID.
client_id string Required
A cli_ client ID.

Request body

Send these fields inside the required attendance JSON object.

status string Required
Use cancelled to cancel; use confirmed to reactivate a cancelled booking. No other values can be written.

Returns

200 · updated attendance object. Sending its current status is a no-op. Missing or foreign-source booking: 404. Disallowed transition or cutoff: 422. A full class prevents reactivation with 409 event_full.

cURL · request
curl -X PATCH \
  "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/cli_99aa630c" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "attendance": {
    "status": "cancelled"
  }
}'
200 OK
{
  "data": {
    "id": "att_123abc0c",
    "event_id": "evt_5f2d810a",
    "client_id": "cli_99aa630c",
    "source": "partner_app",
    "status": "cancelled",
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T11:00:00.000000Z"
  }
}
cURL · reactivate
curl -X PATCH \
  "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/cli_99aa630c" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "attendance": {
    "status": "confirmed"
  }
}'
200 OK
{
  "data": {
    "id": "att_123abc0c",
    "event_id": "evt_5f2d810a",
    "client_id": "cli_99aa630c",
    "source": "partner_app",
    "status": "confirmed",
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T11:05:00.000000Z"
  }
}

Attendance response fields

id
String · att_ public ID.
event_id
String · evt_ ID of the group class.
client_id
String · cli_ ID of the booked client.
source
String · read-only integration origin, such as partner_app.
status
String · in_review, confirmed, waitlisted, cancelled, attended, no_show, expired. PATCH accepts only cancelled or confirmed under the rules above.
created_at
String · creation time in ISO 8601 UTC with fractional seconds.
updated_at
String · last update time in ISO 8601 UTC with fractional seconds.

Webhooks

Register a public HTTPS endpoint to receive attendance.created and attendance.updated for your integration’s group-class bookings. Changes made by studio staff or Mojo also generate notifications when they affect an owned booking.

The registration’s whk_ ID identifies the subscription. Each notification has a separate UUID for deduplication. Notifications contain no other source’s roster or schedule-wide updates.

List your registrations

GET /api/webhooks

Lists registrations belonging to the authenticated business and source, including disabled registrations. Signing secrets are omitted.

Query parameters

page integer Optional
Page number, starting at 1. Default: 1. Must be positive.
per_page integer Optional
Number of records per page. Default: 50. Allowed range: 1–100.

Returns

200 · array of webhook objects and pagination meta.

cURL · request
curl \
  "https://www.usemojo.app/api/webhooks" \
  -H "Authorization: Bearer $MOJO_API_TOKEN"
200 OK
{
  "data": [
    {
      "id": "whk_8b7c6d5e",
      "url": "https://partner.example/webhooks/mojo",
      "events": [
        "attendance.created",
        "attendance.updated"
      ],
      "active": true,
      "created_at": "2026-09-16T10:00:00.000000Z",
      "updated_at": "2026-09-16T10:00:00.000000Z"
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 50,
    "next_page": null
  }
}

Register a receiver

POST /api/webhooks

There is one registration per URL and a maximum of five per business, including disabled registrations.

Request body

Send these fields inside the required webhook JSON object.

url string Required
Public HTTPS receiver URL on port 443. Private addresses and redirects are not supported.
events array of strings Required
A nonempty, unique selection of attendance.created and attendance.updated.
active boolean Optional
Default: true. Set false to register without delivering notifications.

Returns

201 · webhook object plus secret. Duplicate URL: 409. Invalid URL, subscriptions, or registration limit: 422.

cURL · request
curl -X POST \
  "https://www.usemojo.app/api/webhooks" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: webhook-example-001" \
  --data '{
  "webhook": {
    "url": "https://partner.example/webhooks/mojo",
    "events": [
      "attendance.created",
      "attendance.updated"
    ]
  }
}'
201 Created
{
  "data": {
    "id": "whk_8b7c6d5e",
    "url": "https://partner.example/webhooks/mojo",
    "events": [
      "attendance.created",
      "attendance.updated"
    ],
    "active": true,
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T10:00:00.000000Z",
    "secret": "YOUR_WEBHOOK_SIGNING_SECRET"
  }
}

Update or disable a receiver

PATCH /api/webhooks/:id

Only supplied fields are changed. Disable registrations with PATCH; there is no DELETE endpoint.

Path parameters

id string Required
A whk_ webhook registration ID.

Request body

Send these fields inside the required webhook JSON object.

url string Optional
Replace the receiver URL. The same public HTTPS validation applies.
events array of strings Optional
Replace the subscriptions with a nonempty, unique selection of supported events.
active boolean Optional
Set false to disable delivery or true to enable it.
rotate_secret boolean Optional
Set true to replace the signing secret independently of the API token. Rotate again if the response is lost.

Returns

200 · webhook object. The new secret is returned only when rotating. Inaccessible registration: 404; duplicate URL: 409; invalid fields: 422.

cURL · request
curl -X PATCH \
  "https://www.usemojo.app/api/webhooks/whk_8b7c6d5e" \
  -H "Authorization: Bearer $MOJO_API_TOKEN" \
  -H "Content-Type: application/json" \
  --data '{
  "webhook": {
    "active": false
  }
}'
200 OK
{
  "data": {
    "id": "whk_8b7c6d5e",
    "url": "https://partner.example/webhooks/mojo",
    "events": [
      "attendance.created",
      "attendance.updated"
    ],
    "active": false,
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T10:00:00.000000Z"
  }
}

Webhook response fields

id
String · whk_ registration ID.
url
String · public HTTPS receiver URL on port 443. Redirects and private network addresses are not supported.
events
Array of strings · nonempty list of attendance.created and/or attendance.updated, without duplicates.
active
Boolean · defaults to true; false disables delivery.
secret
String · signing secret, present only on creation or explicit secret rotation; omitted from other responses. Store securely; it is not the studio’s bearer token.
created_at
String · creation time in ISO 8601 UTC with fractional seconds.
updated_at
String · last update time in ISO 8601 UTC with fractional seconds.

Notification payload

The event UUID and payload remain the same on every retry. The signature timestamp is refreshed for each attempt. The data object is the attendance snapshot at the time of the change.

attendance.updated
{
  "id": "748fc54a-25e1-44e7-8d41-1fb2f0da5c32",
  "type": "attendance.updated",
  "created_at": "2026-09-16T10:00:00.000000Z",
  "data": {
    "id": "att_123abc0c",
    "event_id": "evt_5f2d810a",
    "client_id": "cli_99aa630c",
    "source": "partner_app",
    "status": "cancelled",
    "created_at": "2026-09-16T10:00:00.000000Z",
    "updated_at": "2026-09-16T10:00:00.000000Z"
  }
}

Verify webhook signatures

Read the Mojo-Signature header as t=<unix_seconds>,sha256=<hex_signature>. Compute HMAC-SHA256 with the registration’s signing secret over the timestamp, a period, and the exact raw request body. Compare in constant time and reject timestamps more than five minutes in the past or future.

Verify before parsing or acting on the payload. Then store the notification durably with a unique constraint on its UUID. Acknowledge with 2xx only after the commit, or when that UUID has already been accepted. If durable storage fails, return 5xx so Mojo can retry. Keep business processing asynchronous.

Ruby verification example

Ruby · raw-body verification
require "json"
require "openssl"

def verify_mojo_webhook(raw_body, header, secret, now: Time.now.to_i)
  match = /\At=(\d+),sha256=([0-9a-f]{64})\z/.match(header.to_s)
  raise "Invalid signature" unless match

  timestamp, signature = match.captures
  raise "Expired signature" if (now - timestamp.to_i).abs > 300

  expected = OpenSSL::HMAC.hexdigest(
    "SHA256", secret, "#{timestamp}.#{raw_body}"
  )
  valid = OpenSSL.fixed_length_secure_compare(expected, signature)
  raise "Invalid signature" unless valid

  JSON.parse(raw_body)
end

# In your receiver, pass request.raw_post and the Mojo-Signature header.
# Persist the verified event in a durable inbox with a UNIQUE event UUID.
# Return 2xx only after that commit (or if the UUID is already stored).
# Process the inbox asynchronously and fetch the current booking state.

JavaScript verification example

Node.js · raw-body verification
import { createHmac, timingSafeEqual } from "node:crypto";

export function verifyMojoWebhook(rawBody, header, secret,
  now = Math.floor(Date.now() / 1000)) {
  if (!Buffer.isBuffer(rawBody)) throw new Error("Raw body required");
  const match = /^t=(\d+),sha256=([0-9a-f]{64})$/.exec(header ?? "");
  if (!match) throw new Error("Invalid signature");

  const [, timestamp, signature] = match;
  if (Math.abs(now - Number(timestamp)) > 300) {
    throw new Error("Expired signature");
  }
  const expected = createHmac("sha256", secret)
    .update(timestamp + ".").update(rawBody).digest();
  const received = Buffer.from(signature, "hex");
  if (received.length !== expected.length ||
      !timingSafeEqual(expected, received)) {
    throw new Error("Invalid signature");
  }
  return JSON.parse(rawBody.toString("utf8"));
}

// Capture a raw Buffer before JSON middleware changes the request body.
// Persist the verified event in a durable inbox with a UNIQUE event UUID.
// Return 2xx only after that commit (or if the UUID is already stored).
// Process the inbox asynchronously and fetch the current booking state.

Pagination & safe retries

Event, attendance, and webhook indexes use page (default 1) and per_page (default 50, maximum 100). Follow meta.next_page until it is null. A page beyond the end returns an empty array. Client lookup has no pagination and rejects page and per_page.

Resources can change while paging. Deduplicate by public ID; pages are not a snapshot. Times use ISO 8601 UTC, including fractional seconds.

Idempotency-Key
Use a unique key for each intended POST operation, and reuse it for retries of that same method, path, and payload. Successful responses are retained for 24 hours. Changed input with the same key returns 409. A replay returns the original result without repeating side effects; it does not reopen a later-cancelled booking.
API rate limits
120 requests per business per minute, including up to 30 writes. An additional 300 requests per IP per minute applies before authentication. A 429 response includes Retry-After: 60. Wait before retrying.
Webhook retry schedule
One initial attempt plus up to six retries. Delays are 1, 2, 4, 8, 16, and 32 minutes: the last retry is nominally 63 minutes after the first attempt. Request duration, queue delays, or a longer receiver Retry-After can extend that window.
Delivery responses
Any 2xx accepts a delivery. Network errors, 408, 429, and 5xx are retried. Other HTTP failures are terminal. Keep the receiver fast: delivery has a 10-second overall timeout.
Duplicates & ordering
Notifications can arrive more than once or out of order. Deduplicate by notification UUID. GET the current attendance before overwriting local state with an older snapshot.
Reconciliation
Periodically read known bookings using event/client IDs and refresh the schedule. Exhausted webhook delivery may leave a change undelivered. An authenticated 404 means no accessible owned booking; do not infer the cause or automatically create a replacement. Timeouts, 401, 429, and 5xx are not evidence that a booking disappeared.
Last page of an index
{
  "data": [],
  "meta": {
    "page": 1,
    "per_page": 50,
    "next_page": null
  }
}

Errors

Use the HTTP status and error.code for handling; message is for people. Include request_id when reporting a problem. Unknown write fields return 422; unsupported query filters return 400.

400
Malformed JSON, missing body root, or invalid filters/pagination. Lookup codes include lookup_required, invalid_filter, and lookup_too_broad.
401 / 403
Missing or invalid bearer token / authenticated action prohibited. 401 includes WWW-Authenticate: Bearer.
404
Resource not found, outside your business or source, or an event type outside this API.
409
Duplicate record, idempotency conflict, or event_full. A failed booking creates no attendance.
413 / 415
Request body too large / unsupported content type.
422
Invalid fields or transition, or an unmet booking rule. Cutoff codes: booking_window_closed and cancellation_window_closed.
429
Rate limited. Honor Retry-After before retrying.
500 / 503
Unexpected server error / temporary dependency failure. Retry with backoff; reuse the original Idempotency-Key for POST requests.
409 · full class
{
  "error": {
    "code": "event_full",
    "message": "This class has no remaining spots.",
    "details": []
  },
  "request_id": "example-request-id"
}
422 · validation failed
{
  "error": {
    "code": "validation_failed",
    "message": "The attendance could not be saved.",
    "details": [
      {
        "field": "client",
        "code": "archived"
      }
    ]
  },
  "request_id": "example-request-id"
}