Developer API
Integrate CombatScore with your tools using our REST API and webhook system.
Base URL: https://combatscore.app · API Version: 2026-04-16
Getting Started
Three steps to your first API call:
- Create an API key — go to your gym dashboard at
Dashboard → Settings → API Keysand generate a key. Copy it immediately — it's shown only once. - Make your first request:curl
curl -H "Authorization: Bearer cs_your_key_here" \ https://combatscore.app/api/zapier/triggers?type=new_member
- Set up a webhook (optional) — subscribe to real-time events instead of polling:curl
curl -X POST -H "Authorization: Bearer cs_your_key_here" \ -H "Content-Type: application/json" \ -d '{"url":"https://your-app.com/webhooks/combatscore","events":["member.joined","payment.received"]}' \ https://combatscore.app/api/gym/webhooks
Authentication
All API requests require an API key passed via the Authorization header:
Authorization: Bearer cs_your_api_key_here
Keys are scoped: read for polling triggers and GET endpoints, write for actions that create or modify data. A key can have both scopes.
Keys are prefixed with cs_ followed by 64 hex characters. They are hashed (SHA-256) on our side — if you lose the key, generate a new one.
Zapier Triggers (Polling)
Poll these endpoints to detect new events. Returns the latest 20 items, sorted newest first.
/api/zapier/triggers?type=new_memberscope: readReturns the 20 most recent gym members.
| Field | Type | Description |
|---|---|---|
id | uuid | Member record ID |
user_id | string | Clerk user ID |
role | string | member or coach |
joined_at | datetime | When they joined |
/api/zapier/triggers?type=new_paymentscope: readReturns the 20 most recent payments (invoices).
| Field | Type | Description |
|---|---|---|
id | uuid | Invoice ID |
user_id | string | Clerk user ID |
amount_cents | integer | Amount in cents |
status | string | Payment status |
created_at | datetime | Payment date |
/api/zapier/triggers?type=new_sessionscope: readReturns the 20 most recent training sessions logged by gym members.
| Field | Type | Description |
|---|---|---|
id | uuid | Session ID |
user_id | string | Clerk user ID |
session_type | string | gi, no-gi, open-mat, etc. |
date | date | Session date |
duration_mins | integer | Duration in minutes |
/api/zapier/triggers?type=new_checkinscope: readReturns the 20 most recent kiosk check-ins.
| Field | Type | Description |
|---|---|---|
id | uuid | Attendance record ID |
user_id | string | Clerk user ID |
class_schedule_id | uuid | null | Linked class (if any) |
status | string | approved or pending |
Zapier Actions
Create resources in your gym. Requires a write scope API key.
/api/zapier/actions?type=create_leadscope: writeAdd a new lead to your gym's CRM.
| Field | Type | Description |
|---|---|---|
first_name * | string | Lead's first name (required) |
last_name | string | Lead's last name |
email | string | Email address |
phone | string | Phone number |
source | string | Lead source (default: zapier) |
curl -X POST -H "Authorization: Bearer cs_..." \
-H "Content-Type: application/json" \
-d '{"first_name":"John","last_name":"Doe","email":"john@example.com","source":"website"}' \
https://combatscore.app/api/zapier/actions?type=create_lead/api/zapier/actions?type=send_smsscope: writeQueue an outbound SMS message.
| Field | Type | Description |
|---|---|---|
phone * | string | E.164 format (e.g. +15551234567) |
message * | string | Message body (max 1600 chars) |
curl -X POST -H "Authorization: Bearer cs_..." \
-H "Content-Type: application/json" \
-d '{"phone":"+15551234567","message":"Welcome to our gym!"}' \
https://combatscore.app/api/zapier/actions?type=send_sms/api/zapier/actions?type=log_attendancescope: writeLog a manual attendance check-in for a member.
| Field | Type | Description |
|---|---|---|
user_id * | string | Clerk user ID of the member |
curl -X POST -H "Authorization: Bearer cs_..." \
-H "Content-Type: application/json" \
-d '{"user_id":"user_abc123"}' \
https://combatscore.app/api/zapier/actions?type=log_attendanceWebhooks (Push)
Subscribe to real-time events instead of polling. Each delivery is signed with HMAC-SHA256 using your webhook secret. Verify the signature via the X-CombatScore-Signature header.
Delivery Headers
| Field | Type | Description |
|---|---|---|
X-CombatScore-Signature | string | HMAC-SHA256 hex digest of the JSON body |
X-CombatScore-Event | string | Event type (e.g. member.joined) |
Content-Type | string | application/json |
Payload Shape
{
"event": "member.joined",
"gym_id": "abc-123",
"data": { ... },
"timestamp": "2026-04-16T12:00:00.000Z"
}Signature Verification
const crypto = require('crypto');
function verifyWebhook(body, signature, secret) {
const expected = crypto
.createHmac('sha256', secret)
.update(body) // raw JSON string, not parsed
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}Deliveries time out after 10 seconds. Failed deliveries are logged but not retried automatically. Check your delivery history via the webhook management API.
Webhook Event Catalog
Eight event types with sample payloads:
member.joined
Fired when a new member joins your gym.
{
"event": "member.joined",
"gym_id": "gym_abc123",
"data": {
"user_id": "user_xyz789"
},
"timestamp": "2026-04-16T14:30:00.000Z"
}member.left
Fired when a member leaves or is removed from your gym.
{
"event": "member.left",
"gym_id": "gym_abc123",
"data": {
"user_id": "user_xyz789"
},
"timestamp": "2026-04-16T14:30:00.000Z"
}payment.received
Fired when a membership payment completes via Stripe.
{
"event": "payment.received",
"gym_id": "gym_abc123",
"data": {
"user_id": "user_xyz789",
"plan_id": "plan_456",
"amount_cents": 9900
},
"timestamp": "2026-04-16T14:30:00.000Z"
}session.logged
Fired when a member logs a training session.
{
"event": "session.logged",
"gym_id": "gym_abc123",
"data": {
"user_id": "user_xyz789",
"session_id": "sess_001",
"session_type": "no-gi",
"date": "2026-04-16",
"duration_mins": 90
},
"timestamp": "2026-04-16T14:30:00.000Z"
}attendance.checked_in
Fired when a member checks in at the kiosk.
{
"event": "attendance.checked_in",
"gym_id": "gym_abc123",
"data": {
"user_id": "user_xyz789",
"attendance_id": "att_001",
"status": "approved"
},
"timestamp": "2026-04-16T14:30:00.000Z"
}class.scheduled
Fired when a new class is added to the schedule.
{
"event": "class.scheduled",
"gym_id": "gym_abc123",
"data": {
"schedule_id": "sched_001",
"title": "Advanced No-Gi",
"discipline": "no-gi",
"day_of_week": 2,
"start_time": "18:00"
},
"timestamp": "2026-04-16T14:30:00.000Z"
}lead.created
Fired when a new lead is added (via API, Zapier, or manually).
{
"event": "lead.created",
"gym_id": "gym_abc123",
"data": {
"lead_id": "lead_001",
"first_name": "John",
"last_name": "Doe",
"source": "zapier"
},
"timestamp": "2026-04-16T14:30:00.000Z"
}contract.signed
Fired when a member signs a membership contract.
{
"event": "contract.signed",
"gym_id": "gym_abc123",
"data": {
"contract_id": "contract_001",
"user_id": "user_xyz789"
},
"timestamp": "2026-04-16T14:30:00.000Z"
}Webhook Management API
CRUD endpoints for managing webhook subscriptions programmatically. Requires Clerk session auth (gym owner).
/api/gym/webhooksscope: ownerList all webhook subscriptions for your gym.
{
"webhooks": [
{
"id": "wh_001",
"url": "https://your-app.com/webhooks",
"events": ["member.joined", "payment.received"],
"is_active": true,
"created_at": "2026-04-16T12:00:00.000Z"
}
]
}/api/gym/webhooksscope: ownerCreate a new webhook subscription. The signing secret is returned once.
| Field | Type | Description |
|---|---|---|
url * | string | HTTPS endpoint to receive events |
events | string[] | Event types to subscribe to (empty = all) |
/api/gym/webhooks/{id}scope: ownerUpdate a webhook's URL, events, or active status.
| Field | Type | Description |
|---|---|---|
url | string | New HTTPS endpoint |
events | string[] | Updated event filter |
is_active | boolean | Enable or disable |
/api/gym/webhooks/{id}scope: ownerDeactivate a webhook subscription.
API Key Management
Manage API keys programmatically. Requires Clerk session auth (gym owner).
/api/gym/api-keysscope: ownerList all API keys. Shows prefixes only — full keys cannot be retrieved.
{
"keys": [
{
"id": "key_001",
"name": "Zapier Production",
"key_prefix": "cs_a1b2c3d4",
"scopes": ["read", "write"],
"last_used": "2026-04-15T10:00:00.000Z",
"is_active": true,
"created_at": "2026-03-01T12:00:00.000Z"
}
]
}/api/gym/api-keysscope: ownerGenerate a new API key. The full key is returned once — store it securely.
| Field | Type | Description |
|---|---|---|
name | string | Display name (default: Default) |
scopes | string[] | Permissions: read, write (default: [read]) |
Rate Limits
| Endpoint | Limit |
|---|---|
| Zapier triggers | 60 requests/minute per gym |
| Zapier actions | 30 requests/minute per gym |
| Standard mutations | 30 requests/minute per user |
| Destructive operations | 5 per hour per user |
Rate-limited responses return 429 Too Many Requests with the standard error format.
Error Handling
All errors follow the same shape:
{ "error": "Human-readable error message" }| Status | Meaning |
|---|---|
| 400 | Bad request — missing or invalid parameters |
| 401 | Unauthorized — invalid or missing API key |
| 403 | Forbidden — insufficient scope or not a gym owner |
| 404 | Not found |
| 409 | Conflict — resource already exists |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
SDK Examples
JavaScript (Node.js / fetch)
const API_KEY = "cs_your_key_here";
const BASE = "https://combatscore.app";
// Poll for new members
const members = await fetch(`${BASE}/api/zapier/triggers?type=new_member`, {
headers: { Authorization: `Bearer ${API_KEY}` },
}).then(r => r.json());
// Create a lead
const lead = await fetch(`${BASE}/api/zapier/actions?type=create_lead`, {
method: "POST",
headers: {
Authorization: `Bearer ${API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
first_name: "Jane",
last_name: "Doe",
email: "jane@example.com",
}),
}).then(r => r.json());Python (requests)
import requests
API_KEY = "cs_your_key_here"
BASE = "https://combatscore.app"
HEADERS = {"Authorization": f"Bearer {API_KEY}"}
# Poll for new members
members = requests.get(
f"{BASE}/api/zapier/triggers",
params={"type": "new_member"},
headers=HEADERS,
).json()
# Create a lead
lead = requests.post(
f"{BASE}/api/zapier/actions",
params={"type": "create_lead"},
headers={**HEADERS, "Content-Type": "application/json"},
json={"first_name": "Jane", "last_name": "Doe", "email": "jane@example.com"},
).json()Ruby
require "net/http"
require "json"
require "uri"
API_KEY = "cs_your_key_here"
BASE = "https://combatscore.app"
# Poll for new members
uri = URI("#{BASE}/api/zapier/triggers?type=new_member")
req = Net::HTTP::Get.new(uri)
req["Authorization"] = "Bearer #{API_KEY}"
res = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(req) }
members = JSON.parse(res.body)Webhook Receiver (Express.js)
const express = require("express");
const crypto = require("crypto");
const app = express();
app.use(express.json({ verify: (req, _res, buf) => { req.rawBody = buf; } }));
const WEBHOOK_SECRET = "your_webhook_secret";
app.post("/webhooks/combatscore", (req, res) => {
const signature = req.headers["x-combatscore-signature"];
const expected = crypto.createHmac("sha256", WEBHOOK_SECRET)
.update(req.rawBody)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).send("Bad signature");
}
const { event, data } = req.body;
console.log("Received event:", event, data);
res.sendStatus(200);
});
app.listen(3000);OpenAPI Specification
A machine-readable OpenAPI 3.0 spec is available for generating client SDKs or importing into Postman, Insomnia, or Swagger UI:
https://combatscore.app/openapi.json
Download or import the URL directly into your tool of choice. The spec covers all Zapier, webhook, and API key management endpoints with full request/response schemas.