Goroka Grammar SchoolEnrol now

Read school information

Public information includes contacts, sections, required documents and dated fees. Each subject links to its source page.

updatedAt records the editorial update date of public information. It does not establish school verification or fee publication.

fees[].year states the applicable year. fees[].publishedAt gives the publication date stated by the source. null means this date is unknown.

  • Check the fee year before you use an amount.
  • Read each uncertainty before you act.
  • Use the source page for the full conditions.
  • Join each grade document type to documentTypes[].id.
  • Use label for its name and hint for its description.
  • Read requiredWhen with the grade-specific required flag.
  • Use admissions.uploads for supported formats and size limits.
  • Read named links through their url field.
  • Read links.documents as a list of document links.
Join document metadata and follow links

This example uses Grade 1. Conditional requirements remain in requiredWhen and uncertainties.

const baseUrl = 'http://127.0.0.1:4399';
const response = await fetch(`${baseUrl}/api/v1/school.json`);
if (!response.ok) throw new Error('School information unavailable.');
/** @type {{
  admissions: {
    grades: { id: string, documents: { type: string, required: boolean }[] }[],
    documentTypes: { id: string, label: string, hint: string, requiredWhen: string }[],
    sourceUrl: string,
  },
  links: { calendar: { url: string }, documents: { url: string }[] },
}} */
const school = await response.json();
const grade = school.admissions.grades.find(entry => entry.id === 'grade-1');
if (!grade) throw new Error('Grade metadata unavailable.');
const documents = grade.documents.map(document => {
  const type = school.admissions.documentTypes.find(type => type.id === document.type);
  if (!type) throw new Error('Document metadata unavailable.');
  return { ...type, required: document.required };
});
const calendarUrl = school.links.calendar.url;
const documentUrls = school.links.documents.map(document => document.url);
const sourceUrl = school.admissions.sourceUrl;

Send one complete enrolment

Your agent obtains an intake key automatically. Basic enrolment needs no school login or browser approval.

  1. Prepare the child, parent or guardian details and all required files.
  2. Check the complete packet before your agent sends it.
  3. Ask your agent to save its permit and retry key privately.
  4. Keep the application reference after the school records the packet.
  5. Wait for school correspondence after its intake review.

One permit receives one new application. It cannot read or edit existing applications, download documents, take assessments or make school decisions.

The packet records the caller’s assertion that a parent or guardian instructed submission. This assertion does not prove parent approval.

The school records the reference before intake review. Uncertain or unavailable review holds school correspondence for manual review.

Check a packet before submission

Validation is optional. validateApplication makes one anonymous request without a permit. submitApplication does not run validation automatically.

  • Use POST /api/v1/intake/validate for JSON descriptors or multipart files.
  • Send the API origin in the Origin header for multipart requests.
  • Read fileContentsChecked before you interpret a successful result.
  • Read error.fields for invalid or incomplete packets.
  • Match documents.<type> errors to the corresponding document field.
  • Correct the packet before you request an intake permit.

JSON descriptors check fields, document rules and declared sizes. The complete JSON validation request must fit within 32 KiB. fileContentsChecked is false.

Multipart validation also checks actual byte counts and supported file signatures. fileContentsChecked is true. It does not establish document authenticity.

HTTP 200 returns valid: true. A failed check returns the standard error object. Validation stores no files and creates no application or email.

Validation shares the two packet slots with submission. At most one validation can run. The request limits below apply.

Validation success reserves no capacity and does not guarantee later submission.

An invented manifest

These details belong to an invented family. Use them only with a disposable local database and captured mail.

/** @type {import('./sdk.mjs').IntakeManifest} */
const manifest = {
  student: {
    givenNames: 'Example', familyName: 'Family',
    dateOfBirth: '2017-05-10', gender: 'female',
    province: 'Eastern Highlands', grade: 'grade-1',
    entryYear: 2026, entryTerm: '1', attendance: 'day',
  },
  guardian: {
    name: 'Example Parent', relationship: 'mother',
    phone: '7123 4567', email: 'parent@example.invalid',
    address: 'Invented local-test address',
  },
  guardianAuthorityAsserted: true,
};
Validate metadata or files with the SDK

Use complete, invented document fixtures. The SDK sends file bytes only when you pass an IntakePacket.

