API Reference

Technical documentation and endpoint specifications

API Base URL

https://api.planifica.education

All API endpoints are relative to this base URL. For example, to optimize a schedule, POST to https://api.planifica.education/api/v1/edt/optimize

Authentication

All API requests require authentication using an API key. Include your API key in the request header.

Authorization Header
X-API-Key: digi_xxxxxxxxxxxxxxxxxxxxxxxx
Authorization: ApiKey digi_xxxxxxxxxxxxxxxxxxxxxxxx

Either header works — use whichever fits your HTTP client.

Security Note

Keep your API key secure and never expose it in client-side code. Use environment variables in production.

School Identifier (Required)

In addition to your API key, every optimize, allocation, and validate request must include schoolExternalId and schoolNameas top-level fields identifying which school the request is for. This is enforced server-side — requests without them are rejected.

Required school fields
{
  "schoolExternalId": "your-school-id",
  "schoolName": "School Display Name"
}

schoolExternalId vs schoolName

schoolExternalId is a stable identifier you choose (e.g. your own school ID) and is used as the grouping key for usage tracking and per-school rate limits.schoolName is a human-readable label. Both fields are required strings, max 255 characters.

API Endpoints

POSThttps://api.planifica.education/api/v1/edt/optimize

Start an asynchronous optimization task

Request Body

The request body is the EDT optimization payload, plus two required top-level fields, schoolExternalId and schoolName (identify the school for usage tracking, see Authentication). There is no wrapper object — if you already send EDTRequest bodies directly to the ML backend, just add these two fields.

{
  "schoolExternalId": "your-school-id",
  "schoolName": "School Display Name",
  "requestid": "unique-request-identifier",
  "config": {
    "max_time_in_seconds": 600,
    "objective_type": "AFFINITY",
    "use_objective": false,
    "debug_enabled": true,
    "debug_level": "full"
  },
  "data": {
    "requestid": "unique-request-identifier",
    "config": {
      "max_time_in_seconds": 600,
      "objective_type": "AFFINITY",
      "use_objective": false,
      "debug_enabled": true,
      "debug_level": "full"
    },
    {
      "code": "MA",
      "name": "Mathematics"
    }
  ],
  "grades": [
    {
      "code": "CP",
      "groups": ["A", "B"],
      "working_hours": [
        ["Monday", {
          "available_hours": [["08:00", "12:00"], ["13:00", "17:00"]],
          "unavailable_periods": [["10:00", "10:30"]]
        }]
      ]
    }
  ],
  "classrooms": [
    {
      "id": 1,
      "subjects": ["AR", "MA"]
    }
  ],
  "instructors": [
    {
      "id": 1,
      "name": "John Doe",
      "subjects": ["MA"],
      "capacity": 24,
      "grades": ["CP"],
      "available_days": [
        ["Monday", {
          "available_hours": [["08:00", "17:00"]],
          "unavailable_periods": [["12:00", "13:00"]]
        }]
      ]
    }
  ],
  "schemas": [
    {
      "grade": "CP",
      "sessions": [
        {
          "id": "session_1",
          "session": "MATH_001",
          "grade": "CP",
          "group": "A",
          "duration": 2,
          "subject": "MA"
        },
        {
          "id": "session_2",
          "session": "MATH_002",
          "grade": "CP",
          "group": "B",
          "duration": 2,
          "subject": "MA"
        }
      ]
    }
  ]
}

Note: Requests missing schoolExternalId or schoolName are rejected with a 400 error (SCHOOL_REQUIRED).

Response

{
  "id": "task_123456",
  "status": "queued",
  "message": "Optimization task queued successfully"
}

Optimization Configuration

Control optimization behavior with intelligent stop conditions that balance solution quality, processing time, and resource consumption.

Configuration Guidelines

  • • Use shorter time limits (60-300s) for interactive applications
  • • Enable no_improvement_timeout for resource conservation
  • • Always enable use_objective for better solution quality

Time-Based

Set hard limits on processing time to ensure responsive applications.

max_time_in_seconds: 300

Quality-Based

Stop automatically when solution quality stops improving.

no_improvement_timeout_seconds: 60

Data Models

Key objects used across the optimization, allocation, and validation endpoints.

School Identifier Fields

Top-level fields identifying the target school for the request, alongside your EDTRequest fields. Required on every optimize, allocation, and validate call.

FieldTypeDescription
schoolExternalIdstring (required, max 255)Stable identifier you assign to the school. Used to group requests by school for usage and rate-limiting purposes.
schoolNamestring (required, max 255)Human-readable school name.
{
  "schoolExternalId": "your-school-id",
  "schoolName": "School Display Name"
}

Partner API

The endpoints above belong to a dedicated gateway for managed partner integrations: validate a payload, submit an optimization or allocation job, and poll the task by ID. Authentication and request shape are the same as described in Authentication above.

API Key Headers

Include one of these headers on every request to the partner gateway.

