TatvaOS · API keys · Meetings API

Meetings API Integration Guide

Let your own software schedule classes and hand students the link to join them — a timetable, a student information system, anything that already knows who teaches what and when. A meeting you schedule is hosted by the teacher you name and lands on their calendar.

  1. What this is
  2. Two keys, not one
  3. Scheduling a class
  4. The fields
  5. The calendar
  6. Joining
  7. The timetable
  8. Changing and cancelling
  9. Responses
  10. Code examples
  11. What a key cannot do
  12. Security checklist
  13. What is not in this version

What this is

Two halves of one job. Your staff software schedules a class; your student software shows a Join button. Both talk to the same organisation over HTTPS with an API key, and neither needs anybody to sign in to TatvaOS to do its work.

A meeting created this way is an ordinary TatvaOS Connect meeting. The teacher you name is its host: it appears in their Connect list and on their calendar, and they can edit it by hand exactly as if they had created it themselves.

At a glance

Basehttps://core.tatvaos.com/api/v1/org/meetings
AuthenticationAuthorization: Bearer tvk_… — a Meetings API key
SchedulePOST /api/v1/org/meetings — needs meetings:schedule
TimetableGET /api/v1/org/meetings — needs meetings:schedule
One meetingGET /api/v1/org/meetings/{id} — either key
Join linkPOST /api/v1/org/meetings/{id}/join — needs meetings:join
CancelDELETE /api/v1/org/meetings/{id} — needs meetings:schedule
Rate limit30 requests a minute, per calling address

Two keys, not one

A meetings key can be given either or both of two permissions. Create them in the console under Organisation → API keys → Meetings API. Each key is shown once, when you make it.

PermissionWhat it allows
meetings:scheduleCreate, reschedule and cancel meetings.
meetings:joinRead a meeting it holds the id of, and get its join link. Changes nothing, cannot list the timetable, and never sees an email address or account id.

Use two separate keys. Give the staff half of your software a meetings:schedule key, and the student half a meetings:join key. If one key did both, the copy sitting in a student-facing portal could cancel every class in the school. Revoking one key never affects the other.

The student key is designed to be the one that leaks. It lives in whatever your student portal runs on. So it is built to be worth little if it does: it cannot list the organisation's meetings, it cannot ask whether an address belongs to your organisation, and nothing it reads names a teacher by anything but their display name.

Scheduling a class

One request. hostEmail is the teacher, and must be the address of an active person in your organisation — add them first through the People API or the console.

POST https://core.tatvaos.com/api/v1/org/meetings
Authorization: Bearer tvk_...
Content-Type: application/json

{
  "hostEmail": "r.iyer@yourschool.edu",
  "title":     "Physics — Class 10B",
  "startsAt":  "2026-09-21T09:00:00+05:30",
  "endsAt":    "2026-09-21T10:00:00+05:30",
  "timezone":  "Asia/Kolkata"
}

The answer is 201 and the meeting:

{
  "id":        "6f1b...",
  "code":      "zQ6rH58JaxkJzRw4-uZg4g",
  "joinUrl":   "https://connect.tatvaos.com/connect/room/zQ6rH58JaxkJzRw4-uZg4g",
  "title":     "Physics — Class 10B",
  "status":    "scheduled",
  "startsAt":  "2026-09-21T03:30:00+00:00",
  "endsAt":    "2026-09-21T04:30:00+00:00",
  "timezone":  "Asia/Kolkata",
  "host":      { "id": "d111...", "email": "r.iyer@yourschool.edu", "name": "R Iyer" },
  "waitingRoom":    "guests",
  "allowGuests":    true,
  "needsPassword":  false,
  "onCalendar":     true
}

Store the id. It is what you use to fetch, change or cancel the meeting later. joinUrl is the link students open, and it does not change for the life of the meeting.

Times come back in UTC. Send whatever offset you like — +05:30 is read correctly — and TatvaOS stores the instant. timezone is the zone the meeting is held in, which is what the calendar entry and the invitation display.

The fields

FieldRequiredWhat it is
hostEmailyesThe teacher. Must be an active person in this organisation.
startsAtyesISO 8601 with an offset, e.g. 2026-09-21T09:00:00+05:30.
endsAtnoDefaults to an hour after the start. Must not be before it.
titlenoUp to 200 characters. Left out, it becomes "Firstname's meeting".
timezonenoIANA name. Defaults to Asia/Kolkata.
waitingRoomnoguests (default), everyone, or off. See below.
allowGuestsnotrue by default. false admits only people with a TatvaOS account.
passwordno4–100 characters. Everyone joining must type it.

Choosing a waiting room

