Developers · API 1.0.0
Mentornity API and webhooks
Read your programme's members, matches and meetings over a REST API, and get a signed webhook the moment they change. Included in paid plans.
- Base URL
https://api.mentornity.com/v1- Format
- JSON, UTF-8, snake_case
- Auth
- Bearer API key, one per programme
- Spec
- openapi.json
Quickstart
- In the admin panel, open Settings, then the API & Webhooks tab. If API access is not yet enabled for your programme, the tab has a button to ask us.
- Create a key, pick the permissions it needs, and copy it. It is shown once.
- Call the API from your server:
curl https://api.mentornity.com/v1/members?limit=2 \
-H "Authorization: Bearer mtk_live_..." {
"object": "list",
"data": [
{
"object": "member",
"id": "6650f1c2a7...",
"user": { "object": "person", "id": "64a1...", "name": "Ada Lovelace", "email": "ada@example.com" },
"role": "mentee",
"status": "active",
"joined_at": "2026-09-01T08:30:00Z",
"left_at": null,
"updated_at": "2026-10-02T11:04:12Z"
}
],
"has_more": true,
"next_cursor": "NjY1MGYxYzJhNy4uLg"
} Authentication
Send the key in the Authorization header as Bearer <key>. Keys look like
mtk_live_ or mtk_test_ followed by 32 letters and digits. We store only a hash
of each key, so a lost key cannot be shown again: revoke it and create a new one.
A key belongs to exactly one programme and can read nothing outside it. Each key has permissions
(scopes); a request outside them returns 403 insufficient_scope.
| Scope | Grants |
|---|---|
programme:read | GET /programme |
members:read | GET /members, GET /members/{id} |
matches:read | GET /matches, GET /matches/{id} |
meetings:read | GET /meetings, GET /meetings/{id} |
Keep keys on your server. The API sends no CORS headers, so a browser page cannot call it, and a key in a web or mobile app would be readable by anyone. Revoking a key takes effect within a minute.
Environments
There is one base URL. The key decides the environment: mtk_test_ keys belong to a sandbox
programme filled with demo data, mtk_live_ keys to your live programme. Ask us for a sandbox
programme to build against; its keys work even before API access is enabled on your live programme.
Rate limits and quotas
| Limit | Default | Headers |
|---|---|---|
| Requests per key per minute | 60 | X-RateLimit-Limit, X-RateLimit-Remaining |
| Requests per programme per UTC day | 10,000 | X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset |
| Webhook endpoints per programme | 5 | |
| Active keys per programme | 10 |
Over a limit you get 429 with a Retry-After header (seconds). Higher limits
are available on the Enterprise plan; write to info@mentornity.com.
Pagination and sync
Lists return {"object": "list", "data": [...], "has_more", "next_cursor"}. Pass
next_cursor back as cursor until has_more is false. limit is 1 to 100
(default 50). Unknown query parameters are rejected with 400, so a typo cannot silently return everything.
For an incremental sync, store the time you started the last run and pass it as
updated_since next time. Combine it with webhooks: the webhook tells you something changed,
the API gives you the current state.
Errors
Errors use the HTTP status and a JSON body with a stable code and a readable message.
HTTP/1.1 429 Too Many Requests
Retry-After: 12
{ "error": { "code": "rate_limited", "message": "Rate limit exceeded: 60 requests per minute per key." } } | Status | code | Meaning |
|---|---|---|
| 400 | invalid_parameter | A parameter is unknown, repeated or malformed. |
| 401 | invalid_api_key | Missing, malformed, unknown or revoked key. |
| 403 | insufficient_scope | The key lacks the permission for this endpoint. |
| 403 | api_access_disabled | API access is not enabled for the programme. |
| 404 | not_found | No such object in this programme. |
| 404 | unknown_endpoint | No such endpoint in v1. |
| 405 | method_not_allowed | The API is read-only: GET and HEAD. |
| 429 | rate_limited | Per-key minute limit reached. |
| 429 | daily_quota_exceeded | Per-programme daily quota reached. |
| 429 | too_many_failed_attempts | Too many requests with invalid keys from your address. |
| 503 | service_busy | Too many requests in flight. Retry in a few seconds. |
| 5xx | internal_error | Our fault. Retry with backoff. |
Every response carries an X-Request-Id header. Quote it when you write to support.
Endpoints
- GET
/meDescribe the calling key - GET
/programmeGet the programme - GET
/membersList members - GET
/members/{id}Get a member - GET
/matchesList matches - GET
/matches/{id}Get a match - GET
/meetingsList meetings - GET
/meetings/{id}Get a meeting
GET /me
Describe the calling key
Programme, environment, scopes and limits of the key. Needs no scope; use it to test a key.
Returns ApiKey.
GET /programme
Get the programme
Scope: programme:read.
Returns Programme.
GET /members
List members
Mentors and mentees of the programme, current and past.
Scope: members:read. Ordered by id; page through with cursor.
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1-100 (default 50). |
cursor | query | string | next_cursor from the previous page. |
updated_since | query | string | Only objects changed at or after this time (RFC 3339 or YYYY-MM-DD). Use it for incremental sync. |
status | query | "active" | "passive" | "removed" | "converted" | Filter by status. |
role | query | "mentor" | "mentee" | mentor or mentee |
user_id | query | string | Only this person's memberships. |
Returns MemberList.
GET /members/{id}
Get a member
Scope: members:read.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string | Member id |
Returns Member.
GET /matches
List matches
Mentor-mentee (or mentor-group) pairings.
Scope: matches:read. Ordered by id; page through with cursor.
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1-100 (default 50). |
cursor | query | string | next_cursor from the previous page. |
updated_since | query | string | Only objects changed at or after this time (RFC 3339 or YYYY-MM-DD). Use it for incremental sync. |
status | query | "requested" | "active" | "completed" | "rejected" | "canceled" | "deleted" | Filter by status. |
Returns MatchList.
GET /matches/{id}
Get a match
Scope: matches:read.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string | Match id |
Returns Match.
GET /meetings
List meetings
Mentoring meetings scheduled in the programme.
Scope: meetings:read. Ordered by id; page through with cursor.
| Parameter | In | Type | Description |
|---|---|---|---|
limit | query | integer | Page size, 1-100 (default 50). |
cursor | query | string | next_cursor from the previous page. |
updated_since | query | string | Only objects changed at or after this time (RFC 3339 or YYYY-MM-DD). Use it for incremental sync. |
status | query | "active" | "canceled" | Filter by status. |
from | query | string | Meetings starting at or after (RFC 3339 or date). |
to | query | string | Meetings starting before (RFC 3339 or date). |
Returns MeetingList.
GET /meetings/{id}
Get a meeting
Scope: meetings:read.
| Parameter | In | Type | Description |
|---|---|---|---|
id required | path | string | Meeting id |
Returns Meeting.
Objects
Timestamps are RFC 3339 in UTC. Fields may be added at any time; ignore the ones you do not know.
Error
| Field | Type | Description |
|---|---|---|
error | object |
Person
A person. For an erased account only id is present.
| Field | Type | Description |
|---|---|---|
id | string | |
first_name | string | |
last_name | string | |
email | string (email) |
ApiKey
| Field | Type | Description |
|---|---|---|
object | "api_key" | |
id | string | |
name | string | |
prefix | string | First characters of the key, for recognition. |
environment | "live" | "test" | |
scopes | Scope[] | |
programme | object | |
limits | object |
Programme
| Field | Type | Description |
|---|---|---|
object | "programme" | |
id | string | |
name | string | |
domain | string | Programme address segment, as in mentornity.com/<domain>. |
environment | "live" | "test" | |
language | string | |
created_at | string (date-time) |
Member
A membership: one person in one role in the programme. A person who is both mentor and mentee has two members. Re-adding a removed person creates a new member.
| Field | Type | Description |
|---|---|---|
object | "member" | |
id | string | |
user | Person | |
role | "mentor" | "mentee" | |
status | "active" | "passive" | "removed" | "converted" | passive: deactivated; removed: removed from the programme; converted: the person's role was changed and a new member exists for the new role. |
joined_at | string (date-time) | |
left_at | string | null (date-time) | Set when status is not active. |
updated_at | string (date-time) |
MatchParty
A user (type: user, with Person fields) or a group (type: group, with name).
| Field | Type | Description |
|---|---|---|
type | "user" | "group" | |
id | string | |
first_name | string | |
last_name | string | |
email | string | |
name | string |
Match
| Field | Type | Description |
|---|---|---|
object | "match" | |
id | string | |
status | "requested" | "active" | "completed" | "rejected" | "canceled" | "deleted" | |
mentor | Person | |
mentee | MatchParty | |
requested_at | string | null (date-time) | |
started_at | string | null (date-time) | |
ended_at | string | null (date-time) | |
created_at | string (date-time) | |
updated_at | string (date-time) |
Meeting
| Field | Type | Description |
|---|---|---|
object | "meeting" | |
id | string | |
status | "active" | "canceled" | |
type | string | |
title | string | |
platform | string | Video platform, when one is set. |
start_at | string (date-time) | |
end_at | string (date-time) | |
organizer | Person | |
participants | Participant[] | |
canceled_at | string | null (date-time) | |
created_at | string (date-time) | |
updated_at | string (date-time) |
WebhookEvent
Body of every webhook POST.
| Field | Type | Description |
|---|---|---|
id | string | Event id, also sent as the webhook-id header. Use it to ignore duplicates. |
type | "member.joined" | "member.left" | "match.requested" | "match.started" | "match.ended" | "meeting.scheduled" | "meeting.canceled" | "webhook.test" | |
created_at | string (date-time) | |
programme_id | string | |
data | Member | Match | Meeting | object | The Member, Match or Meeting as it is when the event is sent. |
MemberList, MatchList, MeetingList
A page of results: object ("list"), data (array), has_more (boolean), next_cursor (string or null).
Webhooks
Add an HTTPS endpoint in the API & Webhooks tab and choose its events. We POST each
event as JSON. The signing secret (whsec_...) is shown once; you can create a new one at any time.
| Event | When |
|---|---|
member.joined | A mentor or mentee joined the programme (data: Member). |
member.left | A member is no longer active: deactivated, removed or converted (data: Member). |
match.requested | A mentee requested a mentor (data: Match). |
match.started | A match became active (data: Match). |
match.ended | A match completed, was canceled, rejected or deleted (data: Match). |
meeting.scheduled | A meeting was created (data: Meeting). |
meeting.canceled | A meeting was canceled (data: Meeting). |
webhook.test | Sent by the Send test event button. |
POST /your/endpoint HTTP/1.1
Content-Type: application/json
webhook-id: evt_3f9a1c0d5e7b2a4c6d8e0f12
webhook-timestamp: 1761037200
webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4=
{
"id": "evt_3f9a1c0d5e7b2a4c6d8e0f12",
"type": "match.started",
"created_at": "2026-10-21T09:00:00Z",
"programme_id": "5f2b...",
"data": { "object": "match", "id": "6651...", "status": "active", ... }
} Verifying the signature
Signatures follow the Standard Webhooks specification,
so its libraries work as they are. The signature is v1, plus the base64 HMAC-SHA256 of
webhook-id.webhook-timestamp.body, keyed with the base64-decoded part of your secret after
whsec_. Use the raw body as received, before any JSON parsing, and reject timestamps more than
five minutes old.
Node.js
import crypto from 'node:crypto';
// secret: "whsec_..." from the admin panel. body: the RAW request body (string or Buffer).
export function verifyWebhook(secret, headers, body) {
const id = headers['webhook-id'];
const ts = headers['webhook-timestamp'];
const sigs = String(headers['webhook-signature'] || '').split(' ');
if (!id || !ts || Math.abs(Date.now() / 1000 - Number(ts)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');
const expected = crypto.createHmac('sha256', key).update(`${id}.${ts}.${body}`).digest();
return sigs.some((s) => {
const [version, value] = s.split(',');
if (version !== 'v1' || !value) return false;
const given = Buffer.from(value, 'base64');
return given.length === expected.length && crypto.timingSafeEqual(given, expected);
});
} Python
import base64, hashlib, hmac, time
def verify_webhook(secret: str, headers: dict, body: bytes) -> bool:
msg_id = headers.get("webhook-id")
ts = headers.get("webhook-timestamp")
if not msg_id or not ts or abs(time.time() - int(ts)) > 300:
return False
key = base64.b64decode(secret.removeprefix("whsec_"))
signed = f"{msg_id}.{ts}.".encode() + body
expected = base64.b64encode(hmac.new(key, signed, hashlib.sha256).digest()).decode()
for part in headers.get("webhook-signature", "").split(" "):
version, _, value = part.partition(",")
if version == "v1" and hmac.compare_digest(value, expected):
return True
return False Delivery and retries
- Answer with any 2xx within 10 seconds. Do slow work after you answer.
- Anything else is retried 7 more times: after 1 minute, 5 minutes, 30 minutes, 2 hours, 6 hours, 12 hours and 24 hours.
- Delivery is at least once and not ordered. Deduplicate by
webhook-id, and re-read the object from the API when order matters. - Events are detected within about a minute of the change.
- An endpoint that fails every delivery for 72 hours is paused; turn it back on in the panel. The panel lists the last deliveries with their status and response, and can resend one or send a test event.
- Endpoints must be public HTTPS on port 443, outside mentornity.com. We do not follow redirects.
Versioning and support
This is version 1. Breaking changes only ship in a new major version under a new path (/v2),
and v1 then stays available for at least 12 months. New endpoints, fields and event types can be added to v1
at any time.
Questions, higher limits or a sandbox programme: info@mentornity.com.
Include the X-Request-Id of the request in question.