{
  "openapi": "3.1.0",
  "info": {
    "title": "Y-Send",
    "description": "消息推送服务：一个 HTTP 请求把消息送到企业微信 / 钉钉 / 飞书 / 自定义 Webhook，支持幂等发送。\n\n⚠️ 关键约定：本服务所有业务错误（超配额、限流、投递失败等）也返回 HTTP 200，成败必须依据响应 body 的 code 字段判断——code 为 0 才是成功，绝不能依据 HTTP 状态码。",
    "version": "1.0.0"
  },
  "servers": [
    { "url": "https://send.yimeijia.cc" }
  ],
  "paths": {
    "/{sendkey}.send": {
      "parameters": [
        {
          "name": "sendkey",
          "in": "path",
          "required": true,
          "schema": { "type": "string" },
          "description": "控制台分配的 SendKey（形如 YS...）。属于凭证，不要写进日志或代码仓库。"
        }
      ],
      "get": {
        "operationId": "sendMessageGet",
        "summary": "发送消息（GET，参数走 query）",
        "parameters": [
          {
            "name": "title",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "maxLength": 32, "default": "无标题" },
            "description": "消息标题，最长 32 字符，超长静默截断，缺省为「无标题」"
          },
          {
            "name": "desp",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "maxLength": 32768, "default": "" },
            "description": "消息正文，最长 32768 字符，超长静默截断；企业微信 / 钉钉通道支持 Markdown。推送到企业微信（单条 4096 字节，含标题）和飞书（请求体 20 KB）时，超出部分由服务端截断并附「查看全文」链接"
          },
          {
            "name": "idempotency_key",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "minLength": 1, "maxLength": 128 },
            "description": "幂等键，1~128 字符且不含控制字符；请求头 Idempotency-Key 优先于该参数。同一 SendKey + 同一键的重复请求返回首次持久化结果，绝不重复投递"
          }
        ],
        "responses": {
          "200": { "$ref": "#/components/responses/SendResult" },
          "400": { "$ref": "#/components/responses/ProtocolError" },
          "413": { "$ref": "#/components/responses/ProtocolError" }
        }
      },
      "post": {
        "operationId": "sendMessagePost",
        "summary": "发送消息（POST，支持 JSON 与表单）",
        "parameters": [
          {
            "name": "Idempotency-Key",
            "in": "header",
            "required": false,
            "schema": { "type": "string", "minLength": 1, "maxLength": 128 },
            "description": "幂等键（推荐始终携带），1~128 字符且不含控制字符；优先于 idempotency_key 参数"
          }
        ],
        "requestBody": {
          "required": false,
          "content": {
            "application/json": {
              "schema": { "$ref": "#/components/schemas/SendParams" }
            },
            "application/x-www-form-urlencoded": {
              "schema": { "$ref": "#/components/schemas/SendParams" }
            }
          }
        },
        "responses": {
          "200": { "$ref": "#/components/responses/SendResult" },
          "400": { "$ref": "#/components/responses/ProtocolError" },
          "413": { "$ref": "#/components/responses/ProtocolError" }
        }
      }
    },
    "/push": {
      "get": {
        "operationId": "queryPushStatus",
        "summary": "消息投递状态回查（凭 readkey 公开访问）",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "pattern": "^[0-9a-f]{16}$" },
            "description": "发送响应中的 pushid"
          },
          {
            "name": "readkey",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "发送响应中的 readkey"
          },
          {
            "name": "full",
            "in": "query",
            "required": false,
            "schema": { "type": "string", "enum": ["1"] },
            "description": "传 1 时 data 额外包含 desp 正文；默认不返回正文"
          }
        ],
        "responses": {
          "200": {
            "description": "查询成功（code 为 0）",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/PushStatusOk" }
              }
            }
          },
          "400": {
            "description": "参数不完整（缺 id 或 readkey）",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GenericError" },
                "example": { "code": 400, "message": "参数不完整", "data": null }
              }
            }
          },
          "404": {
            "description": "记录不存在或凭证无效（id 不存在、readkey 错误、功能关闭、冷却中——文案完全一致，不做区分）",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GenericError" },
                "example": { "code": 404, "message": "记录不存在或凭证无效", "data": null }
              }
            }
          },
          "429": {
            "description": "IP 限流",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GenericError" }
              }
            }
          }
        }
      }
    },
    "/r": {
      "get": {
        "operationId": "readMessagePage",
        "summary": "消息详情页（HTML，凭 readkey 公开访问）",
        "parameters": [
          {
            "name": "id",
            "in": "query",
            "required": true,
            "schema": { "type": "string", "pattern": "^[0-9a-f]{16}$" },
            "description": "发送响应中的 pushid"
          },
          {
            "name": "readkey",
            "in": "query",
            "required": true,
            "schema": { "type": "string" },
            "description": "发送响应中的 readkey"
          }
        ],
        "responses": {
          "200": {
            "description": "消息详情页（text/html）",
            "content": {
              "text/html": {
                "schema": { "type": "string" }
              }
            }
          },
          "404": {
            "description": "统一的 404 页面（id 不存在、readkey 错误、功能关闭、冷却中——页面完全一致，不做区分）",
            "content": {
              "text/html": {
                "schema": { "type": "string" }
              }
            }
          },
          "429": {
            "description": "IP 限流",
            "content": {
              "application/json": {
                "schema": { "$ref": "#/components/schemas/GenericError" }
              }
            }
          }
        }
      }
    }
  },
  "components": {
    "schemas": {
      "SendParams": {
        "type": "object",
        "properties": {
          "title": {
            "type": "string",
            "maxLength": 32,
            "default": "无标题",
            "description": "消息标题，最长 32 字符，超长静默截断，缺省为「无标题」"
          },
          "desp": {
            "type": "string",
            "maxLength": 32768,
            "default": "",
            "description": "消息正文，最长 32768 字符，超长静默截断；企业微信 / 钉钉通道支持 Markdown。推送到企业微信（单条 4096 字节，含标题）和飞书（请求体 20 KB）时，超出部分由服务端截断并附「查看全文」链接"
          },
          "idempotency_key": {
            "type": "string",
            "minLength": 1,
            "maxLength": 128,
            "description": "幂等键，1~128 字符且不含控制字符；Idempotency-Key 请求头优先于该参数"
          }
        },
        "additionalProperties": true
      },
      "SendSuccess": {
        "type": "object",
        "required": ["code", "message", "data"],
        "properties": {
          "code": { "type": "integer", "enum": [0] },
          "message": { "type": "string" },
          "data": {
            "type": "object",
            "required": ["pushid", "readkey", "error", "errno", "delivery"],
            "properties": {
              "pushid": { "type": "string", "description": "消息 ID（16 位小写 hex），用于 /push 与 /r" },
              "readkey": { "type": "string", "description": "消息阅读密钥" },
              "readUrl": { "type": "string", "description": "消息详情页完整 URL（仅公开阅读页功能开启时输出）" },
              "error": { "type": "string", "enum": ["SUCCESS"] },
              "errno": { "type": "integer", "enum": [0] },
              "delivery": { "type": "string", "enum": ["success"], "description": "全部通道成功或仅站内保存" }
            }
          }
        },
        "example": {
          "code": 0,
          "message": "",
          "data": {
            "pushid": "8f3ad2c1e5b7a9d0",
            "readkey": "AbCdEfGhIjKlMnOp",
            "readUrl": "https://send.yimeijia.cc/r?id=8f3ad2c1e5b7a9d0&readkey=AbCdEfGhIjKlMnOp",
            "error": "SUCCESS",
            "errno": 0,
            "delivery": "success"
          }
        }
      },
      "SendError": {
        "type": "object",
        "required": ["code", "message"],
        "properties": {
          "code": {
            "type": "integer",
            "enum": [40001, 40002, 40003, 40004, 40005, 40006, 40007],
            "description": "40001 sendkey 不正确或不存在；40002 超过每分钟上限（单 SendKey 50 条/分钟）；40003 超过每日上限（免费版 5 条/天、Pro 200 条/天，按账号汇总）；40004 通道投递失败（结果已落盘，同一幂等键重试只重放原结果、不会重投，重投需换新键）；40005 IP+SendKey 限流（默认 60 条/分钟）；40006 投递状态未知（DELIVERY_UNKNOWN）；40007 幂等键非法"
          },
          "message": { "type": "string" },
          "data": {
            "description": "40001/40002/40003/40005/40007 时为 null；40004/40006 时含 pushid、readkey、readUrl、error（CHANNEL_FAILED/DELIVERY_UNKNOWN）、errno、delivery（failed/unknown），40004 与部分 40006 还含 results（各通道投递结果）"
          }
        },
        "example": {
          "code": 40004,
          "message": "通道投递失败：webhook",
          "data": {
            "pushid": "8f3ad2c1e5b7a9d0",
            "readkey": "AbCdEfGhIjKlMnOp",
            "error": "CHANNEL_FAILED",
            "errno": 40004,
            "delivery": "failed",
            "results": { "page": { "ok": true }, "webhook": { "ok": false, "status": 500, "error": "http_500" } }
          }
        }
      },
      "PushStatusOk": {
        "type": "object",
        "required": ["code", "message", "data"],
        "properties": {
          "code": { "type": "integer", "enum": [0] },
          "message": { "type": "string" },
          "data": {
            "type": "object",
            "required": ["pushid", "createdAt", "title", "delivery", "results", "readUrl"],
            "properties": {
              "pushid": { "type": "string" },
              "createdAt": { "type": "string", "format": "date-time" },
              "title": { "type": "string" },
              "delivery": { "type": "string", "enum": ["success", "failed", "pending", "unknown"] },
              "results": { "type": "object", "description": "各通道投递结果" },
              "readUrl": { "type": "string" },
              "desp": { "type": "string", "description": "消息正文，仅当查询参数 full=1 时返回" }
            }
          }
        }
      },
      "GenericError": {
        "type": "object",
        "required": ["code", "message"],
        "properties": {
          "code": { "type": "integer" },
          "message": { "type": "string" },
          "data": { "type": ["object", "null"] }
        }
      }
    },
    "responses": {
      "SendResult": {
        "description": "⚠️ 本服务所有业务错误也返回 HTTP 200：请依据 body 的 code 字段判断成败，不要依据 HTTP 状态码。code 为 0 才是成功；40001~40007 均为 HTTP 200 返回的业务错误（其中 40003 超每日配额也是 200，绝不能当成功处理）。",
        "content": {
          "application/json": {
            "schema": {
              "oneOf": [
                { "$ref": "#/components/schemas/SendSuccess" },
                { "$ref": "#/components/schemas/SendError" }
              ]
            }
          }
        }
      },
      "ProtocolError": {
        "description": "协议级错误（参数不完整、无法确定客户端地址、请求体超过 64 KB 等）",
        "content": {
          "application/json": {
            "schema": { "$ref": "#/components/schemas/GenericError" }
          }
        }
      }
    }
  }
}
