Aller au contenu
公共 API · 免费

YA-Planning API 文档

集成课程表的生成与查询,管理您学校的配置,并实时接收事件。完整参考、示例与沙盒。

简介

该 API 是 REST 风格、基于 JSON、带版本管理的。所有业务资源均位于前缀 /api/v1/pub/v1 下。公共演示端点位于 /api/v1/public/api-demo 下。

  • 基础 URL:https://votre-domaine/api/v1/pub/v1
  • 格式:请求与响应采用 application/json(ICS/PDF 导出除外)。
  • 版本管理:版本包含在路径中(/v1);向后兼容的新增不会改变版本。
  • CORS:公共 API 对第三方浏览器应用开放(不含 cookie)。

认证

每个请求都在以下两个请求头之一中携带 API 密钥:

X-API-Key: yap_live_xxxxxxxxxxxxxxxxxxxx
# ou
Authorization: Bearer yap_live_xxxxxxxxxxxxxxxxxxxx

一个密钥可被授权用于多所学校(企业密钥)。此时请通过请求头 X-Etablissement-Id 指定目标;如果仅有一所被授权的学校,则会自动推断。密钥在控制台中按权限范围创建。

环境与沙盒

两种环境,通过密钥前缀区分:

  • yap_live_… — 生产环境:您的真实数据;生成会计入每周配额。
  • yap_test_… — 沙盒:读取会返回一组演示数据(绝非您的真实数据),生成不计入配额,且配置写入会被拒绝(SANDBOX_READONLY)。非常适合无风险地开发。

权限范围

每个密钥携带权限范围的一个子集;如果缺少所需的权限范围,端点会拒绝(403 INSUFFICIENT_SCOPE)。

config:read读取学段、班级、科目、教师、时段、课时量。
config:write创建 / 修改 / 删除学校的配置。
timetables:read列出任务、读取结果和已发布的课程表、导出。
timetables:write启动生成并发布课程表。
webhooks:manage创建、列出并删除 webhook 端点。
establishments:manage创建并列出自己的学校(账户级,无 X-Etablissement-Id)。

速率与配额

每个密钥都有速率限制(默认 60 次请求/分钟)。响应会携带:

X-RateLimit-Limit: 60
X-RateLimit-Remaining: 59
X-RateLimit-Reset: 1783919311   # epoch (s) de réinitialisation

超出限制会返回 429 RATE_LIMITED 以及 Retry-After 请求头。课程表生成还额外受套餐每周配额的限制(可通过 /me 查看)。

API 的每个响应还会携带一个 X-Request-Id 请求头(关联标识)— 发生故障时请将其提供给支持团队。注意:速率计数目前按实例应用;在多实例部署中,实际限制可能是所显示值的若干倍。

错误

统一的错误封装,含用于支持团队的 requestId:

{ "error": "INSUFFICIENT_SCOPE", "message": "Scope requis : config:write.",
  "requestId": "6181762d-…", "requiredScope": "config:write" }
代码error原因
400BAD_REQUEST参数缺失或无效。
401UNAUTHORIZED / INVALID_API_KEY密钥缺失、无效或已吊销。
403INSUFFICIENT_SCOPE密钥不具备所需的权限范围。
403ETABLISSEMENT_NOT_GRANTED密钥未被授权用于所请求的学校。
403SANDBOX_READONLY使用沙盒密钥禁止写入(只读)。
402PLAN_UPGRADE_REQUIRED此功能仅限付费套餐(例如 iCal 导出)。
404NOT_FOUND资源不存在或不属于您的学校。
409WORKFLOW_ERROR无法进行工作流转换(例如发布一份草稿)。
429RATE_LIMITED超出速率 — 请在 Retry-After 请求头指定的时间后重试。

在线测试 — 无需注册

公共演示端点(无需密钥)。

下载 OpenAPI

Postman:« Import » → « Link » → 粘贴 /q/openapi?format=json。

参考 — 账户与内省

GET/me

当前密钥的详情:环境、目标学校、已授予的权限范围以及剩余的生成配额。无需任何权限范围。

