Skip to Content
Portal API (v1)

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.

MethodPathScopeHandling
PUT/seasons/external/{externalId}seasons:writeImmediate
PUT/students/{studentCode}students:writeImmediate
POST/students/batchstudents:writeJob (202)
POST/students/{studentCode}/blockstudents:writeImmediate
POST/students/{studentCode}/unblockstudents:writeImmediate
POST/seasons/{id}/enrollmentsenrollments:writeJob (202)
GET/jobs/{id}any write scope
GET/jobs/{id}/failuresany 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
FieldRequiredNotes
nameyes2 to 80 characters
startDate, endDatenoYYYY-MM-DD; endDate not before startDate

The portal places a new season by the term's dates:

TermNew season
Already endedClosed (kept for history)
In progress, no season openOpen, closing at the end of endDate
In progress, another season openUpcoming; the answer has switchNeeded
Not startedUpcoming

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).

FieldRequiredNotes
nameyesFull name, up to 120 characters
emailwithout passwordThe student's Google account; up to 254 characters
passwordno8 to 128 characters. With a password the student signs in with a login name
usernamenoLogin name (3 to 64 letters, digits, ., -, _), only with password. Defaults to the student code
externalIdnoThe 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/batch

The 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):

statusMeaning
queuedWaiting
runningIn progress; processed / total shows how far
succeededDone (some rows may have failed, see failureCount)
failedCould not run
cancelledCancelled 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.

On this page