API reference
The API
What each endpoint is for, how a program authenticates, pages a list, retries a write and reads an error, and what the version commits to.
Two ways to call it
There are two ways to call Kestrel. The screens use a browser session; a program uses a scoped token against /api/v1. Both are checked on the server, by the same code, on each request.
/api/v1 is the supported surface. It is versioned, and the commitments under Versioning apply to it. The /api/app routes serve Kestrel's own screens, and they can change without notice. Build against /api/v1 alone.
The reference is published in full, because you should be able to decide whether to integrate with a vendor before you sign anything. You can read all of it here, with no partner conversation first.
The reference is generated
The machine-readable reference is an OpenAPI document the server produces from its own route table. It is not maintained by hand, so it cannot drift from the routes it describes.
curl https://app.kestrel.hoverpoint.io/_openapi.jsonPoint your generator at it. It is built from the schemas each endpoint validates with, so each request body, query parameter and response it describes is the one the server accepts. It lists the endpoints and nothing else.
Two calls that need no account
Both are useful for checking connectivity.
curl https://app.kestrel.hoverpoint.io/health
{ "status": "healthy", "uptime": 1284, "version": "2026-08-29T04:11:00Z" }version is the build timestamp. It tells one deployment from the next without naming the source revision the build was cut from. The route answers while database migrations are still running, so a health check can tell "starting" from "broken".
curl https://app.kestrel.hoverpoint.io/api/helloAuthentication
For a person, sign-in is passwordless: a passkey, or a one-time code sent by email. It produces a session cookie, and that cookie is what the /api/app endpoints check. There is no password to send, and therefore no basic auth.
For a program, /api/v1 takes a bearer token in the Authorization header.
curl -H "Authorization: Bearer kst_…" https://app.kestrel.hoverpoint.io/api/v1/peopleEach route that serves student data checks its caller itself. The browser-side route guard exists to make navigation behave, and the access control is on the server.
Tokens
- Who issues it
- An administrator at the school, under Settings, API tokens. No-one has to ask us.
- What it is for
- The administrator names what the token is for and ticks its scopes.
- Its school
- A token names one school, and it cannot be pointed at another.
- The secret
- Shown once, when the token is issued, and stored nowhere. We keep a hash, so we cannot resend it and neither can anyone who reaches our database.
- Its expiry
- Chosen by the administrator who issues it: a year by default, and up to two years. The listing under Settings shows the day each token expires. From that moment each request presenting it is refused with
unauthenticated, the same answer a revoked or unknown token gets. - Rotating it
- Issue a new token with the same scopes, move your integration onto it, then revoke the old one. Both work in between, so the switch needs no pause. Rotate before the expiry, and at once if a secret may have been seen.
- Revoking it
- An administrator can revoke a token at any time, and the next request presenting it is refused.
Scopes
A token's scopes are the same capability names a plugin is granted. A token cannot read a field its scopes do not reach, and those fields are absent from the response. Ask for the narrowest set that does your job.
- person.read
- Read names, dates of birth and the name history of the students, parents and staff at this school.
- affiliation.read
- Read whether someone is a student, staff member, applicant, volunteer, contractor, prac student or board member, and when that started and ended.
- billing.read
- Read who pays each student's fees, in what shares and for which charge categories, and which concessions and scholarships apply.
- enquiry.write
- Record an enquiry from a family: the child, the adult who asked, how to reach them, and where they heard of the school.
- tour.read
- Read the tours a school has scheduled that are still ahead: the title, the moment, the venue, the campus, who it is for and how many places are left.
- tour.book
- Book a family into a tour from the school's website: the child, the adult who asked, how to reach them, and the tour they chose.
- application.write
- Send the school an application from another admissions system, with the child, the adults and the place sought, start or withdraw a child on an application, and enrol a child admitted elsewhere.
- starter.read
- Read the children who have started at the school, or are due to, in a window of days: the name, the date of birth, the day, the year level, the campus and the adults with standing, with how to reach them.
- roster.read
- Read the school's roster as OneRoster or School Data Sync files: each current student and teaching member of staff by name, year level and sign-in name, each class and course, and who is in each class.
- staff.import
- Send the school's staff export: who is employed, as what, and the checks the HRIS keeps for them.
- people.sync
- Send the school's student information system export: its students, parents and contacts, their homes, and who may act for each student.
- export.read
- List the nightly warehouse files this school produces and fetch each one through a signed URL that expires.
- restricted.read
- Read parenting orders, court documents, exit reasons and the merge history behind an identity decision.
People and their roles
Who the school has, and who is a student, staff member, applicant, volunteer, contractor, prac student or board member there, with the span each of those has run for. A nightly sync is the people, then the affiliations, each read with a page loop.
GET /api/v1/people
Scope person.read · 300 requests a minute
A page of the people at the school, ordered by name, with their names and dates of birth. A record the school has deleted is left out.
curl -H "Authorization: Bearer kst_…" "https://app.kestrel.hoverpoint.io/api/v1/people?limit=100"
{ "items": [ { "id": "6f1c…", "familyName": "Nguyen", "givenNames": "Mai", … } ], "nextCursor": "eyJvIjoxMDB9" }GET /api/v1/people/{id}
Scope person.read · 300 requests a minute
One person, by the id a page of people gave. A person at another school is not_found, so the answer never says that a record exists somewhere you cannot reach.
curl -H "Authorization: Bearer kst_…" https://app.kestrel.hoverpoint.io/api/v1/people/6f1c…GET /api/v1/affiliations
Scope affiliation.read · 300 requests a minute
A page of affiliations, newest first. state=open gives the roll as it stands today, and by default closed spans come back as well, so a sync can see who has left. kind=student narrows it to one kind.
curl -H "Authorization: Bearer kst_…" "https://app.kestrel.hoverpoint.io/api/v1/affiliations?state=open&kind=student"
{ "items": [ { "personId": "6f1c…", "kind": "student", "validFrom": "2024-01-29", "validTo": null } ], "nextCursor": null }GET /api/v1/alumni
Scope affiliation.read · 300 requests a minute
A page of former students, ordered by name, with the year each left in the school's own time zone. The school decides who counts: any former student, or only those who completed their final year. Where it counts completion a page can have fewer than limit items and still give a cursor. asAt counts the alumni as at a past day. Names need person.read as well.
curl -H "Authorization: Bearer kst_…" "https://app.kestrel.hoverpoint.io/api/v1/alumni?asAt=2026-12-31"
{ "items": [ { "id": "a83e…", "familyName": "Okafor", "givenNames": "Ada", "leftAt": "2025-12-05T…", "exitYear": 2025 } ], "nextCursor": null }Who pays
Who pays a student's fees, for a finance platform to sync from. The shares are basis points and the concessions are kinds: the API has no amounts, because pricing is the finance platform's.
GET /api/v1/people/{id}/billing
Scope billing.read · 300 requests a minute
Each responsibility on one student, open and closed, with the payer's id, the charge category and the share in basis points, and each concession that applies. A category's open shares come to 10,000. A category with no rows of its own is paid the way all is.
curl -H "Authorization: Bearer kst_…" https://app.kestrel.hoverpoint.io/api/v1/people/6f1c…/billing
{ "id": "6f1c…", "responsibilities": [ { "payerId": "c20d…", "category": "all", "share": 5000, … } ], "concessions": [ … ] }Enquiries and tour bookings
What a school's own website posts when a family enquires or books a tour. Each write answers with a reference and the moment it was received, and the token that sent it reads nothing back. Send an Idempotency-Key with each write, because a form that retried a timed-out post would otherwise enquire or book twice. The enquirer gives an email address or a phone number, or both.
POST /api/v1/enquiries
Scope enquiry.write · 30 requests a minute
An enquiry from the website's form. It opens a record on the child and on the adult who asked, and records how they heard of the school. channel is website unless the form names another of the school's channels, and a channel or relationship the school does not list is refused with the field named.
curl -X POST -H "Authorization: Bearer kst_…" -H "Idempotency-Key: 5d0b…" -H "Content-Type: application/json" \
-d '{ "student": { "familyName": "Lee", "givenNames": "Sam" }, "enquirer": { "familyName": "Lee", "givenNames": "Jo", "relationship": "guardian", "email": "jo@example.com" }, "intendedEntryYear": 2028 }' \
https://app.kestrel.hoverpoint.io/api/v1/enquiries
{ "id": "e41a…", "receivedAt": "2026-10-13T01:22:07.000Z" }GET /api/v1/tours
Scope tour.read · 60 requests a minute
The tours still ahead that have a place, soonest first, with the campus, who each is for and how many places are left. campus and audience narrow the list. It reads no family and no booking.
curl -H "Authorization: Bearer kst_…" "https://app.kestrel.hoverpoint.io/api/v1/tours?campus=senior"
{ "tours": [ { "id": "7b92…", "title": "Open morning", "startsAt": "2026-11-07T22:30:00.000Z", "venue": "Hall", "placesLeft": 14, … } ] }POST /api/v1/tours/{id}/bookings
Scope tour.book · 30 requests a minute
A booking from the website: the fields an enquiry takes, posted to the tour the family chose. The enquiry and the booking are made together, so a family is never booked without a record. A full tour is refused with conflict and tour-full in detail, and no enquiry is recorded.
curl -X POST -H "Authorization: Bearer kst_…" -H "Idempotency-Key: 9c3e…" -H "Content-Type: application/json" \
-d '{ "student": { … }, "enquirer": { … } }' \
https://app.kestrel.hoverpoint.io/api/v1/tours/7b92…/bookings
{ "id": "b07f…", "enquiryId": "e52c…", "receivedAt": "2026-10-13T01:24:51.000Z" }Applications and starters
What another admissions system sends after taking the application or deciding the admission itself, and the children starting soon for the system that opens their accounts. Each write answers with references only. Send an Idempotency-Key with each write, because a retried post would otherwise apply twice. A person is named by the personId the people endpoints give, or sent with their names, and one who may already be on file stops the whole send with conflict, naming the records they may be: send the right one's personId, or new: true. A person found is never updated.
POST /api/v1/applications
Scope application.write · 30 requests a minute
An application made somewhere else: the child with a date of birth, up to four adults with the first the one the school answers, and the year level and entry year sought. It opens at submitted, and the office takes it from there.
curl -X POST -H "Authorization: Bearer kst_…" -H "Idempotency-Key: 2a7d…" -H "Content-Type: application/json" \
-d '{ "student": { "familyName": "Lee", "givenNames": "Sam", "dateOfBirth": "2019-03-02" }, "adults": [ { "personId": "6f1c…", "relationship": "mother" } ], "placement": { "yearLevel": "PREP", "entryYear": 2027 } }' \
https://app.kestrel.hoverpoint.io/api/v1/applications
{ "id": "a19e…", "studentId": "c3d0…", "adultIds": [ "6f1c…" ], "receivedAt": "2026-10-14T01:22:07.000Z" }POST /api/v1/applications/{id}/events
Scope application.write · 30 requests a minute
A start or a withdrawal on an application. started puts the child on the roll from a day, at the year level, campus and load sent, and needs the family to have accepted an offer. withdrawn takes them off it, or closes an application that never reached a start, for a reason from the school's list. A refusal names the rung that is missing.
curl -X POST -H "Authorization: Bearer kst_…" -H "Idempotency-Key: 4b1f…" -H "Content-Type: application/json" \
-d '{ "kind": "started", "occurredOn": "2027-01-28", "placement": { "yearLevel": "PREP", "campus": "junior" } }' \
https://app.kestrel.hoverpoint.io/api/v1/applications/a19e…/events
{ "id": "e7c2…", "personId": "c3d0…", "kind": "started", "occurredOn": "2027-01-28" }POST /api/v1/enrolments
Scope application.write · 30 requests a minute
A child whose admission was decided somewhere else, started on the roll from a day. Each parent or guardian sent is given parental responsibility. A child already enrolled is refused with conflict, and the campus is required where the school has more than one.
curl -X POST -H "Authorization: Bearer kst_…" -H "Idempotency-Key: 8e0a…" -H "Content-Type: application/json" \
-d '{ "student": { … }, "carers": [ { … } ], "yearLevel": "7", "campus": "senior", "startsOn": "2027-01-28" }' \
https://app.kestrel.hoverpoint.io/api/v1/enrolments
{ "applicationId": "a2b4…", "studentId": "c3d0…", "carerIds": [ "6f1c…" ], "startsOn": "2027-01-28" }GET /api/v1/starters
Scope starter.read · 300 requests a minute
The children who started, or are due to start on an accepted offer, between from and to, earliest first, with the adults beside them. A child with an open protection or a court order in force is left out, and the office sends them by hand. At a school with more than one campus, a child whose campus the school has not matched to its SIS code is listed in waiting until it is.
curl -H "Authorization: Bearer kst_…" "https://app.kestrel.hoverpoint.io/api/v1/starters?from=2027-01-01&to=2027-02-28"
{ "items": [ { "applicationId": "a19e…", "personId": "c3d0…", "started": false, "startsOn": "2027-01-28", … } ], "adults": [ … ], "waiting": [], "nextCursor": null }Class rosters
The class roster, for a tool that reads OneRoster or School Data Sync. An administrator registers the tool under Settings, Rostering, which issues its token and chooses the identifier each person's username is read from. A token the school issued anywhere else is refused here as forbidden, even one with roster.read.
GET /api/v1/roster/{format}
Scope roster.read · 6 requests a minute
Each current student, each member of staff teaching or assisting a class, each class and course running today, and who is in each, as the files the format names. oneroster is a OneRoster 1.1 bulk set with its manifest, and sds is the seven-file School Data Sync v2.1 set. Each file is CSV in the answer. No email address, phone number or password is written.
curl -H "Authorization: Bearer kst_…" https://app.kestrel.hoverpoint.io/api/v1/roster/oneroster
{ "format": "oneroster", "asAt": "2026-10-13T01:30:00.000Z", "files": [ { "name": "users.csv", "content": "sourcedId,…" }, … ], "counts": { "students": 812, … } }Staff import
The school's HRIS posts its staff export here, and Kestrel never calls the HRIS. Before the first post, the school saves a mapping from the export's columns on the HRIS export screen, under Data imports, and the screen shows the mapping's id.
POST /api/v1/staff/import
Scope staff.import · 300 requests a minute
The same run as the office's upload. It creates staff, opens and closes their employment and records their checks, and answers with the counts and each row it could not settle. A person missing from the export comes back as a problem to settle, and their employment stays open. csv is the export as the HRIS writes it, up to 8 MB. Send an Idempotency-Key.
curl -X POST -H "Authorization: Bearer kst_…" -H "Idempotency-Key: 41fa…" -H "Content-Type: application/json" \
-d '{ "mappingId": "0c9d…", "csv": "Employee No,Surname,…" }' \
https://app.kestrel.hoverpoint.io/api/v1/staff/import
{ "run": { … }, "problems": [ … ], "counts": { "rows": 212, "created": 2, "opened": 3, "closed": 2, "checks": 4, "problems": 1 } }People sync
A school running Kestrel beside its SIS sends the SIS's export of students, parents and contacts here, and Kestrel never calls the SIS. Each person is matched on the SIS's own ID. What the run cannot settle waits for the registrar on the people sync screen, under Data imports.
POST /api/v1/people/sync
Scope people.sync · 300 requests a minute
The same run as the office's upload. It answers 202 with the run as staged, and the run applies a few seconds later. scope is full for a file of each person the SIS has, and changes for a file of only the people it changed. A run that would leave more than 2% absent or amend or create more than 20% is paused until somebody at the school confirms it, and so is the first run. A file with a line Kestrel could not read is failed and applies nothing. file is the export as CSV, up to 8 MB. Send an Idempotency-Key.
curl -X POST -H "Authorization: Bearer kst_…" -H "Idempotency-Key: 7be2…" -H "Content-Type: application/json" \
-d '{ "system": "synergetic", "scope": "full", "file": "id,kind,family_name,given_names,…" }' \
https://app.kestrel.hoverpoint.io/api/v1/people/sync
{ "run": { "id": "5d1e…", "status": "queued", "rows": 2140, "created": 3, "amended": 41, … }, "unreadable": [], "problems": [ … ] }Nightly exports
The warehouse files, for the school's own BI tool. Each night the school's record is written out as CSV files, one for each table the school's modules export, and this endpoint lists the newest of each. No column the school classifies as restricted is in a file, and no database credential is issued.
GET /api/v1/exports
Scope export.read · 20 requests a minute
Each file with its columns, its row count, its checksum and the path to fetch it from. The path stops working ten minutes after the answer, so list again rather than storing it. A file the job has not produced yet is absent.
curl -H "Authorization: Bearer kst_…" https://app.kestrel.hoverpoint.io/api/v1/exports
{ "files": [ { "key": "warehouse-enrolment-placements", "fileName": "…csv", "rows": 812, "checksum": "…", "url": "/api/v1/exports/3a7c…/file?grant=…", "expiresAt": "2026-10-13T01:40:00.000Z", "columns": [ … ] } ], "producedAt": "2026-10-13" }GET /api/v1/exports/{id}/file
Scope export.read · 60 requests a minute
The CSV itself, as an attachment under its own name. Call the url the listing gave, with the token that listed it: a path listed for another token or more than ten minutes ago is refused with forbidden.
curl -H "Authorization: Bearer kst_…" -o placements.csv "https://app.kestrel.hoverpoint.io/api/v1/exports/3a7c…/file?grant=…"Lists, and retrying a write
Each collection answers with items and a nextCursor, and each write accepts an Idempotency-Key, which you should send. If your request times out, you have no way to know whether it happened, and the key makes the retry safe.
GET /api/v1/people?limit=100
{ "items": [ … ], "nextCursor": "eyJvIjoxMDB9" }- limit
- How many items to return: 100 when you leave it out, and at most 1000.
- cursor
- The
nextCursorfrom the previous page. Leave it out for the first page. - items
- The page of results.
- nextCursor
- Pass it back as
cursorto get the next page, and stop once it comes back null. The cursor is opaque: do not parse it or do arithmetic on it, because what is inside it is ours to change. - Idempotency-Key
- A header each write accepts. Retrying with the same key returns the first attempt's answer without doing the work again. Keys are honoured for 24 hours.
Reusing a key for a different request is refused with conflict, because completing it under the first one's key would be worse than failing.
Rate limits
Each endpoint allows a number of requests a minute from one token. A request past it is refused with rate_limited, and the work is not done.
- The window
- A minute, counted for each token on each endpoint, so a busy sync on one endpoint leaves the others alone. The count starts again when the minute is up.
- The limit
- Each endpoint's section above gives its own. Most allow 300 requests a minute, and the generated document gives each operation's figure as
x-rate-limit. - X-RateLimit-Remaining
- On each response, so you can pace yourself before being refused.
- Retry-After
- On a refusal for too many requests, the number of seconds until the window resets.
HTTP/1.1 429 Too Many Requests
Retry-After: 23
X-RateLimit-Remaining: 0
{
"statusCode": 429,
"statusMessage": "This endpoint is being called too often. Wait 23 seconds and try again.",
"data": { "code": "rate_limited", "traceId": "4bf92f3577b34da6a3ce929d0e0e4736" }
} Wait the seconds Retry-After gives, then resend. A request sent sooner is refused again.
Errors
Each failure is JSON with the same shape, from all endpoints on both surfaces. statusMessage is written for a person; data is for your code. Branch on data.code. One status can have several meanings, and each code has one.
{
"statusCode": 422,
"statusMessage": "This palette was refused.",
"data": {
"code": "unprocessable",
"detail": ["The accent on the page in light mode is 2.4:1, and needs 3:1."],
"traceId": "0af7651916cd43dd8448eb211c80319c"
}
}- bad_requestHTTP 400
- The request was malformed, or it belongs to no school. Sending it again unchanged fails the same way.
- validation_failedHTTP 400
- A field was missing or in the wrong shape.
fieldsnames all of them, each by its path in your request. - unauthenticatedHTTP 401
- The request had no credential, or one that is no longer valid. Sign in again, or check the token. Do not retry unchanged.
- forbiddenHTTP 403
- The caller was identified and is not permitted. The message names the missing capability. Retrying does not help.
- step_up_requiredHTTP 403
- The endpoint asks for the person's passkey again, and the session has not proved it in the last five minutes. Complete a passkey sign-in, then resend.
- passkey_requiredHTTP 403
- The school requires staff to sign in with a passkey, and this session was made by an emailed code. Sign in with a passkey, then resend.
- no_portal_identityHTTP 403
- The account is signed in and the school has not given it family access, so the portal's endpoints answer it nothing.
- standing_withheldHTTP 403
- A family reader asked about a child for a purpose their standing on that child does not grant, or about a child they have no standing on. The answer is the same for both.
- section_offHTTP 403
- The school has switched this portal section off for families. The same endpoint still answers staff who have its capability.
- component_offHTTP 403
- The school does not run the part of Kestrel this endpoint belongs to, or keeps it to be read and this is a write. The same answer is given for each caller, a token's scopes included.
- preview_read_onlyHTTP 403
- A member of staff viewing the portal as a family sent a change. The view reads and never writes, so nothing was changed.
- not_foundHTTP 404
- The record or the route does not exist. A record another school has gets this answer too.
- conflictHTTP 409
- The record changed since you read it, or an
Idempotency-Keywas reused for a different request. Re-read, reapply, resend. - held_elsewhereHTTP 409
- The school keeps this part of the person's record in its student information system, which sends it here. The change is made there, and nothing was written.
- payload_too_largeHTTP 413
- The body is larger than the endpoint accepts.
- unprocessableHTTP 422
- The request was understood and refused.
detaillists all the reasons, one sentence each. - audience_unknownHTTP 422
- A message named a group, such as
activity:rowing, that no part of this deployment answers.detailnames each one. Nothing was sent. - audience_emptyHTTP 422
- A message's groups contained no-one who may be told, so no message was written. Change the groups or the purpose.
- no_consent_standingHTTP 422
- A consent named a giver who does not have parental responsibility for the child on the day it was given. The answer is the same whether the person never had it, had it and it closed, or an order or a protected record keeps them out, so the code says nothing about which. Nothing was recorded.
- subject_requiredHTTP 422
- A consent was recorded for a purpose that is about a named thing, an excursion say, and named none. Name the thing it is about. Nothing was recorded.
- ask_closedHTTP 422
- A parent answered a consent ask the school has closed, or one about a thing that has since left, been cancelled or stopped taking answers. Nothing was recorded.
- schedule_publishedHTTP 422
- A change named a line, a billing period or a draft of a fee schedule that has since been published. A published schedule is never changed: start a correction, which is a new version, and make the change there. Nothing was recorded.
- rate_limitedHTTP 429
- The caller has made too many requests.
Retry-Aftergives the number of seconds to wait. - internalHTTP 500
- The fault is ours. It is safe to retry once, after a delay. If it keeps happening, quote the
traceId. - not_implementedHTTP 501
- The request was understood, and this build has nowhere to put it. The message names what is missing, and retrying fails the same way.
detail contains sentences a person can act on, and fields contains the parts of your request that were wrong, each with the path you sent it under. Both list all the failures, so a form does not have to be submitted six times to find six problems. Neither contains a stack trace or an internal message.
The distinction between conflict and unprocessable matters most. A conflict means the record moved on and your change is probably still valid: re-read, reapply, resend. Unprocessable means the content itself was refused and resending it unchanged will be refused again.
Webhooks
A school can have Kestrel post to an address of yours each time a record changes, so a sync need not poll. An administrator adds the subscription under Settings, Webhooks, naming the address, the kinds of change it wants, such as person.updated, and the token it reads with. The address has to be HTTPS and on the public internet. The signing secret is shown once, as the subscription is made.
POST https://sync.example.com/kestrel
Content-Type: application/json
Kestrel-Delivery: 3f6a…
Kestrel-Signature: sha256=9b1c…
{ "kind": "person.updated", "table": "person", "id": "6f1c…", "at": "2026-10-13T01:22:07.000Z" } The body names the kind of change, the table, the record's id and the moment, and nothing of the record itself. Fetch the record through /api/v1 with the subscription's token, so what you receive is what that token's scopes reach. A subscription delivers nothing while its token is revoked or expired.
- Kestrel-Signature
sha256=and the HMAC-SHA256 of the body, in hex. The key is the SHA-256 of the subscription's signing secret, in hex. The secret is shown once, when the subscription is made. Compare in constant time, and refuse a post whose signature does not match.- Kestrel-Delivery
- The delivery's id. Delivery is at least once, so the same change can arrive twice: drop a post whose id you have seen.
- Your answer
- Any 2xx within 10 seconds is a delivery. Anything else is a failure, a redirect included, because none is followed.
- Retries
- A failed delivery is tried again a minute later, then after twice as long each time, up to six hours between tries. After 13 attempts, a little over a day, it is given up on, and the screen shows what your server last answered.
- Order
- A subscription's deliveries are posted in the order the changes were made. While one waits to be retried, the deliveries behind it wait too.
Versioning and deprecation
The version is in the path. A breaking change to /api/v1 becomes /api/v2 and leaves what v1 answers unchanged. An endpoint can change shape between releases, so three commitments apply.
- A new field
- Adding a field to a response is not a breaking change. Write your client to ignore fields it does not know.
- A changed field
- Removing or renaming a field, or changing its type, means a new major version. Both versions run side by side for at least twelve months.
- A deprecation
- Announced on this page before it takes effect, and the announcement gives the removal date.
- The version number
- The one at the top of this page, which
/_openapi.jsonpublishes asinfo.version. Its first number is the one in the path. The second goes up when an endpoint or a field is added, which a client that ignores what it does not know never notices.
Questions about the API?
Write to api