响应
{
  "environment": "LIVE",
  "etablissementId": "61a9…8cb",
  "scopes": ["config:read", "timetables:write"],
  "entitlements": {
    "plan": "PRO",
    "generationsRestantes": -1,
    "resetLe": "2026-07-14T00:00:00Z",
    "maxClassesExport": -1
  }
}
GET/me/etablissement

目标学校的公开资料:名称、简称、城市、国家/地区、类型、网站。无需任何权限范围。

响应
{"id":"…","nom":"Groupe Scolaire Les Palmiers","slug":"gs-palmiers","sigle":"GSP","ville":"Abidjan","pays":"CI","type":"PRIVE","siteWeb":"https://…"}
GET/me/usage

当前密钥的近期用量(按天的调用与错误)。无需任何权限范围。

参数:
  • days — 时间窗口(天)(默认 30,最大 90)
响应
{"apiKeyId":"…","days":30,"daily":[{"day":"2026-07-12","calls":128,"errors":3}]}

参考 — 账户 — 学校

账户级开通:在不带 X-Etablissement-Id 请求头的情况下创建您自己的学校(/account 路径不与某所学校绑定)。权限范围 establishments:manage。创建时,发起调用的密钥会自动获得对新学校的 ACTIVE 授权 — 之后请使用 X-Etablissement-Id 对其进行配置和生成。

POST/account/etablissementsestablishments:manage

创建一所学校(开发者配额池,受您账户上限约束)。

请求体
{"nom":"Groupe Scolaire Les Palmiers","type":"PRIVE_LAIC","pays":"CI"}
响应
{"id":"…","nom":"Groupe Scolaire Les Palmiers","slug":"gs-les-palmiers","pays":"CI","type":"PRIVE_LAIC"}
GET/account/etablissementsestablishments:manage

列出您已创建的学校。

响应
[{"id":"…","nom":"…","slug":"…","pays":"CI","type":"PRIVE_LAIC"}]

参考 — 配置

学校的教学结构。读取:权限范围 config:read。写入:权限范围 config:write(沙盒中禁止)。一所新创建的学校已包含一个«主学段»。

GET/cyclesconfig:read

学段列表。

响应
[{"id":"…","libelle":"Cycle principal","ordre":0}]
POST/cyclesconfig:write

创建一个学段。

请求体
{"libelle":"Second cycle","ordre":1}
DELETE/cycles/{id}config:write

删除一个学段。

GET/niveauxconfig:read

班级/年级列表。

响应
[{"id":"…","libelle":"6ème 1","cycleId":"…","effectif":42,"ordre":1}]
POST/niveauxconfig:write

创建一个班级。

请求体
{"libelle":"6ème 3","cycleId":"<uuid-cycle>","effectif":40,"ordre":3}
PUT/niveaux/{id}config:write

修改一个班级(部分字段)。

DELETE/niveaux/{id}config:write

删除一个班级。

GET/disciplinesconfig:read

科目列表。

响应
[{"id":"…","libelle":"Mathématiques","code":"MATH","couleur":"#6C5CE7","ordre":1}]
POST/disciplinesconfig:write

创建一门科目。

请求体
{"libelle":"Physique-Chimie","code":"PC","couleur":"#00B894","ordre":5}
PUT/disciplines/{id}config:write

修改一门科目。

DELETE/disciplines/{id}config:write

删除一门科目。

GET/professeursconfig:read

教师列表。

响应
[{"id":"…","nom":"Koné","prenoms":"Awa","matricule":"P-0142","email":"…","telephone":"…","nombreHeureAutorise":18,"statut":"TITULAIRE","actif":true}]
POST/professeursconfig:write

创建一名教师。

请求体
{"nom":"Diarra","prenoms":"Sékou","matricule":"P-0210","email":"s.diarra@ecole.ci","nombreHeureAutorise":18,"statut":"VACATAIRE","actif":true}
PUT/professeurs/{id}config:write

修改一名教师。

DELETE/professeurs/{id}config:write

删除一名教师。

GET/horairesconfig:read

课表网格的时段。

响应
[{"id":"…","jour":"MONDAY","heureDebut":"08:00","heureFin":"09:00","estPause":false,"libelle":null}]
POST/horairesconfig:write

创建一个时段(日期,HH:mm)。

