Mojo logo Developers
Back to Mojo ↗

Mojo API

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

Download OpenAPI

Overview

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

Requests and responses use JSON.

Client, event, and attendance objects include links.self, the full URL to retrieve that resource. Send the bearer token when following a link. On creation, the Location header matches links.self.

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.

Each studio receives a token automatically. The studio owner can copy it from Settings → API. Use the same API token to verify webhook signatures.

The owner sets the source in Settings → API to distinguish attendances created through the API. An empty source defaults to external.

  • Missing or invalid tokens and disabled API access 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 for a new booking or 200 for an existing one. New and reactivated bookings have 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",
    "links": {
      "self": "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c"
    }
  }
}

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,
      "links": {
        "self": "https://www.usemojo.app/api/clients/cli_99aa630c"
      }
    }
  ]
}

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,
    "links": {
      "self": "https://www.usemojo.app/api/clients/cli_99aa630c"
    }
  }
}

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: https://www.usemojo.app/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,
    "links": {
      "self": "https://www.usemojo.app/api/clients/cli_99aa630c"
    }
  }
}

Client response fields

id
String · cli_ public ID.
name
String or null · client name. Existing clients may have an incomplete profile.
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.
links.self
String · full URL to retrieve this resource.

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. Without filters, includes all dates and statuses.

Query parameters

starts_at_from timestamp Optional
Inclusive start bound. ISO 8601 with seconds and Z or a timezone offset.
starts_at_before timestamp Optional
Exclusive start bound. ISO 8601 with seconds and 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, pagination links, and 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,
      "links": {
        "self": "https://www.usemojo.app/api/events/evt_5f2d810a"
      }
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 50,
    "next_page": null
  },
  "links": {
    "next": null,
    "prev": 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,
    "links": {
      "self": "https://www.usemojo.app/api/events/evt_5f2d810a"
    }
  }
}

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 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.
links.self
String · full URL to retrieve this resource.

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.

  • POST creates a new booking with 201, reactivates your cancelled booking with 200, or returns your existing non-cancelled booking unchanged with 200. Reactivation keeps the same attendance ID and source. A Mojo-owned booking returns 409 duplicate_attendance; bookings from another API source return 404.
  • 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.
  • The source comes from the owner’s API settings and defaults to external. mojo is reserved for direct bookings. Changing the configured source changes which bookings the integration can access; existing bookings keep their source.
  • 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, pagination links, and 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",
      "links": {
        "self": "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c"
      }
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 50,
    "next_page": null
  },
  "links": {
    "next": null,
    "prev": 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",
    "links": {
      "self": "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c"
    }
  }
}

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",
    "links": {
      "self": "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c"
    }
  }
}

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 · new attendance, or 200 · reactivated/existing attendance. Full class when creating or reactivating: 409 event_full. Mojo-owned booking: 409 duplicate_attendance. Location matches the returned links.self URL.

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 · new booking / 200 OK · reactivated booking
{
  "data": {
    "id": "att_123abc0c",
    "event_id": "evt_5f2d810a",
    "client_id": "cli_99aa630c",
    "source": "partner_app",
    "status": "confirmed",
    "links": {
      "self": "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c"
    }
  }
}

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",
    "links": {
      "self": "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c"
    }
  }
}
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",
    "links": {
      "self": "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c"
    }
  }
}

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",
    "links": {
      "self": "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c"
    }
  }
}
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",
    "links": {
      "self": "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c"
    }
  }
}

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.
links.self
String · full URL using the att_ ID. You can still retrieve or update the booking using only its event and client IDs.

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.

Disabling a registration or changing its URL stops queued notifications. Re-enabling it sends future changes only. A request already in progress may finish.

List your registrations

GET /api/webhooks

Lists registrations belonging to the authenticated business and source, including disabled registrations.

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, pagination links, and 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
    }
  ],
  "meta": {
    "page": 1,
    "per_page": 50,
    "next_page": null
  },
  "links": {
    "next": null,
    "prev": 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. 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
  }
}

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.

Returns

200 · webhook object. 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
  }
}

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.

Notification payload

created_at records when the change happened. 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",
    "links": {
      "self": "https://www.usemojo.app/api/events/evt_5f2d810a/attendances/att_123abc0c"
    }
  }
}

Verify webhook signatures

Read the Mojo-Signature header as t=<unix_seconds>,sha256=<hex_signature>. Compute HMAC-SHA256 with your API token 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. Use the token as its 64-character string; do not hex-decode it.

When your API token is replaced, update it for both API calls and webhook verification. Each delivery attempt, including retries, uses the current token.

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, api_token, 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", api_token, "#{timestamp}.#{raw_body}"
  )
  valid = OpenSSL.fixed_length_secure_compare(expected, signature)
  raise "Invalid signature" unless valid

  JSON.parse(raw_body)
end

# Pass request.raw_post, the Mojo-Signature header, and ENV.fetch("MOJO_API_TOKEN").
# 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, apiToken,
  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", apiToken)
    .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"));
}

// Pass the raw Buffer, Mojo-Signature header, and process.env.MOJO_API_TOKEN.
// Capture the 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 links.next until it is null; links.prev returns the previous page or null on page 1. These full URLs preserve filters and page size. Pagination metadata remains available under meta. 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. Use a new key to intentionally reactivate through POST. Successful response bodies and their 200/201 status are retained for 24 hours. Changed input with the same key returns 409. Keys contain 1–200 printable ASCII characters without spaces. A replay returns the original result without repeating side effects; it does not reopen a later-cancelled booking.
API rate limits
100 requests per business per minute, shared by reads and writes. A 100-request limit per IP per minute also 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": 2,
    "per_page": 50,
    "next_page": null
  },
  "links": {
    "next": null,
    "prev": "https://www.usemojo.app/api/events?page=1&per_page=50"
  }
}

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 client/webhook, Mojo-owned booking, 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"
}