X-API-Key: digi_xxxxxxxxxxxxxxxxxxxxxxxxAuthorization: ApiKey digi_xxxxxxxxxxxxxxxxxxxxxxxx

Gateway Endpoints

Method
Endpoint
POST
/api/v1/edt/optimize
Submit an optimization request: Starts a Planifica optimization job and returns a taskId for polling. Requires schoolExternalId and schoolName in the body.
202 Accepted
POST
/api/v1/edt/allocation
Submit an allocation request: Finds possible teacher, class, and room allocations before running full timetable optimization. Requires schoolExternalId and schoolName in the body.
202 Accepted
POST
/api/v1/edt/validate
Validate a scheduling payload: Checks a scheduling request without starting an optimization job. Requires schoolExternalId and schoolName in the body.
200 OK
GET
/api/v1/edt/{taskId}
Read task status: Returns the task status and result when available.
200 OK
GET
/api/v1/edt/{taskId}/solutions
Read task solution history: Returns the full solution history for an optimization task. Only meaningful for optimize tasks — allocation requests are synchronous and have no pollable task.
200 OK
DELETE
/api/v1/edt/{taskId}
Cancel a task: Cancels a pending or processing optimization task when the backend still accepts cancellation.
204 No Content

Task Status Values

PENDINGPROCESSINGCOMPLETEDFAILED

See Error Codes below for the optimizer's domain-level errors, and the dedicated error code reference for the partner gateway's own auth/HTTP error codes (invalid key, rate limit, etc.).

Migration Guide: Direct ML Backend → Partner Gateway

If you previously integrated directly against the optimization engine (without an API key, on a different host), use this section to move onto the partner gateway above. Two independent things change: where and how you call it, and what you send— the request schema itself has breaking changes versus an older combined-config shape some direct integrations still use.

What's Changing

Legacy (direct)Partner Gateway
HostDirect engine host, no gatewayapi.planifica.education
AuthNoneAPI key required
Request bodyRaw EDT payload, old combined subject configSame payload + schoolExternalId/schoolName, new split config
Usage trackingNonePer-school, per-API-key, automatic

The gateway is a thin, mostly pass-through proxy in front of the same optimization engine — it adds auth, telemetry, and usage tracking, and strips two extra fields before forwarding your payload. It does not change endpoint semantics (async optimize, sync allocation, validate). If your old client called /allocate, the gateway path is /allocation — see API Endpoints above for the full path list.

Required Fields

Add schoolExternalId and schoolName as top-level siblings of your existing request — there is no wrapper object. See Authentication above for the exact shape and validation rules.

Breaking Schema Change: Combined Subject Config Removed

The old combined subject config (one object per subject mixing capacity, affinity, cardinality, and a single-day window) is not deprecated — it has been removed entirely. There is no combined model in the backend anymore; only the three split configs below exist.

Old (combined, single-day window)

{
  "subject": "math",
  "capacity": 30,
  "affinity": 5,
  "cardinality": 2,
  "day": "monday",
  "start": "08:00",
  "end": "12:00"
}

New (split into typed lists, multi-day window)

// schemas[].capacity_configs
{
  "subject": "math",
  "capacity": 30,
  "start_day": "monday",
  "start_time": "08:00",
  "end_day": "monday",
  "end_time": "12:00"
}
// affinity_configs / cardinality_configs
// follow the same start_day/start_time/
// end_day/end_time shape
  • Each schemas[] entry (one per grade) now carries separate arrays: capacity_configs, affinity_configs, cardinality_configs — instead of one combined object per subject.
  • Field rename: day / start / endstart_day / start_time / end_day / end_time. The window can now span multiple days.
  • New optional array ordering_configs — session-ordering constraints (follows / avoid_after / distance_between) between two subjects, with min_gap_slots / max_gap_slots. Omit it if you don't need ordering constraints.
  • classrooms[] gained an optional specialized: boolean (default false) to mark scarce/specialized rooms (labs, gyms) for capacity-aware scheduling.

This fails silently, not loudly. The request body as a whole does not reject unrecognized fields at the schemas[] level, so if you still send the old combined-config shape under its old field name, the call will succeed (no validation error) but your capacity/affinity/cardinality constraints will be silently dropped from the solve — they simply aren't mapped to anything. Only session objects (schemas[].sessions[]) reject unknown fields; the constraint-config lists do not. A 2xx response does not mean your constraints were applied — after migrating, verify constraints actually show up in the result (e.g. check coverage_percent or solution statsagainst a known-good baseline), don't just check the HTTP status.

New error codes you may see from stricter validation and the features above: SESSION_ORDERING_CYCLE, DISTANCE_BETWEEN_INFEASIBLE, INVALID_DISTANCE_BETWEEN_CONFIG, SPECIALIZED_ROOM_CAPACITY_EXCEEDED, INSUFFICIENT_CONTINUOUS_SLOTS, among others — see Error Codes below.