请求体
{"jour":"MONDAY","heureDebut":"08:00","heureFin":"09:00","estPause":false,"libelle":"M1"}
DELETE/horaires/{id}config:write

删除一个时段。

GET/volumesconfig:read

课时量(科目 × 学段)。

响应
[{"id":"…","cycleId":"…","disciplineId":"…","nombreHeuresObligatoire":4,"minHeuresConsecutives":1,"maxHeuresConsecutives":2}]
POST/volumesconfig:write

创建一个课时量。

请求体
{"cycleId":"<uuid>","disciplineId":"<uuid>","nombreHeuresObligatoire":4,"minHeuresConsecutives":1,"maxHeuresConsecutives":2}
DELETE/volumes/{id}config:write

删除一个课时量。

参考 — 课程表

异步生成(求解器在后台运行 — 请轮询状态)、发布与查询。在沙盒中,生成使用一组演示数据,且不计入配额。

POST/timetables/generatetimetables:write

启动一次生成。返回 202 + 一个待轮询的 jobId。LIVE 会计入每周配额。可选请求头 Idempotency-Key:相同的标识会重放已记忆的响应(响应标记为 Idempotency-Replayed: true),而不会重新启动一次生成。

参数:
  • Idempotency-Key — 请求头(可选)— LIVE 端的幂等键
请求体
{"durationMinutes": 5}
响应
{"jobId":"…","status":"SOLVING_SCHEDULED"}
GET/timetables/jobstimetables:read

任务的分页列表。

参数:
  • page — 页码(默认 0)
  • size — 大小(默认 25,最大 100)
响应
{"items":[{"jobId":"…","name":"…","status":"SOLVING_ENDED","workflowStatus":"BROUILLON","feasible":true,"hardScore":0,"softScore":-42,"createdAt":"…"}],"total":7,"page":0,"size":25}
GET/timetables/jobs/{jobId}timetables:read

某个任务的状态。

GET/timetables/jobs/{jobId}/resulttimetables:read

结果:课程列表(当 status = SOLVING_ENDED 时)。

响应
{"jobId":"…","status":"SOLVING_ENDED","feasible":true,"lessons":[{"id":"1","matiere":"Maths","niveau":"6ème 1","professeur":"M. Koné","jour":"MONDAY","debut":"08:00:00","fin":"09:00:00","couleur":"#6C5CE7","niveauOrdre":1,"verrouille":false}]}
POST/timetables/jobs/{jobId}/publishtimetables:write

发布课程表(工作流 → PUBLIE)。触发 webhook 事件 timetable.published。

GET/timetables/published/latesttimetables:read

学校最近发布的课程表。

GET/timetables/published/{jobId}/classestimetables:read

已发布课程表中所含班级的名称。

响应
{"classes":["6ème 1","6ème 2","5ème 1"]}
GET/timetables/published/{jobId}/classe/{niveau}timetables:read

已发布课程表中某个班级的课程。

GET/timetables/jobs/{jobId}/export.icstimetables:read

iCalendar 导出(text/calendar)。仅限专业版套餐 → 否则返回 402。

参数:
  • niveau — 按班级筛选
  • professeur — 按教师筛选
GET/timetables/jobs/{jobId}/export.pdftimetables:read

PDF 导出(application/pdf)— 全部、按班级或按教师。

参数:
  • mode — all | class | teacher
  • niveau — 当 mode=class 时必填
  • professeur — 当 mode=teacher 时必填

参考 — Webhooks

接收已签名的事件。权限范围 webhooks:manage。密钥仅在创建时(或轮换后)返回。事件:timetable.job.completed、timetable.job.failed、timetable.published、webhook.test。每次投递都经过签名(HMAC-SHA256),失败时最多重放 5 次。URL 会经过反 SSRF 校验(拒绝内部/私有目标)。

GET/webhookswebhooks:manage

端点列表。

POST/webhookswebhooks:manage

创建一个端点。返回签名密钥(请妥善保存)。

请求体
{"url":"https://exemple.com/webhooks/yap","events":["timetable.published","timetable.job.completed"]}
响应
{"endpoint":{"id":"…","url":"…","events":["timetable.published"],"active":true},"secret":"whsec_…"}
DELETE/webhooks/{id}webhooks:manage

