Sending data
Send seasons, students, enrollments and account blocks from the school's systems; follow the jobs.
Paths are relative to https://<trường>.<tên-miền-nền-tảng>/integration/v1. Bodies are JSON (Content-Type: application/json), up to 5 MB.
| Method | Path | Scope | Handling |
|---|---|---|---|
PUT | /seasons/external/{externalId} | seasons:write | Immediate |
PUT | /students/{studentCode} | students:write | Immediate |
POST | /students/batch | students:write | Job (202) |
POST | /students/{studentCode}/block | students:write | Immediate |
POST | /students/{studentCode}/unblock | students:write | Immediate |
POST | /seasons/{id}/enrollments | enrollments:write | Job (202) |
GET | /jobs/{id} | any write scope | |
GET | /jobs/{id}/failures | any write scope |
Batch writes are always queued and processed in the background, so one large send cannot slow the portal down. Every write accepts ?dryRun=true.
Rules
- Nothing is deleted. The API never deletes students or seasons. Students who leave are blocked; students missing from a send are left as they are.
- Existing accounts keep how they sign in. Updates change the name, student code and
externalId; an existing account's email, password and sign-in method stay. - Every student sent is an internship student of the school.
- Every change is written to the school's Nhật ký (log), with the connection's name as the actor.
Seasons
PUT /seasons/external/{externalId} creates or updates a season from a term in the school's system. externalId is the term's id (up to 128 characters).
curl -X PUT -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Học kỳ 2 năm học 2026-2027","startDate":"2027-02-10","endDate":"2027-06-30"}' \
https://<trường>.<tên-miền-nền-tảng>/integration/v1/seasons/external/HK2-2026| Field | Required | Notes |
|---|---|---|
name | yes | 2 to 80 characters |
startDate, endDate | no | YYYY-MM-DD; endDate not before startDate |
The portal places a new season by the term's dates:
| Term | New season |
|---|---|
| Already ended | Closed (kept for history) |
| In progress, no season open | Open, closing at the end of endDate |
| In progress, another season open | Upcoming; the answer has switchNeeded |
| Not started | Upcoming |
The portal never closes the open season by itself: with switchNeeded, school staff confirm the switch under Kết nối hệ thống. Sending the same externalId again updates the name and closing date. An upcoming season opens when you send it again while its term is in progress and no other season is open (the portal does not open seasons on a schedule).
{
"result": "created",
"data": { "id": "k3v9x2m1q8w7e6r5t4y3u2i1", "externalId": "HK2-2026", "name": "Học kỳ 2 năm học 2026-2027", "status": "upcoming", "openedAt": null, "closesAt": "2027-06-30T16:59:59.000Z", "closedAt": null, "updatedAt": "2026-11-13T02:00:00.000Z" },
"switchNeeded": { "from": "Đợt thực tập học kỳ 1 năm học 2026-2027", "to": "Học kỳ 2 năm học 2026-2027" }
}result: created, opened, updated, unchanged. 201 when created, 200 when updated.
One student
PUT /students/{studentCode} creates or updates one student (no season change).
| Field | Required | Notes |
|---|---|---|
name | yes | Full name, up to 120 characters |
email | without password | The student's Google account; up to 254 characters |
password | no | 8 to 128 characters. With a password the student signs in with a login name |
username | no | Login name (3 to 64 letters, digits, ., -, _), only with password. Defaults to the student code |
externalId | no | The student's id in the school's system, up to 128 characters |
curl -X PUT -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
-d '{"name":"Nguyễn Văn An","email":"[email protected]","externalId":"U-1001"}' \
https://<trường>.<tên-miền-nền-tảng>/integration/v1/students/SV2026001{ "result": "created", "data": { "id": "p0o9i8u7y6t5r4e3w2q1a2s3", "studentCode": "SV2026001", "externalId": "U-1001", "name": "Nguyễn Văn An", "email": "[email protected]", "signIn": "google", "loginName": null, "status": "active", "createdAt": "2026-11-13T02:00:00.000Z", "updatedAt": "2026-11-13T02:00:00.000Z" } }result: created (201), updated, unchanged. The portal finds the student by externalId, then student code, then email. It returns 409 when the data points to two different accounts, for example an email that belongs to a student with another code, or a student code already used as someone's login name.
Many students
POST /students/batch takes up to 500 students per request, with the fields above plus studentCode. Add season (a season id) to also admit them to that season.
curl -X POST -H "Authorization: Bearer ACCESS_TOKEN" -H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a0e-6d55-4a8e-9a57-0f3b8e7d2c11" \
-d '{"season":"k3v9x2m1q8w7e6r5t4y3u2i1","students":[{"studentCode":"SV2026001","name":"Nguyễn Văn An","email":"[email protected]"},{"studentCode":"SV2026002","name":"Trần Thị Bình","password":"MatKhau@2026"}]}' \
https://<trường>.<tên-miền-nền-tảng>/integration/v1/students/batchThe whole batch is checked first: any invalid row (missing field, duplicate code in the batch) returns 422 with an errors list and nothing is queued. Otherwise the answer is 202 Accepted with a Location header pointing at the job:
{ "job": { "id": "at8w3w2oxfpggp9ng0w5a3de", "kind": "students", "status": "queued", "dryRun": false, "total": 2, "processed": 0, "counts": {}, "warnings": {}, "failureCount": 0, "createdAt": "2026-11-13T02:00:00.000Z", "startedAt": null, "finishedAt": null, "by": "Kết nối “Phần mềm đào tạo”" } }Admitting students to a season
POST /seasons/{id}/enrollments takes up to 500 existing students, each { "studentCode": "..." } or { "externalId": "..." }. A closed season returns 409. The answer is a job (202), as above.
Blocking and unblocking
POST /students/{studentCode}/block blocks the account, exactly as when the school blocks it on screen: the student can no longer sign in, applications waiting for a decision or an answer are withdrawn and the companies are told. POST /students/{studentCode}/unblock reopens it. No body; the answer is the student after the change.
Following a job
GET /jobs/{id} returns the job's state (only jobs sent by the same connection):
status | Meaning |
|---|---|
queued | Waiting |
running | In progress; processed / total shows how far |
succeeded | Done (some rows may have failed, see failureCount) |
failed | Could not run |
cancelled | Cancelled by the school |
counts tallies outcomes, such as students.created, students.updated, students.blocked, enrollments.enrolled, seasons.created, failed. Poll every 2 to 5 seconds.
GET /jobs/{id}/failures returns failed rows as CSV (ref,code,message). Failed rows are kept for 30 days.