SettingWho waits to be let inGood for
guestsAnyone without a TatvaOS account.The default. Students who have accounts walk straight in.
everyoneEveryone, including colleagues.Vivas, interviews, one-to-ones.
offNobody.A large class where admitting forty people one by one is not realistic.

The calendar

A scheduled meeting is put on the host teacher's own TatvaOS calendar as part of creating it, with the join link on the entry. There is nothing extra to call.

To put the class on students' calendars as well, invite them from Connect — that sends each person a proper calendar invitation by email. Inviting from this API is not in this version; see what is not in this version.

Joining

The join link is the meeting link, and you already have it from joinUrl. What the join call adds is what to tell the student — whether this meeting holds people at the door — so your software can say so before the class rather than after. It takes nothing but the meeting id.

POST https://core.tatvaos.com/api/v1/org/meetings/6f1b.../join
Authorization: Bearer tvk_...
{
  "joinUrl":       "https://connect.tatvaos.com/connect/room/zQ6rH...",
  "meetingId":     "6f1b...",
  "title":         "Physics — Class 10B",
  "startsAt":      "2026-09-21T03:30:00+00:00",
  "endsAt":        "2026-09-21T04:30:00+00:00",
  "hostName":      "R Iyer",
  "waitingRoom":   "guests",
  "allowGuests":   true,
  "needsPassword": false,
  "guidance":      "Open the link. Sign in to TatvaOS to go straight in; otherwise give your name and the host will let you in."
}
FieldWhat to do with it
waitingRoomguests: students with a TatvaOS account walk in, anyone else waits. everyone: all wait. off: nobody waits.
allowGuestsfalse means only people with a TatvaOS account can enter at all.
needsPasswordThe meeting has a password. Show it, or tell them to expect one.
guidanceA ready-made sentence for the combination above, if you would rather not write your own.

The call does not take a student's email, on purpose. Your software knows whether a student has a TatvaOS account — it is the thing that created it through the People API — and combining that with waitingRoom tells you exactly what they will see. An endpoint that confirmed whether an address belongs to your organisation would let anyone holding a leaked portal key enumerate your school.

Students who have TatvaOS accounts have the better experience. They join under their real name, skip the waiting room, and the teacher sees who is present. Adding students through the People API is what makes that true.

The timetable

What is on, for the whole organisation or for one teacher. This is the staff view and needs meetings:schedule — it names every teacher and every class, which is not something a student portal should be able to pull. The student side does not need it: it holds the ids of the classes your staff side scheduled.

GET /api/v1/org/meetings
GET /api/v1/org/meetings?hostEmail=r.iyer@yourschool.edu
GET /api/v1/org/meetings?from=2026-09-21T00:00:00Z&to=2026-09-28T00:00:00Z
ParameterDefaultWhat it does
from12 hours agoEarliest start time to include.
to30 days after fromLatest start time to include.
hostEmaileveryoneOne teacher's classes. An address you do not have returns an empty list, not an error.
cursor—The next value from the previous page. The window and teacher are remembered inside it, so from, to and hostEmail are ignored when it is present.

Cancelled meetings are never listed. The response is a page of up to 500, in start order, and carries next:

{
  "meetings": [ ...500 classes... ],
  "next": "eyJTIjoiMjAyNi0..."     ← pass this back as ?cursor= to get the rest
}

{
  "meetings": [ ...the remaining 37... ],
  "next": null                     ← the last page says so
}

Always follow next until it is null. A school with 600 classes in the window gets 500 on the first page, and a timetable that stops reading there looks complete while missing a hundred classes — which will be discovered by a parent, not by you. next is always present in the response: null on the last page, never absent.

Treat next as opaque. Pass it back alone and exactly as it came; do not build, edit or keep cursors. When cursor is supplied, from, to and hostEmail are ignored: the cursor already carries the window, the teacher and the position, so page two can never be a different question from page one. A cursor that cannot be read is refused with 400. That is a check of its format, not of where it came from: a hand-made cursor that happens to be well-formed is accepted and answered as the query it describes, within your own organisation only. So a cursor your code has altered does not fail loudly — it quietly answers a different question.

Paging is not a frozen list. Reading page by page never skips or repeats a class that stays where it was while you read. But the timetable can change between two page requests: a class scheduled at, or moved to, a time earlier than the point you have reached does not appear in that read, and a class moved later can appear twice. Match classes by id, and re-read the timetable on your usual schedule rather than treating one read as final.

Changing and cancelling

Cancelling needs meetings:schedule:

DELETE /api/v1/org/meetings/6f1b...
Authorization: Bearer tvk_...

→ 204 No Content

It is safe to call twice: a meeting that is already cancelled answers 204 again. A meeting that is running answers 409 — ending a live class is a different act with different consequences for the people in it, and it is done from Connect.