import { readFile, stat } from 'node:fs/promises';
import { validateApplication } from './sdk.mjs';
const baseUrl = 'http://127.0.0.1:4399';
/** @type {readonly { type: import('./sdk.mjs').DocumentType, fileName: string, format: import('./sdk.mjs').DocumentFormat }[]} */
const documentFiles = [
  { type: 'birth-certificate', fileName: 'birth-certificate.pdf', format: 'pdf' },
  { type: 'photo', fileName: 'photo.png', format: 'png' },
  { type: 'fee-receipt', fileName: 'fee-receipt.pdf', format: 'pdf' },
  { type: 'school-report', fileName: 'school-report.pdf', format: 'pdf' },
];
const documents = await Promise.all(documentFiles.map(async document => ({
  ...document, bytes: (await stat(`./fixtures/${document.fileName}`)).size,
})));
const metadataCheck = await validateApplication(
  { manifest, documents }, { baseUrl },
); // fileContentsChecked: false
const files = await Promise.all(documents.map(async document => ({
  type: document.type,
  fileName: document.fileName,
  file: new Blob([await readFile(`./fixtures/${document.fileName}`)], {
    type: document.format === 'png' ? 'image/png' : 'application/pdf',
  }),
})));
const packet = { ...manifest, documents: files };
const fileCheck = await validateApplication(packet, { baseUrl });
// fileContentsChecked: true
Validate multipart data with HTTP

The manifest field contains JSON text. File fields use document type identifiers, such as birth-certificate.

const form = new FormData();
form.set('manifest', JSON.stringify(manifest));
for (const document of packet.documents) {
  form.set(document.type, document.file, document.fileName);
}
const response = await fetch(`${baseUrl}/api/v1/intake/validate`, {
  method: 'POST', headers: { Origin: new URL(baseUrl).origin }, body: form,
});
const result = await response.json();
if (!response.ok) {
  // Read result.error.code and result.error.fields.
  // A field can be documents.school-report.
}

Connect an agent

Use ordinary HTTP or the small JavaScript SDK. The normal SDK path makes two API requests: permit, then complete packet.

  • Download the SDK and TypeScript declarations.
  • Use validateApplication only when you want a separate check.
  • Use submitApplication with a persistAttempt callback.
  • Finish saving the attempt before the callback returns.
  • Send one JSON manifest and the required files as multipart data.
  • Send the API origin in the Origin header for multipart requests.
  • Set guardianAuthorityAsserted only when a parent or guardian instructed the submission.
  • Keep permits and retry keys outside URLs and logs.
  • Use the same permit, key and packet with resumeAttempt.
  • Use getReceipt to recover the reference without uploading files again.
  • Keep the saved attempt after a lost response or uncertain error.
  • Never request a replacement permit after an uncertain packet outcome.
  • Contact the school after receipt recovery expires.

New creation expires after 24 hours. Receipt-only recovery lasts seven days after submission, using the same permit.

The manifest limit is 32 KiB. Each file can use 10 MiB. Seven files can use 28 MiB in total.

The complete multipart request must fit within 30 MiB. Each document type permits one file.

Save an attempt before submission

This local example saves the secret before the packet request. Keep the private directory until the receipt is recovered.

import { mkdtemp, writeFile } from 'node:fs/promises';
import { tmpdir } from 'node:os';
import { join } from 'node:path';
import { submitApplication } from './sdk.mjs';
const privateDirectory = await mkdtemp(join(tmpdir(), 'ggs-attempt-'));
const attemptFile = join(privateDirectory, 'attempt.json');
const receipt = await submitApplication(packet, {
  baseUrl,
  persistAttempt: async attempt => {
    await writeFile(attemptFile, JSON.stringify(attempt), {
      mode: 0o600, flag: 'wx',
    });
  },
});
Recover an uncertain attempt

Recover a receipt first. Retry receipt_not_found only while creation permission remains valid. Keep the same packet and saved attempt.

import { readFile } from 'node:fs/promises';
import { getReceipt, IntakeSdkError, resumeAttempt } from './sdk.mjs';
const saved = JSON.parse(await readFile(attemptFile, 'utf8'));
let receipt;
try {
  receipt = await getReceipt(saved);
} catch (error) {
  if (!(error instanceof IntakeSdkError)
      || error.code !== 'receipt_not_found') throw error;
  if (Date.now() >= Date.parse(saved.expiresAt)) {
    throw new Error('Creation expired. Keep the saved attempt and contact the school.');
  }
  receipt = await resumeAttempt(saved, packet);
}
// Keep permits and packet details outside logs.

Correct errors and pace retries

Errors use error.code and error.message. Field errors also provide error.fields. An origin-check rejection can return plain text.

