This API lets a martial arts school's own website read the school's public information (studio details, programs, classes, a dated schedule, events, trial offers, instructors) and send visitors in as leads or as full prospects with their students. Everything comes back as JSON.
It is for whoever builds or runs the school's website. The school owner creates the key in Axion Dojo and gives it to you together with the address below.
Want it as plain text for your notes or an AI assistant? Download this page as Markdown.
https://YOUR-DOJO-ADDRESS/pub/dm/v1
Each school has its own address. The owner copies it from Settings → Website API → Address (there is a Copy button beside it).
Send the API key on every request:
Authorization: Bearer YOUR_KEY
If your tool cannot set an Authorization header, send X-API-Key: YOUR_KEY
instead. Both work.
There are two kinds of key:
dm_pk_...): safe to put in a web page. It works only from
the websites the school lists against it, and it can never read student,
attendance or payment records.dm_sk_...): for code running on a server. Never put it in a
web page.A browser request from a website the key does not list is refused. Ask the
school to add your domain, including https:// (and your staging domain, if you
have one). Server-to-server calls send no Origin header, so the key alone
authorizes them.
curl -H "Authorization: Bearer YOUR_KEY" https://YOUR-DOJO-ADDRESS/pub/dm/v1/ping
A working key returns the school's name and the key's permissions. A bad key
returns 401.
The school can rotate a key in Settings. That issues a new key with the same
websites and permissions, and the old key keeps working for a period the school
chooses (from immediately up to seven days) so you can swap it in without any
downtime. After that the old key returns 401 key_expired. Keep the key in one
place in your code so a swap is a one-line change.
Every response is JSON in the same envelope:
{ "ok": true, "data": { }, "meta": { } }
{ "ok": false, "error": { "code": "invalid_key", "message": "..." }, "meta": { } }
meta describes the call itself, on success and on failure:
"meta": {
"request_id": "req_4f9c2a1be07d3a55",
"method": "GET",
"endpoint": "/pub/dm/v1/schedule",
"query": { "start": "2026-10-01", "end": "2026-10-14" },
"received_at": "2026-09-24T18:02:11Z",
"api_version": "v1"
}
request_id is also sent in the X-Request-Id header. Quote it when something
looks wrong, and the school can find that exact call under Recent activity
in their settings.
Read responses are cached for up to five minutes, so a change made in Axion Dojo
shows on the website within a few minutes, not instantly. A cached answer
repeats the request_id of the call that filled the cache.
| Method | Path | What it does |
|---|---|---|
| GET | /ping |
Checks the key; returns the school name and the key's permissions |
| GET | /studio |
Name, phone, email, address, map link, logo, social links, time zone |
| GET | /programs |
The styles taught |
| GET | /classes |
The weekly class timetable |
| GET | /schedule |
Classes and events on real calendar dates |
| GET | /events |
Events the school has chosen to show on its website |
| GET | /trials |
Trial and intro offers |
| GET | /instructors |
Instructor names, titles, ranks, bios and programs |
| POST | /leads |
A contact form: creates a lead |
| POST | /trial-requests |
"Book a free class": a lead that records what they picked |
| POST | /prospects |
A full prospect: the account plus each student, as records |
The read endpoints need the key's read permission. The three POST endpoints need the key to allow submitting leads; the school sets that when it creates the key.
Everything a header, footer or contact page needs: name, phone, email,
timezone, address (line1, line2, city, state, postal_code,
one_line, maps_url), logo_url and socials (platform, url).
programs: each with id, name, short_name, description.
The recurring weekly timetable. classes: each with id, name, program,
days (such as ["MON","WED"]), start_time and end_time ("18:30"),
start_label and end_label ("6:30 PM"), duration_minutes, age_min,
age_max, age_label, starts_on, ends_on. Also returns timezone.
The classes placed on real dates, with events merged in, so you can draw an actual calendar.
| Query | Meaning |
|---|---|
start |
First date, YYYY-MM-DD. Defaults to today. |
end |
Last date, YYYY-MM-DD. Defaults to 30 days after start. At most 180 days after it. |
Returns timezone, start, end and days: each day has date, weekday
and entries, sorted by start time. Each entry has kind ("class" or
"event") and the fields from /classes or /events.
Takes the same start and end as /schedule. events: each with id,
name, description, location, category, date, start, end (in the
school's own time zone), start_time, start_label, cost, capacity.
Only events the school has marked for its website appear.
trials: each with id, name, description, price, duration_value,
duration_type ("classes" or "days"), duration_label.
instructors: each with id, name, title, rank, bio, programs. No
photos or personal contact details.
Class times are plain weekly times such as "18:30", with a ready-made label
such as "6:30 PM". Event times are already converted to the school's own time
zone, which every payload with a time also returns as timezone. You do not
need to convert anything.
All three POST endpoints accept JSON or an ordinary form post. They land on the school's dashboard as a new lead straight away, and the school can choose to get an email or text when one arrives.
The simplest option: a contact form.
{
"first_name": "Sam",
"last_name": "Example",
"email": "[email protected]",
"phone": "903-555-0142",
"message": "Looking for kids classes on weekends.",
"interest": "Kids Karate",
"source": "Contact page"
}
first_name and last_name are required, plus at least one of email or
phone. If the email already belongs to someone the school knows, no duplicate
is made: the enquiry is added to that person's history and the response says
"existing": true.
Returns lead_id, existing and received.
Add ?with_signup_link=1 to also get a signup_url, valid for seven days, that
sends the visitor into the school's own full signup (students, waivers and card
details). Do not collect card details on your own site.
The same fields as /leads, plus any of trial_id, class_id,
preferred_date and student_name. It makes the same kind of lead and records
exactly what the visitor picked, so the school can call back already knowing.
Brings a family in as real records: the account with its address and emergency contact, and each student marked as a prospect. These are the same records the school's own sign-up link creates.
{
"first_name": "Sam",
"last_name": "Example",
"email": "[email protected]",
"phone": "903-555-0142",
"address": "12 Main St",
"city": "Springfield",
"state": "TX",
"zip_code": "75001",
"emergency_name": "Pat Example",
"emergency_phone": "903-555-0199",
"message": "Two kids, evenings work best.",
"interest": "Kids Karate",
"source": "Enroll page",
"students": [
{ "first_name": "Riley", "dob": "2017-05-14", "gender": "F" },
{ "first_name": "Jordan", "last_name": "Example", "dob": "2014-11-02",
"medical_notes": "Mild asthma, has an inhaler" }
]
}
| Field | Required | Notes |
|---|---|---|
first_name, last_name |
Yes | The account holder (usually the parent) |
email |
Yes | Must be a valid address |
phone |
Yes | A 10-digit U.S. number, or an international number starting with + |
address, city, state, zip_code |
No | |
emergency_name, emergency_phone |
No | Saved as the account's emergency contact |
message |
No | Saved on the account as what they are looking for |
interest, source |
No | Noted on the account's history |
students |
No | Up to 10. Each needs first_name; last_name defaults to the account's; dob is YYYY-MM-DD; gender and medical_notes are optional |
A plain HTML form cannot post a list, so a form post may send one student as
student_first_name, student_last_name, student_dob, student_gender and
student_medical_notes.
Returns 201:
{
"ok": true,
"data": {
"received": true,
"account_id": 81,
"account_number": "ACC-00081",
"students": [
{ "id": 501, "first_name": "Riley", "last_name": "Example",
"dob": "2017-05-14", "status": "prospect" },
{ "id": 502, "first_name": "Jordan", "last_name": "Example",
"dob": "2014-11-02", "status": "prospect" }
]
},
"meta": { }
}
/prospects always makes a new account. If the email or phone matches a family
the school already has, the school is warned to check for a duplicate. Nothing
is merged automatically. A refused field comes back with error.field naming
it, so your form can point at it.
A website cannot create a parent-portal login or store a card through this API.
To take payment, send the visitor to a signup_url from /leads.
Add a text input named website, hidden with CSS and left empty. People never
see it; spam bots fill in every field they find. A submission with it filled in
gets a normal-looking answer and is thrown away.
<input type="text" name="website" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px;" aria-hidden="true">
<form id="enquiry">
<input name="first_name" placeholder="First name" required>
<input name="last_name" placeholder="Last name" required>
<input name="email" type="email" placeholder="Email" required>
<input name="phone" placeholder="Phone">
<textarea name="message" placeholder="How can we help?"></textarea>
<input type="text" name="website" tabindex="-1" autocomplete="off"
style="position:absolute;left:-9999px;" aria-hidden="true">
<button>Send</button>
</form>
<script>
document.getElementById('enquiry').addEventListener('submit', async (e) => {
e.preventDefault();
const res = await fetch('https://YOUR-DOJO-ADDRESS/pub/dm/v1/leads', {
method: 'POST',
headers: { 'Authorization': 'Bearer YOUR_PUBLISHABLE_KEY',
'Content-Type': 'application/json' },
body: JSON.stringify(Object.fromEntries(new FormData(e.target)))
});
const out = await res.json();
alert(out.ok ? 'Thanks! We will be in touch.' : out.error.message);
});
</script>
| What | Limit |
|---|---|
| Reads, per key | 600 requests every 5 minutes |
| Submissions, per key | 120 an hour |
| Submissions, per visitor | 5 an hour |
A limited request returns 429 with a Retry-After header in seconds. Successful
responses carry X-RateLimit-Limit and X-RateLimit-Remaining.
| Status and code | Meaning |
|---|---|
401 missing_key |
No key was sent. Check the Authorization header. |
401 invalid_key |
The key is wrong, or the school revoked it. |
401 key_expired |
The school rotated this key and the old one has stopped. Ask for the new key. |
403 insufficient_scope |
The key is not allowed to submit leads. |
403 origin_not_allowed |
Your website is not on the key's list. Ask the school to add it, with https://. |
400 bad_request |
A date is malformed, or the range is too long. |
400 missing_field |
A required field is empty. The message (and error.field on /prospects) names it. |
400 invalid_field / invalid_email |
A value is not valid, such as an email or a date of birth. |
400 too_many_students |
More than 10 students in one /prospects call. |
429 rate_limited |
Too many requests. Wait for Retry-After. |
500 server_error / 503 server_error |
Something went wrong on our side. Try again. |
A CORS error in the browser usually means origin_not_allowed. The school can
confirm it under Recent activity, which lists every call and why any were
refused.
That is what makes a publishable key safe to put in a web page.