简介
该 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 | 原因 |
|---|---|---|
| 400 | BAD_REQUEST | 参数缺失或无效。 |
| 401 | UNAUTHORIZED / INVALID_API_KEY | 密钥缺失、无效或已吊销。 |
| 403 | INSUFFICIENT_SCOPE | 密钥不具备所需的权限范围。 |
| 403 | ETABLISSEMENT_NOT_GRANTED | 密钥未被授权用于所请求的学校。 |
| 403 | SANDBOX_READONLY | 使用沙盒密钥禁止写入(只读)。 |
| 402 | PLAN_UPGRADE_REQUIRED | 此功能仅限付费套餐(例如 iCal 导出)。 |
| 404 | NOT_FOUND | 资源不存在或不属于您的学校。 |
| 409 | WORKFLOW_ERROR | 无法进行工作流转换(例如发布一份草稿)。 |
| 429 | RATE_LIMITED | 超出速率 — 请在 Retry-After 请求头指定的时间后重试。 |
在线测试 — 无需注册
公共演示端点(无需密钥)。
Postman:« Import » → « Link » → 粘贴 /q/openapi?format=json。
参考 — 账户与内省
/me当前密钥的详情:环境、目标学校、已授予的权限范围以及剩余的生成配额。无需任何权限范围。
{
"environment": "LIVE",
"etablissementId": "61a9…8cb",
"scopes": ["config:read", "timetables:write"],
"entitlements": {
"plan": "PRO",
"generationsRestantes": -1,
"resetLe": "2026-07-14T00:00:00Z",
"maxClassesExport": -1
}
}/me/etablissement目标学校的公开资料:名称、简称、城市、国家/地区、类型、网站。无需任何权限范围。
{"id":"…","nom":"Groupe Scolaire Les Palmiers","slug":"gs-palmiers","sigle":"GSP","ville":"Abidjan","pays":"CI","type":"PRIVE","siteWeb":"https://…"}/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 对其进行配置和生成。
/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"}/account/etablissementsestablishments:manage列出您已创建的学校。
[{"id":"…","nom":"…","slug":"…","pays":"CI","type":"PRIVE_LAIC"}]参考 — 配置
学校的教学结构。读取:权限范围 config:read。写入:权限范围 config:write(沙盒中禁止)。一所新创建的学校已包含一个«主学段»。
/cyclesconfig:read学段列表。
[{"id":"…","libelle":"Cycle principal","ordre":0}]/cyclesconfig:write创建一个学段。
{"libelle":"Second cycle","ordre":1}/cycles/{id}config:write删除一个学段。
/niveauxconfig:read班级/年级列表。
[{"id":"…","libelle":"6ème 1","cycleId":"…","effectif":42,"ordre":1}]/niveauxconfig:write创建一个班级。
{"libelle":"6ème 3","cycleId":"<uuid-cycle>","effectif":40,"ordre":3}/niveaux/{id}config:write修改一个班级(部分字段)。
/niveaux/{id}config:write删除一个班级。
/disciplinesconfig:read科目列表。
[{"id":"…","libelle":"Mathématiques","code":"MATH","couleur":"#6C5CE7","ordre":1}]/disciplinesconfig:write创建一门科目。
{"libelle":"Physique-Chimie","code":"PC","couleur":"#00B894","ordre":5}/disciplines/{id}config:write修改一门科目。
/disciplines/{id}config:write删除一门科目。
/professeursconfig:read教师列表。
[{"id":"…","nom":"Koné","prenoms":"Awa","matricule":"P-0142","email":"…","telephone":"…","nombreHeureAutorise":18,"statut":"TITULAIRE","actif":true}]/professeursconfig:write创建一名教师。
{"nom":"Diarra","prenoms":"Sékou","matricule":"P-0210","email":"s.diarra@ecole.ci","nombreHeureAutorise":18,"statut":"VACATAIRE","actif":true}/professeurs/{id}config:write修改一名教师。
/professeurs/{id}config:write删除一名教师。
/horairesconfig:read课表网格的时段。
[{"id":"…","jour":"MONDAY","heureDebut":"08:00","heureFin":"09:00","estPause":false,"libelle":null}]/horairesconfig:write创建一个时段(日期,HH:mm)。
{"jour":"MONDAY","heureDebut":"08:00","heureFin":"09:00","estPause":false,"libelle":"M1"}/horaires/{id}config:write删除一个时段。
/volumesconfig:read课时量(科目 × 学段)。
[{"id":"…","cycleId":"…","disciplineId":"…","nombreHeuresObligatoire":4,"minHeuresConsecutives":1,"maxHeuresConsecutives":2}]/volumesconfig:write创建一个课时量。
{"cycleId":"<uuid>","disciplineId":"<uuid>","nombreHeuresObligatoire":4,"minHeuresConsecutives":1,"maxHeuresConsecutives":2}/volumes/{id}config:write删除一个课时量。
参考 — 课程表
异步生成(求解器在后台运行 — 请轮询状态)、发布与查询。在沙盒中,生成使用一组演示数据,且不计入配额。
/timetables/generatetimetables:write启动一次生成。返回 202 + 一个待轮询的 jobId。LIVE 会计入每周配额。可选请求头 Idempotency-Key:相同的标识会重放已记忆的响应(响应标记为 Idempotency-Replayed: true),而不会重新启动一次生成。
Idempotency-Key— 请求头(可选)— LIVE 端的幂等键
{"durationMinutes": 5}{"jobId":"…","status":"SOLVING_SCHEDULED"}/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}/timetables/jobs/{jobId}timetables:read某个任务的状态。
/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}]}/timetables/jobs/{jobId}/publishtimetables:write发布课程表(工作流 → PUBLIE)。触发 webhook 事件 timetable.published。
/timetables/published/latesttimetables:read学校最近发布的课程表。
/timetables/published/{jobId}/classestimetables:read已发布课程表中所含班级的名称。
{"classes":["6ème 1","6ème 2","5ème 1"]}/timetables/published/{jobId}/classe/{niveau}timetables:read已发布课程表中某个班级的课程。
/timetables/jobs/{jobId}/export.icstimetables:readiCalendar 导出(text/calendar)。仅限专业版套餐 → 否则返回 402。
niveau— 按班级筛选professeur— 按教师筛选
/timetables/jobs/{jobId}/export.pdftimetables:readPDF 导出(application/pdf)— 全部、按班级或按教师。
mode— all | class | teacherniveau— 当 mode=class 时必填professeur— 当 mode=teacher 时必填
参考 — Webhooks
接收已签名的事件。权限范围 webhooks:manage。密钥仅在创建时(或轮换后)返回。事件:timetable.job.completed、timetable.job.failed、timetable.published、webhook.test。每次投递都经过签名(HMAC-SHA256),失败时最多重放 5 次。URL 会经过反 SSRF 校验(拒绝内部/私有目标)。
/webhookswebhooks:manage端点列表。
/webhookswebhooks:manage创建一个端点。返回签名密钥(请妥善保存)。
{"url":"https://exemple.com/webhooks/yap","events":["timetable.published","timetable.job.completed"]}{"endpoint":{"id":"…","url":"…","events":["timetable.published"],"active":true},"secret":"whsec_…"}/webhooks/{id}webhooks:manage删除一个端点。
/webhooks/{id}/deliverieswebhooks:manage投递日志(状态、HTTP 代码、尝试次数)。
/webhooks/{id}/rotatewebhooks:manage重新生成签名密钥(仅返回一次)。
{"endpoint":{"id":"…","url":"…"},"secret":"whsec_…"}/webhooks/{id}/testwebhooks:manage向端点发送一个 webhook.test 事件(202 + status queued)。
/webhooks/deliveries/{deliveryId}/replaywebhooks:manage重放一次过往投递(202 + status queued)。
参考 — 演示(公共,无需密钥)
无需认证的公共端点,用于了解响应的形态。
/api/v1/public/api-demo/preview一所演示学校的预览(学段、班级、科目、教师、时段)。
/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、沙盒、配额请求头。