Retry-After gives seconds before another attempt. It controls retry pacing. It does not guarantee that an hourly or daily quota has reset.

  • Use error.fields to identify the details or documents that need correction.
  • Wait for Retry-After after rate_limited, in_progress or server_busy.
  • Keep the saved attempt after an uncertain submission.
  • Recover its receipt before any new submission.
  • Never obtain a replacement permit for an uncertain submission.
CodeHTTP statusAction
invalid_input400 or 422Correct error.fields when supplied. Send the documented request shape.
incomplete422Add the missing details or documents identified by error.fields.
idempotency_key_required400Send the saved Idempotency-Key for this attempt.
invalid_permit401Check the saved permit. Recover any recorded receipt before another submission.
revoked_permit401Stop using this permit. Contact the school about the submission.
expired_permit401Creation permission expired. Recover any recorded receipt with the saved permit.
receipt_expired410Contact the school before submitting again.
permit_consumed409Recover this permit’s receipt. It cannot receive another application.
idempotency_conflict409Restore the original permit, retry key and unchanged packet.
in_progress409Wait for Retry-After. Recover the receipt with the same saved attempt.
connected_review_required409Complete the connected family application on the website.
input_too_large413Reduce the request, manifest or files within their applicable limits.
unsupported_media_type415Send application/json or multipart/form-data as documented for the operation.
rate_limited429Wait for Retry-After. The relevant hourly or daily allowance can remain exhausted.
server_busy503Wait for Retry-After. Retain any saved submission attempt.
api_inactive503The API is suspended. Retain any saved attempt and contact the school.
internal_error500Submission can be uncertain. Retain the saved attempt and recover its receipt.
receipt_not_found404An earlier request can still finish. Retry the unchanged attempt only before creation permission expires.
validation_failed500Validation did not finish. Retry validation when the service recovers.
Correct a document field error

This example omits the invented photo descriptor. It restores that descriptor after reading documents.photo.

import { IntakeSdkError, validateApplication } from './sdk.mjs';
const missingPhoto = documents.filter(document => document.type !== 'photo');
try {
  await validateApplication({ manifest, documents: missingPhoto }, { baseUrl });
} catch (error) {
  if (!(error instanceof IntakeSdkError)) throw error;
  const photoIssues = error.fields?.['documents.photo'] ?? [];
  if (error.code !== 'incomplete' || photoIssues.length === 0) throw error;
  const corrected = { manifest, documents };
  await validateApplication(corrected, { baseUrl });
}

Read the scope of each limit

These are proposed technical pilot settings. They are not adopted school policy. School approval, payment and document authenticity remain separate.

Request guards

Request guards are process-local sliding windows. Each process counts requests separately. These guards reset when that process restarts.

ControlLimitScope
Intake HTTP requests120 requestsPer client address per rolling minute.
Submission and receipt requests60 requestsPer permit per rolling minute.
Validation requests10 requestsPer client address per rolling minute, within the shared HTTP allowance.
Permit requests30 requestsPer client address per rolling hour, within the shared HTTP allowance.

Durable intake budgets

Durable budgets apply across processes and restarts. UTC-day budgets count from 00:00 UTC. The per-address permit budget uses a rolling hour.

ControlLimitScope
Issued permits30 permitsPer client address per rolling hour.
Issued permits300 permitsGlobally per UTC day.
New applications100 applicationsGlobally per UTC day. A matching receipt replay uses no additional application allowance.
Attempted staged files1 GiBGlobally per UTC day. Attempted bytes count even when writes become uncertain.
Intake correspondence100 emailsPer audience per UTC day. The parent and office audiences count separately.
Parent receipts6 emailsPer recipient per UTC day.

Email limits postpone correspondence. A recorded receipt does not prove email delivery. Submission and school review remain separate.

Test locally with invented details

Use GGS_RUNTIME_MODE=local with a temporary data directory. Local mode uses private local storage and captures mail without sending it.

  1. Remove external database, storage and mail credentials from the test environment.
  2. Use a new temporary directory for GGS_DATA_DIR.
  3. Run the explicit migrate command before you start the local server.
  4. Use the local server origin in each example.
  5. Download the SDK beside your local example file.
  6. Set entryYear to a year currently accepted by the validator.
  7. Use complete invented PDF or photo fixtures.
  8. Inspect captured mail in the temporary local outbox.
  9. Remove your temporary data and private attempts after verification.

Keep invented tests away from staging and production. Never place real family details, files, permits or saved attempts in logs.