Rescheduling is not on this API in this version. To move a class, cancel it and schedule the new time — or have the teacher change it in Connect, which moves the calendar entry for them. Sending a new startsAt without a matching endsAt is the one mistake worth naming: if the new start falls after the old end, the change is refused with "The meeting cannot end before it starts."

Responses

CodeMeansWhat to do
200 / 201Done.—
204Cancelled.—
400Something in the request was wrong.Read error; it is a sentence. Do not retry unchanged.
401The key is missing, wrong or revoked.Check the header. A revoked key and an unknown key answer alike.
403The key is real but lacks the permission — including a join key asking for the timetable.Use the staff key, or tick the right box on a new one.
404No such meeting in this organisation.Check the id.
409The meeting is over, or running.Nothing to do.
429More than 30 requests in a minute from one address.Back off and retry — see below.

Every failure body is { "error": "a sentence" }. The sentence is meant to be shown to whoever is using your software.

Retries

Retry 429 and 5xx with a growing wait — one second, then two, then four, a few times. Never retry 400, 401, 403 or 409: the answer will not change.

Code examples

In each of these, the key comes from the environment. It is never in the source.

curl

curl -sS -X POST https://core.tatvaos.com/api/v1/org/meetings \
  -H "Authorization: Bearer $TATVAOS_MEETINGS_KEY" \
  -H 'Content-Type: application/json' \
  -d '{
        "hostEmail": "r.iyer@yourschool.edu",
        "title":     "Physics — Class 10B",
        "startsAt":  "2026-09-21T09:00:00+05:30",
        "endsAt":    "2026-09-21T10:00:00+05:30"
      }'

Node.js (18+, built-in fetch)

const BASE = 'https://core.tatvaos.com/api/v1/org/meetings';

async function call(path, init = {}) {
  const r = await fetch(BASE + path, {
    ...init,
    headers: {
      Authorization: `Bearer ${process.env.TATVAOS_MEETINGS_KEY}`,
      'Content-Type': 'application/json',
      ...init.headers,
    },
  });
  if (r.status === 204) return null;
  const body = await r.json().catch(() => ({}));
  if (!r.ok) throw new Error(body.error ?? `HTTP ${r.status}`);
  return body;
}

// A teacher schedules a class.
const meeting = await call('', {
  method: 'POST',
  body: JSON.stringify({
    hostEmail: 'r.iyer@yourschool.edu',
    title:     'Physics — Class 10B',
    startsAt:  '2026-09-21T09:00:00+05:30',
    endsAt:    '2026-09-21T10:00:00+05:30',
  }),
});

// A student opens their timetable. (In your real code this runs with the
// student-side key, not the staff one.)
const link = await call(`/${meeting.id}/join`, { method: 'POST' });
console.log(link.joinUrl, link.guidance);

Python (requests)

import os, requests

BASE = "https://core.tatvaos.com/api/v1/org/meetings"
HEAD = {"Authorization": f"Bearer {os.environ['TATVAOS_MEETINGS_KEY']}"}

def check(r):
    if r.status_code == 204:
        return None
    body = r.json() if r.content else {}
    if not r.ok:
        raise RuntimeError(body.get("error", f"HTTP {r.status_code}"))
    return body

meeting = check(requests.post(BASE, headers=HEAD, json={
    "hostEmail": "r.iyer@yourschool.edu",
    "title":     "Physics — Class 10B",
    "startsAt":  "2026-09-21T09:00:00+05:30",
    "endsAt":    "2026-09-21T10:00:00+05:30",
}))

link = check(requests.post(f"{BASE}/{meeting['id']}/join", headers=HEAD))
print(link["joinUrl"], link["guidance"])

A week's timetable, end to end

The shape most ERPs need: publish next week's classes, then show each student their own list.

# Staff side — a key with meetings:schedule
for row in timetable_for_next_week():          # your data
    try:
        m = check(requests.post(BASE, headers=STAFF, json={
            "hostEmail": row.teacher_email,
            "title":     f"{row.subject} — {row.class_name}",
            "startsAt":  row.starts_at.isoformat(),
            "endsAt":    row.ends_at.isoformat(),
            "waitingRoom": "off",              # a class of forty
        }))
        row.save_meeting_id(m["id"])           # keep it; you need it to cancel
    except RuntimeError as e:
        log.warning("could not schedule %s: %s", row, e)

# Student side — a DIFFERENT key, meetings:join only
for row in classes_for(student):
    link = check(requests.post(f"{BASE}/{row.meeting_id}/join", headers=STUDENT))
    show_join_button(row, link["joinUrl"], note=link["guidance"])

What a key cannot do

Deliberate limits. None of these are oversights, and none can be unlocked by ticking something.

Security checklist

What is not in this version

Named so you can plan around them rather than discover them.

This is what has not been built, named so you can plan around it.