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

Quickstart

  1. 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.
  2. Create a key, pick the permissions it needs, and copy it. It is shown once.
  3. 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.

ScopeGrants
programme:readGET /programme
members:readGET /members, GET /members/{id}
matches:readGET /matches, GET /matches/{id}
meetings:readGET /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

LimitDefaultHeaders
Requests per key per minute60X-RateLimit-Limit, X-RateLimit-Remaining
Requests per programme per UTC day10,000X-Quota-Limit, X-Quota-Remaining, X-Quota-Reset
Webhook endpoints per programme5
Active keys per programme10

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." } }
StatuscodeMeaning
400invalid_parameterA parameter is unknown, repeated or malformed.
401invalid_api_keyMissing, malformed, unknown or revoked key.
403insufficient_scopeThe key lacks the permission for this endpoint.
403api_access_disabledAPI access is not enabled for the programme.
404not_foundNo such object in this programme.
404unknown_endpointNo such endpoint in v1.
405method_not_allowedThe API is read-only: GET and HEAD.
429rate_limitedPer-key minute limit reached.
429daily_quota_exceededPer-programme daily quota reached.
429too_many_failed_attemptsToo many requests with invalid keys from your address.
503service_busyToo many requests in flight. Retry in a few seconds.
5xxinternal_errorOur fault. Retry with backoff.

Every response carries an X-Request-Id header. Quote it when you write to support.

Endpoints

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.

ParameterInTypeDescription
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.

ParameterInTypeDescription
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.

ParameterInTypeDescription
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.

ParameterInTypeDescription
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.

ParameterInTypeDescription
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.

ParameterInTypeDescription
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

FieldTypeDescription
error object

Person

A person. For an erased account only id is present.

FieldTypeDescription
id string
first_name string
last_name string
email string (email)

ApiKey

FieldTypeDescription
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

FieldTypeDescription
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.

FieldTypeDescription
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).

FieldTypeDescription
type "user" | "group"
id string
first_name string
last_name string
email string
name string

Match

FieldTypeDescription
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

FieldTypeDescription
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.

FieldTypeDescription
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.

EventWhen
member.joinedA mentor or mentee joined the programme (data: Member).
member.leftA member is no longer active: deactivated, removed or converted (data: Member).
match.requestedA mentee requested a mentor (data: Match).
match.startedA match became active (data: Match).
match.endedA match completed, was canceled, rejected or deleted (data: Match).
meeting.scheduledA meeting was created (data: Meeting).
meeting.canceledA meeting was canceled (data: Meeting).
webhook.testSent 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.