Response & Polling Changes

  • Task IDs: optimize and allocation responses include a taskId generated by the gateway. Use this taskId — not any internal id in the response body — for all follow-up status, solutions, and cancel calls.
  • Status normalization: task status is normalized to PENDING / PROCESSING / COMPLETED / FAILED (see Task Status Values above).
  • Stats relocation: if you previously read top-level stats on an allocation response, stats now live per solution: allocations.solutions[].stats.

Migration Checklist

  1. Create an API key (see Authentication); store it securely — it's shown once.
  2. Add schoolExternalId and schoolName as top-level sibling fields on every request.
  3. If still on the old combined subject config: split it into capacity_configs, affinity_configs, cardinality_configs under each schemas[] entry.
  4. Rename day / start / end to start_day / start_time / end_day / end_time on every config window.
  5. Remove any non-schema fields from schemas[].sessions[] objects (strictly validated).
  6. Switch the request host to api.planifica.education and confirm paths match /api/v1/edt/* (note /allocate/allocation if applicable).
  7. Add the Authorization: ApiKey <key> (or X-API-Key) header to every request.
  8. Update polling logic to use the gateway's taskId from the initial response, not any internal id.
  9. Update response parsing for the relocated per-solution stats and the normalized status enum.
  10. Re-test validate, optimize, and allocation end-to-end against the new host before cutting over production traffic.

Side-by-Side Example

Before — direct, old config, no auth

POST /api/v1/edt/optimize
Content-Type: application/json

{
  "requestid": "req-001",
  "timetable": { "time_granularity": 30 },
  "schemas": [{
    "grade": "G1",
    "sessions": [],
    "teachers_capacities": [{
      "subject": "math", "capacity": 30,
      "affinity": 5, "cardinality": 2,
      "day": "monday",
      "start": "08:00", "end": "12:00"
    }]
  }]
}

After — gateway, split config, API key

POST /api/v1/edt/optimize
Content-Type: application/json
Authorization: ApiKey digi_xxxxxxxxxxxxxxxxxxxx

{
  "schoolExternalId": "school-abc-001",
  "schoolName": "ABC High School",
  "requestid": "req-001",
  "timetable": { "time_granularity": 30 },
  "schemas": [{
    "grade": "G1",
    "sessions": [],
    "capacity_configs": [{ "subject": "math", "capacity": 30,
      "start_day": "monday", "start_time": "08:00",
      "end_day": "monday", "end_time": "12:00" }],
    "affinity_configs": [{ "subject": "math", "affinity": 5,
      "start_day": "monday", "start_time": "08:00",
      "end_day": "monday", "end_time": "12:00" }],
    "cardinality_configs": [{ "subject": "math", "cardinality": 2,
      "start_day": "monday", "start_time": "08:00",
      "end_day": "monday", "end_time": "12:00" }]
  }]
}

Error Codes

The API returns specific error codes to help identify and resolve scheduling issues. Each errors[]/warnings[] entry has a stable code you should branch on, plus a human-readable message that's safe to show end users but may be reworded between releases.

A 200/202 response is not, by itself, success. Domain-level scheduling problems are returned inside the response body, not as HTTP error statuses. Always check status (/optimize, /allocation) or valid (/validate) and inspect errors[] before assuming a request succeeded. errors[] entries are fatal; warnings[]entries are non-fatal — the request still processed, but something is worth surfacing to the end user.

The full catalog — optimizer/scheduling error codes (data integrity, setup, availability, capacity, locked sessions, ordering, cardinality) plus the partner gateway's own auth/HTTP error codes — lives on a dedicated page so it doesn't crowd out the rest of this reference.

View all error codes →

Error Handling

HTTP Status Codes

200

Success

Request completed successfully

400

Bad Request

Invalid request data or missing required fields

401

Unauthorized

Missing or invalid API key

429

Too Many Requests

Rate limit exceeded

500

Internal Server Error

Unexpected server error

Error Response Format

HTTP-level errors (4xx/5xx) share one envelope across every endpoint. message is always an array of human-readable strings. When the optimizer rejected a specific part of your request — a malformed field during request validation, or a scheduling error tied to a particular grade/group/subject — details is included as an array pinpointing each one via field. code and details are omitted when not applicable (e.g. auth failures).

{
  "statusCode": 422,
  "timestamp": "2026-07-08T10:15:00.000Z",
  "path": "/api/v1/edt/optimize",
  "method": "POST",
  "message": ["Input should be a valid integer"],
  "requestId": "b4d6e6b0-6e0a-4b3a-9a3a-1c9b9a3f2e11",
  "code": "OPTIMIZER_ERROR_422",
  "details": [
    {
      "field": "sessions.3.duration",
      "message": "Input should be a valid integer",
      "type": "int_parsing"
    }
  ]
}

Rate Limits

Current Rate Limits

  • • 100 requests per minute for optimization endpoints
  • • 1000 requests per minute for status and validation endpoints
  • • 10 concurrent optimization tasks per API key

Rate limit information is included in response headers:

X-RateLimit-Limit: 100
X-RateLimit-Remaining: 99
X-RateLimit-Reset: 1640995200