Goroka Grammar SchoolEnrol now

Read school information

Public information summarises contacts, sections, required documents and dated fees. Read the linked documents for full conditions.

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 is the source publication date, or null when unknown. links.calendar.applies states the calendar year. Fields ending in Kina are exact two-decimal PGK strings. Fields ending in Toea are whole toea.

Fee sources by year

  • Check fees[].year and sourceUrls before you use an amount.
  • Check links.calendar.applies before you use a date.
  • 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 reads the live public API. It uses Grade 1. Read requiredWhen and uncertainties before you act.

const baseUrl = 'https://gorokagrammarschool.com';
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,
  },
  fees: { year: number, tuition: { grades: string[], annualKina: 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;
const fee = school.fees.find(entry => entry.year === 2026);
if (!fee) throw new Error('Fee year unavailable.');
const annualKina = fee.tuition.find(entry => entry.grades.includes('grade-1'))?.annualKina;
if (!annualKina) throw new Error('Grade fee unavailable.');

Send one complete enrolment

Your agent obtains an intake key automatically. For 2027, the school emails one review link and private code to the parent. The parent approves the saved packet on the website.

  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. For 2027, open the school review link and accept the current terms before submission.
  6. Wait for school correspondence after intake review.
  7. Ask your agent to check review and correspondence status with the saved permit.

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 or guardianship. The school checks authority during review.

A 2027 receipt first says awaiting_parent. The school sends the parent a link and private code. The application becomes submitted only after parent review and acceptance. Status then shows intake review and correspondence. Clear does not mean admission. Email can be delayed or fail.

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 files only with a disposable local server. Set GGS_API_BASE_URL before running this example.

import { readFile, stat } from 'node:fs/promises';
import { validateApplication } from './sdk.mjs';
const baseUrl = process.env.GGS_API_BASE_URL;
if (!baseUrl) throw new Error('Set GGS_API_BASE_URL to your local test origin.');
/** @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: '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.
  • Set GGS_API_BASE_URL to https://gorokagrammarschool.com for real family packets.
  • 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.
  • For 2027, add connected selected services and one complete request for each selected service.
  • Include regular boarding for boarders and separate media choices for Early Learning.
  • 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.
  • Use getStatus to read parent approval, review and correspondence with the saved permit.
  • Contact the school after receipt recovery expires.

New creation expires after 24 hours. Receipt recovery lasts seven days. Status reads last 90 days after packet receipt or 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 baseUrl = process.env.GGS_API_BASE_URL;
if (!baseUrl) throw new Error('Set GGS_API_BASE_URL before submission.');
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.
Check review and correspondence

Use the saved permit. An awaiting_parent receipt is a draft. Provider acceptance does not prove inbox delivery. A budget state means an email cap delayed that message.

import { getStatus } from './sdk.mjs';
const status = await getStatus(saved);
if (status.review === 'awaiting_parent') {
  // The parent must use the school email to review and approve.
}
if (status.approvalInvitation?.delivery === 'budget'
    || status.correspondence.parent.eligibility === 'budget') {
  // Tell the parent that school email is delayed.
}
if (status.review === 'clear') {
  // Intake screening is clear; admission remains undecided.
}

Correct errors and pace retries

Errors use error.code and error.message. Field errors also provide error.fields. The origin check returns the same JSON error shape.

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.
status_expired410Status access ended. Contact the school for an update.
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.
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.
origin_rejected403Send Origin equal to the API origin for multipart requests.
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.

Shared request guards

Request guards use the shared database. All processes count requests in the same UTC-minute window. A restart does not reset them.

ControlLimitScope
Intake HTTP requests120 requestsPer client address per UTC minute.
Submission and receipt requests60 requestsPer permit per UTC minute.
Validation requests10 requestsPer client address per UTC minute, 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 the parent review invitation or later correspondence. Check status with the saved permit. A recorded receipt does not prove email delivery.

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. Set GGS_API_BASE_URL to the local server origin for invented packet examples.
  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.