API & integration

Printing on the school's letterhead (report templates)

A template is a letterhead, not a document. The school owns the frame — its logo, header, footer, watermark, margins, paper size and orientation — arranged once in Okta's report builder. Your app supplies the body; a template carries no content of its own.

Why not print it yourself: every app that prints anything meets the same wall — Arabic shaping, RTL, a font that renders it, page margins, and a header the school recognises as its own. Solved separately in each app it is solved differently in each app, and the school ends up holding six documents in six layouts, none of them its letterhead.

Scope: reports.builder.read — the same scope as the report catalog, and all most apps need. Authoring one is a separate act behind a separate grant, reports.builder.write; see Proposing a letterhead below.

1. List the letterheads

GET /api/apps/reports/templates

Each entry carries id, names, paper/orientation, and variables — the fields that particular template asks for at print time. Read them: a header reading رقم الخطاب: {{letter_no}} prints a blank there if you send nothing, and the document will not tell you why. required is your cue to ask a person rather than guess.

id is an opaque string — pass it back exactly as received; it is not a ULID, unlike other ids on this surface. Only APPROVED templates are listed: a school mid-redesign has a draft that must not reach a parent, and your app cannot tell a finished design from an unfinished one by looking at it.

2. Print

POST /api/apps/reports/templates/{id}/render
{
  "blocks": [
    { "type": "heading",  "text": "كشف الدرجات", "level": "h1" },
    { "type": "info-row", "label": "الطالب", "value": "أحمد صالح" },
    { "type": "table", "headers": ["المادة", "الدرجة"], "rows": [["الرياضيات", "95"]] }
  ],
  "variables": { "letter_no": "1447-42" }
}

The response is the PDF itself (application/pdf), not a link: a stored file needs a lifetime, a cleanup policy and an access rule, for a document that may carry a named child's marks.

Block types: heading (text/html, level, align), text (html), info-row (label, value), table (headers, rows, striped), divider, spacer, image (path on Okta's public disk). An unknown type is refused, not skipped — a silently dropped block yields a document short by a whole section that says nothing about it. You get 422 naming the block and its type.

Variables: an undeclared key is dropped rather than refused (unlike a block type — a stray key costs the page nothing), platform variables ({{tenant.name}}, {{date.today}}, {{page.number}}) cannot be overwritten, and anything you omit falls back to the school's default.

Approval is re-checked at render, not only at listing: a stored id outlives the listing that handed it out. If you cache a template id, treat 422 on render as ordinary — re-list and let the user choose.

Embedded apps call the services in-process instead, with no HTTP and no token:

$templates = app(\App\Services\PartnerApi\Reports\Templates\ListReportTemplates::class)();
$pdf = app(\App\Services\PartnerApi\Reports\Templates\RenderReportTemplate::class)(
    $templates[0]->id,
    [['type' => 'heading', 'text' => 'كشف الدرجات', 'level' => 'h1']],
    ['letter_no' => '1447-42'],
);
// ['filename' => ..., 'mime' => 'application/pdf', 'bytes' => ...]

Proposing a letterhead

All of the above assumes the school already arranged a letterhead that suits your document. Often it has not: a marks app needs a sheet with the term in the header and two signatories at the foot, and the school does not know that is what it needs until it sees one.

Scope: reports.builder.write — deliberately separate, so the school is asked a second question at install rather than every app holding .read quietly gaining the ability to put drafts in front of them.

Four things the grant does not buy, and none of them bends:

  1. It arrives as a draft and does not print. A template you just authored is absent from GET /reports/templates and POST .../render answers 422 for it. Not an error to work around: everything printed on a letterhead goes out in the school's name, and an app that could author and print in one step could issue documents nobody at the school ever saw.
  2. Only what you authored. The school's own letterhead, and one authored by another app on the same school, both answer 422 on update or delete. (Tenant isolation says nothing here — both apps are legitimately installed.)
  3. An edit revokes approval. Editing a template the school approved sends it back to draft and it stops printing until approved again. That is the cost and it is the point: an approval that survived an edit would let an app get one innocuous design signed off and then write anything into it.
  4. An approved one is no longer yours to delete. Approval is the school taking the design as its own — it may be on a term's paperwork. Deletion answers 422; the school removes it from its own builder, where the consequence is visible to the person choosing it.
POST   /api/apps/reports/templates        → 201, the template as the listing describes it
PATCH  /api/apps/reports/templates/{id}
DELETE /api/apps/reports/templates/{id}
{
  "name_ar": "كشف درجات",
  "name_en": "Marks sheet",
  "header_center_content": "كشف درجات — الفصل {{term}}",
  "signatures_enabled": true,
  "signatures": [
    { "title_ar": "معلم المادة" },
    { "title_ar": "مدير المدرسة" }
  ],
  "variables": [
    { "key": "term", "label_ar": "الفصل الدراسي", "required": true }
  ]
}

Writable fields are an allow-list: identity (name_ar required, name_en, description, type), paper (orientation, paper_size), header_enabled/footer_enabled and the header_*_content / footer_*_content slots (right/center/left), watermark_text and watermark_opacity, the four margin_*_mm, the signature block (signatures_enabled, signatures_columns, signatures_title, signatures[]), and variables[]. Anything absent is not writable — notably header_logo and background_image (a school's mark on its own paper, and the file would arrive from outside the school entirely) and the approval state (writable, an app would approve its own draft and every guarantee above would be ornamental).

The logo and entity name are inherited, copied from a letterhead the school already approved — a proposal that arrived blank-headed would read as a broken app rather than a draft awaiting their logo.

On PATCH every field is optional and omission means "leave it alone", never "clear it", so an app that forgets a key does not blank a school's footer. The exception is variables and signatures, where an explicit empty array means "remove them all".

A custom variable key that shadows a platform one (tenant.name) is dropped silently — the same rule the school's own editor goes through, so it cannot drift between the two ways in.

25 templates per installation. An app proposes a handful of documents it knows how to fill; a buggy loop proposes thousands, and the school finds out as a builder page it can no longer read. Over the cap answers 422.

Uninstalling deletes nothing. Templates you proposed stay with the school — reviewed, approved, printed on; they are the school's documents now, whoever drafted them. They only lose their attribution. So do not rely on a reinstall restoring your authorship: a new installation owns nothing a previous one authored.

Propose once at first run, not on every screen open, or the school's board fills with identical drafts awaiting an approval that will not come. List first and propose only when nothing fits — a letterhead the school arranged by hand always wins. And do not poll for approval: nothing on this surface reports when it happens, the decision can take days, and listing at the moment the user needs a template shows only the approved ones anyway.

Embedded apps use CreateReportTemplate, UpdateReportTemplate and DeleteReportTemplate in the same namespace.


View the whole guide on one page · ↧ Raw Markdown