删除一个端点。

GET/webhooks/{id}/deliverieswebhooks:manage

投递日志(状态、HTTP 代码、尝试次数)。

POST/webhooks/{id}/rotatewebhooks:manage

重新生成签名密钥(仅返回一次)。

响应
{"endpoint":{"id":"…","url":"…"},"secret":"whsec_…"}
POST/webhooks/{id}/testwebhooks:manage

向端点发送一个 webhook.test 事件(202 + status queued)。

POST/webhooks/deliveries/{deliveryId}/replaywebhooks:manage

重放一次过往投递(202 + status queued)。

参考 — 演示(公共,无需密钥)

无需认证的公共端点,用于了解响应的形态。

GET/api/v1/public/api-demo/preview

一所演示学校的预览(学段、班级、科目、教师、时段)。

GET/api/v1/public/api-demo/sample-timetable

课程示例(所生成课程表的形态)。

Webhooks — 验证签名

每次投递都是一个 JSON POST,带有请求头 X-Webhook-Event、X-Webhook-Timestamp 和 X-Webhook-Signature = HMAC-SHA256(timestamp + "." + body, secret)。请验证签名并核对时间戳的新鲜度(小于 5 分钟)以防止重放。事件:timetable.published、timetable.job.completed、timetable.job.failed、webhook.test。

POST https://votre-app/webhook
X-Webhook-Event: timetable.published
X-Webhook-Timestamp: 1712345678
X-Webhook-Signature: 9f86d081…

{ "event":"timetable.published", "jobId":"…", "etablissementId":"…", "occurredAt":"…" }
import crypto from "crypto";

// Express — vérifier la signature d'un webhook YA-Planning
app.post("/webhooks/yap", express.raw({ type: "*/*" }), (req, res) => {
  const ts  = req.header("X-Webhook-Timestamp");
  const sig = req.header("X-Webhook-Signature");
  const body = req.body.toString("utf8");

  const expected = crypto.createHmac("sha256", process.env.YAP_WEBHOOK_SECRET)
    .update(`${ts}.${body}`).digest("hex");

  // fraîcheur (anti-rejeu) : < 5 min
  const fresh = Math.abs(Date.now()/1000 - Number(ts)) < 300;
  if (!fresh || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected)))
    return res.sendStatus(400);

  const event = JSON.parse(body); // { event, jobId, etablissementId, occurredAt }
  res.sendStatus(200);
});

代码示例

# Vérifier ses droits
curl https://votre-domaine/api/v1/pub/v1/me \
  -H "X-API-Key: yap_live_xxxxxxxxxxxx"

# Générer un EDT puis suivre le job
JOB=$(curl -s -X POST .../api/v1/pub/v1/timetables/generate \
  -H "X-API-Key: yap_live_xxxx" -H "Content-Type: application/json" \
  -d '{"durationMinutes":5}' | jq -r .jobId)

curl .../api/v1/pub/v1/timetables/jobs/$JOB \
  -H "X-API-Key: yap_live_xxxx"

版本与弃用

该 API 在路径中带版本(/api/v1/pub/v1/…)。我们以向后兼容的方式新增字段和端点;这些新增不构成破坏性变更,因此请以宽容的方式编码(忽略未知字段)。任何破坏性变更都将产生一个新的主版本,并至少提前 90 天通过响应头 Deprecation 及下方的变更日志发出预告。

变更日志
  • 2026-07 — 开发者账户与仪表盘;账户级开通(POST /account/etablissements,权限范围 establishments:manage);/cycles 端点;PDF 导出 mode=teacher。
  • 2026-07 — 生成上的 Idempotency-Key;/me/etablissement、/me/usage 端点;webhook 的轮换 / 测试 / 重放;timetable.job.failed 事件;X-Request-Id 请求头;配置列表的分页;密钥的过期与 IP 限制;webhook URL 的反 SSRF 校验。
  • 2026-06 — 公共 API v1 发布:按权限范围的密钥、配置、课程表生成与发布、已签名的 webhook、沙盒、配额请求头。

准备好集成了吗?

30 秒内创建一个沙盒密钥并发起您的第一次调用。

打开 API 控制台