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.
- What this is
- Two keys, not one
- Scheduling a class
- The fields
- The calendar
- Joining
- The timetable
- Changing and cancelling
- Responses
- Code examples
- What a key cannot do
- Security checklist
- 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
| Base | https://core.tatvaos.com/api/v1/org/meetings |
| Authentication | Authorization: Bearer tvk_… — a Meetings API key |
| Schedule | POST /api/v1/org/meetings — needs meetings:schedule |
| Timetable | GET /api/v1/org/meetings — needs meetings:schedule |
| One meeting | GET /api/v1/org/meetings/{id} — either key |
| Join link | POST /api/v1/org/meetings/{id}/join — needs meetings:join |
| Cancel | DELETE /api/v1/org/meetings/{id} — needs meetings:schedule |
| Rate limit | 30 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.
| Permission | What it allows |
|---|---|
meetings:schedule | Create, reschedule and cancel meetings. |
meetings:join | Read 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
| Field | Required | What it is |
|---|---|---|
hostEmail | yes | The teacher. Must be an active person in this organisation. |
startsAt | yes | ISO 8601 with an offset, e.g. 2026-09-21T09:00:00+05:30. |
endsAt | no | Defaults to an hour after the start. Must not be before it. |
title | no | Up to 200 characters. Left out, it becomes "Firstname's meeting". |
timezone | no | IANA name. Defaults to Asia/Kolkata. |
waitingRoom | no | guests (default), everyone, or off. See below. |
allowGuests | no | true by default. false admits only people with a TatvaOS account. |
password | no | 4–100 characters. Everyone joining must type it. |
Choosing a waiting room
| Setting | Who waits to be let in | Good for |
|---|---|---|
guests | Anyone without a TatvaOS account. | The default. Students who have accounts walk straight in. |
everyone | Everyone, including colleagues. | Vivas, interviews, one-to-ones. |
off | Nobody. | 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.
- Rescheduling moves the entry — it is never duplicated.
- Cancelling removes it, so a cancelled class does not sit on anybody's Monday.
onCalendarin the response tells you it happened.
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."
}
| Field | What to do with it |
|---|---|
waitingRoom | guests: students with a TatvaOS account walk in, anyone else waits. everyone: all wait. off: nobody waits. |
allowGuests | false means only people with a TatvaOS account can enter at all. |
needsPassword | The meeting has a password. Show it, or tell them to expect one. |
guidance | A 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
| Parameter | Default | What it does |
|---|---|---|
from | 12 hours ago | Earliest start time to include. |
to | 30 days after from | Latest start time to include. |
hostEmail | everyone | One 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
| Code | Means | What to do |
|---|---|---|
200 / 201 | Done. | — |
204 | Cancelled. | — |
400 | Something in the request was wrong. | Read error; it is a sentence. Do not retry unchanged. |
401 | The key is missing, wrong or revoked. | Check the header. A revoked key and an unknown key answer alike. |
403 | The 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. |
404 | No such meeting in this organisation. | Check the id. |
409 | The meeting is over, or running. | Nothing to do. |
429 | More 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.
- It cannot host a meeting itself. Every meeting belongs to a named person, so a teacher can always edit their own class in TatvaOS.
- It cannot end a meeting that is running. A batch job must not be able to hang up on a class in progress.
- It cannot add people. That is the People API, and a separate key.
- It cannot make a meeting private or recorded. Whether a meeting can be recorded is chosen once, when it is created, and can never change afterwards — so it is not something an API caller sets.
- It cannot read what happened in a meeting. No recordings, no captions, no minutes, no chat.
- A join key cannot see who is in your organisation. It cannot list meetings, cannot look up an address, and is never shown an email or account id.
- It cannot see another organisation. A key names exactly one, and that boundary is enforced by the database, not by the code that answers you.
Security checklist
- Two keys. Schedule on the staff side, join on the student side. Never one key for both.
- Out of source control. An environment variable or a secret store, never a commit.
- Server-side only. A key in a browser, a mobile app or a student portal's front end is a key anybody can read and reuse.
- One key per program. Named for what it does, so revoking one never takes down another.
- Watch "Last used". The console shows it. A key nobody uses is a key to revoke.
- Revoke immediately if a key is ever pasted into a ticket, a log or a chat. Revoking is instant and creating a replacement takes a moment.
- Do not log the key. It appears nowhere in TatvaOS's own logs, and should appear nowhere in yours.
What is not in this version
Named so you can plan around them rather than discover them.
- Rescheduling. Cancel and re-create, or change it in Connect.
- Inviting students by email from the API. The calendar entry is created for the host only; emailed invitations are sent from Connect.
- Per-student join links. The join link is the meeting link, so anyone holding it can open the door — the waiting room and the meeting password are what control who gets in. If you need attendance to be attributable, give your students TatvaOS accounts and set
waitingRoomtoguests, so only named people walk straight in. - Attendance reporting. Who joined and for how long is not on this API.
- Recurring meetings. Create each occurrence.
- Webhooks. Nothing is pushed to you; ask for the timetable when you need it.
This is what has not been built, named so you can plan around it.