Start here
Environments
The API has two live environments: production, and a sandbox for building and testing your integration. This page covers both, explains how to reach the API (HTTPS, from your servers only), and shows how to monitor it.
Base URLs
| Environment | Base URL | Status | Use it for |
|---|---|---|---|
| Production | https://api.main-team.org/v1 | Available | Real students, real exams, real results |
| Sandbox | https://apisnd.main-team.org/v1 | Available | Building and testing your integration without touching real data |
Every API route is under /v1. The version is part of the path, so /v1 stays stable until a future major version, and a new major version would get a new prefix. The bare host, https://api.main-team.org/, answers 404 not_found, as does any path under /v1 that is not a route. See Versioning for what can change within /v1.
Keep the base URL in configuration, not in code, so that moving between environments only means changing a setting:
# production
MTO_API_BASE=https://api.main-team.org/v1
# sandbox
# MTO_API_BASE=https://apisnd.main-team.org/v1
The official client libraries accept only the production base URL. To call the sandbox, send plain HTTPS requests, or use a client of your own such as the one in Build your own client.
Production
Production is the live platform. Every request acts on real data:
- A student you register is a real account on the platform.
- A sign-in link you mint signs a real browser in as that student.
- An application you create enters a real student for a real exam session.
- Certificates and reports are the students' real results.
There is no test mode and no "dry run" flag in production. The API has no route that deletes a student, so a student registered by mistake stays registered. Build and test your integration against the sandbox first. When you move to production, agree your first writes with us (info@main-team.org). For example, use one agreed test student rather than inventing new ones on every run.
Warning
Do not point automated tests or CI pipelines at production. Read-only checks, such as validate-me or listing organizations, are safe. Anything that registers students or creates applications is not: run it against the sandbox.
Sandbox
The sandbox is a separate copy of the platform for integration work. It runs the same release of the API as production and follows each production release. It has its own data and holds no real students, and nothing you do there reaches production.
| Production | Sandbox | |
|---|---|---|
| Base URL | https://api.main-team.org/v1 | https://apisnd.main-team.org/v1 |
| Accounts | Issued by an operator on request | Separate accounts, issued by an operator from the sandbox's own panel. Ask at info@main-team.org |
| Credentials | Work only in production | Work only in the sandbox |
| Reference data | Real countries, grades, organizations and exams | Seeded reference data. The organization _ids are the same as in production; there may be fewer exams |
| Students | Real students | Only students registered for testing. Registration (POST /v1/student) is open, with the same permissions as in production |
| Emails | Real emails can be sent by the platform | No emails are sent |
| Payments | Real cards, real money | Stripe test mode: test cards only, no real money moves. See Test payments |
| Rate limits | See Rate limits | The same as production |
| "Try it" console on the reference pages | Not available | Available, with a token you paste |
| Official client libraries | Supported | Not supported: call the sandbox over HTTPS directly |
A production key does not work in the sandbox, and a sandbox key does not work in production. Mixing them up gives the usual 401 unauthorized (see Authentication), so check that your key and your base URL come from the same environment.
The routes, the request and response formats, the token rules, the permission model and the organization ids are the same in both environments, so code written against the sandbox needs only a new base URL and new credentials to run in production.
What to know when you test there:
- Use invented details. Every student in the sandbox is a test student. Don't enter real people's names or email addresses.
- No emails arrive. Nothing reaches a mailbox, including the 6-digit code the panel uses to confirm a student's email address. So the sandbox's panels don't ask students to confirm their address, and
emailConfirmedstaysfalsefor the students you register there. See Email confirmation. - Sign-in links open the sandbox's panels. A link you mint in the sandbox opens on
authsnd.main-team.organd signs the student in to the organization's sandbox panel, never to production's. A sandbox panel is atsnd.<brand>.orgwhere production's ismy.<brand>.org, for examplesnd.stemolympiad.org. Open it in a separate browser profile from any production session. - The same rate limits. 100 requests per 60 seconds per account and per operation, as in production, and the same limit per client address in front of the API. Don't load-test the sandbox; ask us if you need to measure throughput.
- The data may be reset. A reset removes the students and applications created since the last one. We tell you in advance.
- No availability guarantee. The sandbox can be briefly unavailable, for example while it is updated.
Test payments
The API never charges anyone. When an exam has a price, the student pays for the application in the organization's panel, by card. The sandbox's panels take these payments through Stripe in test mode, so no real money moves and no real card is charged.
To test a payment, create an application for an exam with a price, send the student to the panel with a sign-in link, and pay with one of Stripe's test cards:
| Card number | What happens |
|---|---|
4242 4242 4242 4242 | The payment succeeds without authentication |
4000 0025 0000 3155 | The payment asks for 3D Secure authentication first |
4000 0000 0000 9995 | The payment is declined for insufficient funds |
With each card, enter any future expiry date, any 3-digit CVC and any postal code. The panel takes payment by card only, on Stripe's checkout page, so a test card is all you need. Stripe lists more test cards, for other declines and authentication cases, at https://docs.stripe.com/testing.
Warning
Test cards are for the sandbox. In production only real cards work and every payment is real, so never use a test card there.
The "Try it" console
The reference pages have an interactive "Try it" console. It sends requests only to the sandbox. It never calls production: the production API accepts no requests from browser pages (see below), and you should never paste production credentials into a web page anyway.
To use it, sign a token with your sandbox apiKey and apiSecret on your own machine (see Authentication) and paste the token. Never paste your apiSecret: the console refuses anything that looks like one and sends nothing. The token is kept only in the page's memory, never stored.
Call the API from your servers
The API is for server-to-server use.
- No browser calls. Production sends no CORS headers, so a web page served from another origin cannot call the API. This is on purpose. A browser integration would need your
apiSecretin the page, which hands it to every visitor. The sandbox accepts browser requests from one origin only, these documentation pages, for the "Try it" console. - No mobile or desktop apps. For the same reason, never put credentials in an app you distribute. Anything shipped to users can be extracted.
- Put your own backend in between. Your web or mobile front end talks to your server; your server holds the secret, signs tokens and calls the API.
When a student needs to reach their panel, your server mints a sign-in link and redirects the browser to it. The browser never sees your token. See Sign-in links.
HTTPS only
Always use https://, and never send a request to http://. A request sent over plain HTTP has already carried your Authorization header across the network unencrypted, whatever the server does next. Put https:// in your base URL and do not let your HTTP client follow redirects to other hosts.
Responses carry Strict-Transport-Security, so browsers and clients that honor it stay on HTTPS for this host.
Response headers to know
| Header | When | Meaning |
|---|---|---|
X-Request-Id | Every response | The id of this request. Send your own (letters, digits and ._:;=+/@-, 1 to 256 characters) to have it reused; a value with any other character, or a longer one, is replaced. Without one, an id is generated for you. Treat it as an opaque string, because the format of a generated id can vary. It also appears as error.request_id in error bodies. Log it. |
X-RateLimit-Limit, X-RateLimit-Remaining, X-RateLimit-Reset | Every request counted against your rate limit, except a 429 | Your budget on this operation: the limit, what is left, and the seconds until the window resets. See Rate limits |
Retry-After | 429 only | The number of seconds to wait before you send again. A request sent sooner is refused too |
Content-Type | Every response with a body | application/json; charset=utf-8 for JSON. Certificate and report downloads carry the file's own type (normally application/pdf), or application/octet-stream when none is recorded |
Content-Disposition | Downloads | The file name. Prefer its filename*=UTF-8''… form |
Details on request ids, bodies and envelopes are in Requests and responses.
Monitoring: GET /v1/health
GET /v1/health tells you whether the API is up. It needs no token and does not count against any account's rate limit. The network in front of the API still limits how fast each client address may send requests, with or without a token, so poll at a steady interval, such as once a minute, not in a tight loop.
curl -s https://api.main-team.org/v1/health
{
"success": true,
"message": "Request completed successfully.",
"data": { "status": "ok" }
}
Treat any other status code, or no response within a few seconds, as "down".
/v1/health only checks that the service is running. It does not check your credentials, your roles or the data behind the routes. To check your integration end to end, also make one authenticated call on a schedule, for example GET /v1/api-account/validate-me every few minutes:
| Check | What a failure means |
|---|---|
GET /v1/health is not 200 | The API is down or unreachable from your network |
validate-me is 401 | Your token, secret, clock or account is the problem (see the 401 checklist) |
validate-me is 403 | Your account lost the api/* role (see Permissions) |
validate-me is 5xx while /v1/health is 200 | The API is running but has a problem on our side. Retry, and report it with the request_id if it persists |
validate-me counts against your rate limit like any other operation (100 requests per 60 seconds on that operation), which leaves plenty of room for a check every minute.
Timeouts
Set explicit timeouts on your HTTP client instead of relying on defaults. Reasonable starting values are 5 seconds to connect and 30 seconds to read a JSON response. Allow longer for certificate and report downloads, which stream PDF files. The API closes a connection whose request has not arrived completely within 30 seconds. See Retries and idempotency for which requests are safe to retry after a timeout.
Time and dates
Base your token timestamps (iat, exp) on UTC seconds since the Unix epoch, and keep your server clock synchronized with NTP. The API tolerates at most 30 seconds of clock difference. Date formats in request and response bodies are described in Requests and responses.
Versions and deprecation
The version of the API these pages describe is shown in the API reference. What changes, and when, is recorded in the changelog, with a page for every version.
- Before anything is removed or changed incompatibly, you get at least 6 months' notice, through the changelog and through
DeprecationandSunsetheaders on the affected responses. - Removals happen only in a new major version, which gets a new path prefix.
- Your code should ignore response fields it does not recognize, because new fields can be added within
/v1.
The full policy is in Versioning.
Support
Write to info@main-team.org. Include the environment, the request_id, the time in UTC and the operation you called. Never include your apiSecret or a token. See Support.