K12 Implementation Guide
This guide walks through integrating with the K12 Safety API and its three workflow families: EOP Editing, QRG Generation, and QRG Editing.
Authentication
The K12 API requires two authentication mechanisms on every request.
OAuth 2.0 Client Credentials
Obtain an access token from Azure Entra ID:
TOKEN=$(curl -s -X POST \
"https://login.microsoftonline.com/ef3e1dbf-4cc9-4a06-bc23-71dd7a46fd1b/oauth2/v2.0/token" \
-d "client_id={CLIENT_ID}" \
-d "client_secret={CLIENT_SECRET}" \
-d "scope=api://dev-k12-aitools-t27p-api/.default" \
-d "grant_type=client_credentials" \
| jq -r '.access_token')
Required Headers
Every API call must include:
| Header | Value |
|---|---|
Authorization | Bearer {access_token} |
Ocp-Apim-Subscription-Key | {subscription_key} |
Content-Type | application/json |
Environment URLs
| Environment | Base URL | OAuth Scope |
|---|---|---|
| Development | https://dev-k12-aitools-t27p-apim.azure-api.net | api://dev-k12-aitools-t27p-api/.default |
Replace {CLIENT_ID} and {CLIENT_SECRET} with your credentials. These will be provided separately by your administrator.
Endpoint Patterns
Legacy Tool Execute (Current)
All K12 tools are available through the tool execution endpoint:
POST {BASE_URL}/ai/api/tools/{tool-id}/execute
Request bodies wrap tool-specific inputs in a data envelope:
{
"data": {
"field1": "value1",
"field2": "value2"
}
}
Domain Routes (Alternative)
The API also exposes domain-scoped routes with cleaner URLs and flat request bodies (no data wrapper):
POST {BASE_URL}/ai/k12/tools/{slug} -- Tool execution (direct JSON)
POST {BASE_URL}/ai/k12/workflows/{slug} -- Synchronous workflow
POST {BASE_URL}/ai/k12/workflows/{slug}/start-async -- Async workflow (returns runId)
GET {BASE_URL}/ai/k12/workflows/{slug}/runs/{runId} -- Poll workflow run status
Legacy (/ai/api/tools/.../execute) | Domain (/ai/k12/tools/...) | |
|---|---|---|
| Request body | { "data": { ... } } | { ... } (flat JSON) |
| Validation errors | 400 | 422 |
| URL style | Generic tool ID | Descriptive domain path |
Both patterns reach the same underlying tools. The examples in this guide use the legacy endpoint. To use domain routes, drop the data wrapper and replace the tool slug (e.g., k12-qrg-generation-kickoff becomes /ai/k12/tools/qrg-generation-kickoff).
EOP Editing
EOP (Emergency Operation Plan) editing enables AI-assisted revision of functional annex sections.
Workflow
Kickoff
curl -X POST "${BASE_URL}/ai/api/tools/k12-functional-annex-kickoff/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"emergencyOperationPlan": {"id": "d4e5f6a7-b8c9-0123-d456-e78901234567", "organizationId": "c3d4e5f6-a7b8-9012-cdef-345678901234"},
"userId": "user-456",
"roleId": "safety-coordinator",
"form": {
"name": "Fire Safety Annex",
"field": {
"group": "Response Procedures",
"name": "evacuation_steps",
"content": "<p>Current evacuation procedures.</p>"
}
},
"prompt": "Add assembly point verification step after building clearance"
}
}'
Kickoff Fields
| Field | Required | Description |
|---|---|---|
emergencyOperationPlanId | Conditional | EOP identifier. Organization context still requires emergencyOperationPlan.organizationId |
emergencyOperationPlan | Conditional | Frontend-provided EOP object. Its id, emergencyOperationPlanId, planId, or eopId is normalized as the EOP identifier; organizationId is used directly for district/organization details |
emergencyOperationPlanConfigurationId | Conditional | Pre-EOP configuration ID. Organization context still requires emergencyOperationPlanConfiguration.organizationId; mutually exclusive with plan and Master Copy identifiers |
emergencyOperationPlanConfiguration | Conditional | Frontend-provided configuration object. Its ID aliases are normalized as the configuration identifier; organizationId is used directly for district/organization details |
eopMasterCopyId | Conditional | Master Copy identifier for pre-EOP template form editing. Organization context still requires emergencyOperationPlanMasterCopy.organizationId |
emergencyOperationPlanMasterCopy | Conditional | Frontend-provided Master Copy object. Saved objects may provide an ID alias; organizationId is used directly for district/organization details |
userId | Yes | Requesting user identifier |
roleId | No | Optional role identifier retained for traceability |
userTagIds | No | Alias for selected role/tag IDs. May be an empty array while frontend tag filling is pending |
roles, userTags, generalUserTags | No | Frontend-provided role/tag objects or names. These override backend-resolved role tags for prompt context when present |
form.field.content | Yes | Current annex content (HTML). Empty string for prompt-only edits |
prompt | No | Instructions describing the desired changes |
sessionId | No | Correlation ID (auto-generated if omitted) |
waitForResult | No | true to block until complete (default: false) |
Poll Status
curl -X POST "${BASE_URL}/ai/api/tools/k12-annex-editing-status/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestIds": ["req-7f3a..."]}}'
Fetch Result
curl -X POST "${BASE_URL}/ai/api/tools/k12-annex-editing-result/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestId": "req-7f3a..."}}'
Result Fields
| Field | Description |
|---|---|
requestId | Tracking ID |
sessionId | Session this job belongs to |
status | success or error |
message | Edited content (HTML) or error description |
isEdit | Whether the content was modified |
Cancel
curl -X POST "${BASE_URL}/ai/api/tools/k12-cancel-task-request/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestId": "req-7f3a..."}}'
QRG Generation
QRG (Quick Reference Guide) generation transforms an entire EOP into role-specific action guides with BEFORE, DURING, and AFTER phases.
Pipeline
For each annex in the EOP, the system runs four AI steps:
Kickoff
curl -X POST "${BASE_URL}/ai/api/tools/k12-qrg-generation-kickoff/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"emergencyOperationPlanId": "d4e5f6a7-b8c9-0123-d456-e78901234567",
"generalUserTagIds": ["a1b2c3d4-e5f6-7890-abcd-100000000001"],
"sessionId": null,
"waitForResult": false
}
}'
Kickoff Fields
| Field | Type | Required | Description |
|---|---|---|---|
emergencyOperationPlanId | string (UUID) | Yes | EOP to generate QRGs from |
generalUserTagIds | array of UUID | No | Filter to specific role tags (all roles used if omitted) |
sessionId | string | No | Correlation ID (auto-generated if null) |
waitForResult | boolean | No | Block until complete (default: false) |
Poll and Fetch
# Poll status
curl -X POST "${BASE_URL}/ai/api/tools/k12-qrg-generation-status/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestId": "a1b2c3d4-..."}}'
# Fetch result (once status is SUCCESS)
curl -X POST "${BASE_URL}/ai/api/tools/k12-qrg-generation-result/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestId": "a1b2c3d4-..."}}'
Result Structure
Each generated QRG contains:
{
"name": "Fire Safety - Teacher",
"organizationId": "c3d4e5f6-...",
"type": "EMERGENCY",
"hazard": "FIRE",
"generalUserTagManagementType": "ASSIGN_SELECTED",
"generalUserTagIds": ["a1b2c3d4-..."],
"contentBlocks": [
{ "type": "BEFORE", "content": "<ul><li>Know your evacuation routes</li>...</ul>" },
{ "type": "DURING", "content": "<ul><li>Alert students calmly</li>...</ul>" },
{ "type": "AFTER", "content": "<ul><li>Account for all students</li>...</ul>" }
]
}
The result also includes aiStats with per-step success/error counts for diagnostics.
Role Batching
When an EOP has a large number of role tags (more than 12), the system automatically splits them into batches and runs the role mapping step for each batch in parallel. Results are merged with deduplication, so the output is always a single unified mapping. This is transparent to the caller -- no changes are needed in the kickoff request.
Expected Timelines
| EOP Size | Annex Count | Duration |
|---|---|---|
| Small | 1-3 | 30s - 1 min |
| Medium | 4-8 | 1-3 min |
| Large | 10+ | 3-5 min |
QRG Generation returns UPPERCASE status values (SUCCESS, ERROR, PROCESSING). This differs from EOP Editing and QRG Editing which use lowercase. Your polling logic must handle both conventions.
QRG Editing
QRG editing modifies a single section (BEFORE, DURING, or AFTER) of an existing Quick Reference Guide.
Kickoff
curl -X POST "${BASE_URL}/ai/api/tools/k12-qrg-editing-kickoff/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{
"data": {
"userId": "user-456",
"roleId": "a1b2c3d4-e5f6-7890-abcd-100000000001",
"section": "DURING",
"content": "<ul><li>Alert students calmly and begin evacuation</li></ul>",
"prompt": "Add a step about checking classroom headcount before leaving",
"sessionId": null,
"quickReferenceGuide": {
"name": "Fire Safety - Teacher",
"type": "EMERGENCY",
"instructionFor": "FIRE",
"generalUserTagManagementType": "ASSIGN_SELECTED",
"generalUserTags": [
{ "label": "Teacher", "value": "a1b2c3d4-e5f6-7890-abcd-100000000001" }
],
"organization": {
"id": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"name": "Lincoln Elementary",
"type": "FACILITY",
"parent": {
"id": "d4e5f6a7-b8c9-0123-d456-e78901234567",
"name": "Springfield School District",
"type": "DISTRICT"
}
}
}
}
}'
Kickoff Fields
| Field | Type | Required | Description |
|---|---|---|---|
userId | string (UUID) | Yes | Requesting user |
roleId | string (UUID) | Yes | Role performing the edit |
section | string | Yes | BEFORE, DURING, or AFTER |
content | string | Yes | Current section HTML content |
prompt | string | Yes | Edit instructions (min 1 character) |
sessionId | string | No | Correlation ID (pass null for new session) |
quickReferenceGuide | object | Yes | QRG metadata (name, type, instructionFor, tags, organization) |
Poll and Fetch
# Poll status
curl -X POST "${BASE_URL}/ai/api/tools/k12-qrg-editing-status/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestIds": ["req-abc..."]}}'
# Fetch result (once status is success)
curl -X POST "${BASE_URL}/ai/api/tools/k12-qrg-editing-result/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestId": "req-abc..."}}'
Result Fields
| Field | Description |
|---|---|
requestId | Tracking ID |
sessionId | Session this edit belongs to |
status | success or error |
message | Edited section content (HTML) |
isEdit | Boolean confirming the content was modified |
Cancel
curl -X POST "${BASE_URL}/ai/api/tools/k12-qrg-cancel/execute" \
-H "Authorization: Bearer $TOKEN" \
-H "Ocp-Apim-Subscription-Key: $SUBSCRIPTION_KEY" \
-H "Content-Type: application/json" \
-d '{"data": {"requestId": "req-abc...", "jobType": "editing"}}'
Tool Reference Summary
| Tool ID | Group | Purpose |
|---|---|---|
k12-district-planning-kickoff | EOP | Start district-level EOP planning edits |
k12-facility-planning-kickoff | EOP | Start facility-level EOP planning edits |
k12-functional-annex-kickoff | EOP | Start functional annex editing |
k12-hazard-annex-kickoff | EOP | Start hazard annex editing |
k12-functional-annex-suggest-roles-kickoff | EOP | Suggest missing roles for a functional annex |
k12-hazard-annex-suggest-roles-kickoff | EOP | Suggest missing roles for a hazard annex |
k12-annex-editing-kickoff | EOP | Compatibility kickoff for general EOP annex editing |
k12-annex-editing-status | EOP | Poll editing status |
k12-annex-editing-result | EOP | Fetch editing result |
k12-qrg-generation-kickoff | QRG Gen | Start QRG generation from EOP |
k12-qrg-generation-status | QRG Gen | Poll generation status |
k12-qrg-generation-result | QRG Gen | Fetch generated QRGs |
k12-qrg-editing-kickoff | QRG Edit | Start QRG section editing |
k12-qrg-editing-status | QRG Edit | Poll editing status |
k12-qrg-editing-result | QRG Edit | Fetch editing result |
k12-cancel-task-request | EOP | Cancel EOP editing job |
k12-qrg-cancel | QRG | Cancel QRG generation or editing job |
Status Reference
All workflows follow the async pattern: kickoff returns accepted, poll until terminal, then fetch result.
EOP Editing and QRG Editing (lowercase)
| Status | Terminal | Meaning |
|---|---|---|
accepted | No | Job queued |
processing | No | AI processing |
success | Yes | Complete |
error | Yes | Failed |
cancelled | Yes | Cancelled |
QRG Generation (UPPERCASE)
| Status | Terminal | Meaning |
|---|---|---|
accepted | No | Job queued |
PROCESSING | No | AI pipeline running |
SUCCESS | Yes | Complete |
ERROR | Yes | Failed |
CANCELLED | Yes | Cancelled |
Error Handling
| HTTP Status | Meaning |
|---|---|
| 400 | Missing required fields or invalid format |
| 401 | Invalid token or missing subscription key |
| 403 | Token lacks required scope |
| 404 | Tool ID not found |
| 422 | Validation error (schema failure) |
| 429 | Rate limit exceeded |
| 500 | Internal server error |
Integration Best Practices
- Poll with exponential backoff -- Start at 2 seconds, increase the interval. Set a maximum timeout based on expected EOP size.
- Persist
requestIdandsessionId-- Both are required for status polling and result retrieval. - Handle both status conventions -- QRG Generation uses UPPERCASE; EOP and QRG Editing use lowercase. Normalize before comparison.
- Expect partial success in QRG Generation -- Individual annexes can fail while others succeed. Check
aiStatsfor per-step diagnostics. - Use sessions for multi-step flows -- Pass
sessionIdfrom a previous response to maintain conversation context across related edits. - Verify organization type -- QRGs can only be saved to FACILITY organizations, not DISTRICT.
- Cache tokens -- OAuth tokens expire after ~1 hour. Cache and refresh proactively.
Next Steps
Detailed OAuth setup, environment URLs, and credential management
Domain-scoped endpoints with flat JSON bodies and workflow routes
Complete request and response schemas for all 17 tools
4-step AI pipeline, form processing rules, and result interpretation