您的预订
中文
查找停车场

运营商 API · v1

把您的停车场推进来。把您的预订拉回去。

这是给要把自己的系统接到 Parkena 上的运营商看的参考文档。下面写到的每一样东西,都是已经部署好、正在应答的软件——包括那些会说不的部分。

在您围绕它规划项目之前,先读这一段。

这是一份完整而准确的参考文档。您运营商账户上的所有者或管理员,在控制台的 Account → API keys 下面签发和吊销密钥——secret 只显示一次。还没有开放的是其余部分:没有沙盒,我们一次只接一家运营商,所以在您围绕它规划开发之前,请先写信给我们。v1 拒绝做的事,都完整写在 §12 里,而不是留给您在第三周自己撞上。

1. 这是什么,又不是什么

这是 Parkena 运营商 API v1 的参考文档。它是“把库存放在 Parkena 控制台里经营”之外的另一条路:把您的停车场和您的价格推进来,把您的 Parkena 预订拉回去。两条路写的是同一批表、过的是同一套安全机制,而且哪一条都够不到另一家运营商的数据。您可以两条一起用——需要人来拿主意的事走控制台,每晚一次的同步走 API——它们不会互相打架。

它不是一件您可以无人值守接上去的产品。密钥由已登录的所有者或管理员在控制台里生成;没有沙盒供您练手,第一次对接是在我们这边有人陪着的情况下做出来的。这说的是 Parkena 眼下所处的阶段,而不是一条您可以插队绕过的队伍。

先把这个 API 不做的五件事讲在前面

预订是拉取的,用一个游标,而这个游标就是事实的来源。一个登记过的回调地址可以收到一条带签名的 booking.changed 提示,意思是“现在来拉取”——见 §13——但预订数据从不装进 webhook 里,这是刻意的;§12 写明了仍然没有的东西。

没有可用车位日历。封闭时段——见 §7——能把整段日期区间从售卖中撤下来,但您没法把“今晚还剩几个车位”作为数字推送,而那个看起来像它的字段——capacity——是总车位数。把可用量填在那里,既谎报了您的停车场,也会让它的上架审核每晚作废一次。

这个 API 上不走钱。没有扣款、没有退款、没有打款数据、没有佣金数字。

这里没有任何东西会创建、修改、取消一笔预订,也不会为预订办理入场或退款。既没有相应的权限范围,背后也没有相应的授权。

基础 URL 是 https://api.parkena.com/v1——见 §3。除此之外不该指向任何地方。

2. 动笔写第一行代码之前要弄对的两条规则

这是两件即便称职的对接也照样会搞错的事,因为这两种情况下,做错的那一种看上去都像是成功了。正因如此,它们被提到了本页最前面;它们之后的一切都是寻常的参考材料。

规则一:先比对差异,再发更新

当 Parkena 的审核人批准您的停车场信息时,他批准的是具体的内容。如果这些内容变了,原来那次批准就不再描述已经发布出去的东西,于是它作废,停车场退回重审——而且在有人重新批准之前,它在 parkena.com 上停售。

这是正确的行为;可是面对一台每晚把整批停车场重推一遍的机器,它同时也是一种让您每天凌晨 3 点被下架、永远如此的办法。所以这个 API 不会发出一条什么都没改的更新。两条写接口都会先读出当前这一行,逐字段比对,如果没有任何差别,它们干脆不发出任何语句——不是一条空转的 UPDATE;是一条语句都没有。您拿到的是 "result": "unchanged""changed": []

要得到这个行为,您什么都不用做。它不是一个开关,也没有什么请求头要发。每晚把一模一样的数据全量重推一遍,就是一次空操作,代价是每个停车场一次读取。

# an identical re-push of an approved car park
{ "result": "unchanged", "changed": [], "listing_review_superseded": false }
#
# the same car park, with one word of the name changed
{ "result": "updated",   "changed": ["name"], "listing_review_superseded": true }
已部署函数返回的响应,有删节。完整结构见 §7。

第二条响应不是一句您可以不理的提醒:那个停车场已经离开了 parkena.com,并且会一直不在上面,直到它重新获得批准。§8 列出了每一个会让您付出重审代价的字段,以及每一个不会的字段。

规则二:GET 返回的字段,PUT 不收

GET /lots 返回的每个停车场都带着它的 rates 结构,因为读的时候这才是有用的东西。PUT /lots/{external_id} 不收这个字段——价格是在 PUT /lots/{external_id}/rates 上设定的。所以那个看上去理所当然的循环,读出一个停车场、改掉一个字段、再放回去,在您把 rates 去掉之前一直会失败。顺手把 external_id 也去掉:它在路径里。

curl "$BASE/lots" -H "Authorization: Bearer $KEY.$SECRET" \
  | jq '.lots[0] | del(.rates, .external_id)' > lot.json
# edit lot.json
curl -X PUT "$BASE/lots/edge-main" -H "Authorization: Bearer $KEY.$SECRET" \
  -H 'Content-Type: application/json' --data @lot.json

rates 原样带回来,您得到的是 422 {"error":"field_belongs_to_another_route","field":"rates"}。我们选择拒绝它,而不是忽略它,这个分别就是全部要点:如果您把一个新价格放进停车场的请求体里,我们却回了 200,您完全有理由相信价格已经改了。而它并没有。

3. 基础 URL

本页上的每一个路径都相对于 https://api.parkena.com/v1。进出都是 JSON。

api.parkena.com 是 Parkena 自己拥有的一个主机名

它挡在真正应答的那个函数前面;就算它后面的东西换了,下面这些路径也不会变。即便如此,还是把基础地址放在配置里,而不要写死在源码里。

针对本仓库自己那套环境做本地开发时,同样的路径位于本地 Supabase 源下的 /functions/v1/operator-api/v1——路径相同,基础地址不同。

没有沙盒,也没有测试用的基础地址。上面那个就是线上的那个。至于一个请求作用在哪一家运营商身上,是从您出示的凭证解析出来的——绝不会来自 URL 或请求体里的任何东西。见 §4,以及 §12 里关于租户选择的那一条。

4. 认证

每个请求都带一个请求头:

Authorization: Bearer <key_id>.<secret>

key_idsecret 是同一份凭证的两半,用一个点连起来。第一个点把它们分开;再往后的点都属于 secret。

curl "$BASE/ping" \
  -H "Authorization: Bearer pk_api_3f9c….pk_sec_a71b…"
  • key_id —— pk_api_ 后面跟 32 个十六进制字符。这是公开的那一半。它按设计就会出现在我们的日志里,您可以放心把它写进配置文件,或者在支持请求里引用它。知道了它,也就相当于知道了一个用户名。
  • secret —— pk_sec_ 后面跟 64 个十六进制字符,也就是 256 位。我们不存它。我们存的是它的加盐 HMAC-SHA256,放在数据库里任何角色都读不到的两个列中。我们永远没法替您把它找回来。丢了就签发一份新凭证,再把旧的吊销。

没有 ?key= 这种查询串的退路,将来也不会有:URL 里的 secret,就是代理日志里的 secret、浏览器历史里的 secret、Referer 请求头里的 secret。

每一种认证失败都返回同一个 401

未知的 key_id、错误的 secret、已吊销的凭证、已过期的凭证、被停用的运营商账户、缺少某条接口所需权限范围的凭证,以及一个并不属于您名下停车场的 external_id,全都是 401 {"error":"unauthorized"}。它们故意做成无法区分——§9 说明了这样做换来了什么,又让您付出了什么。

要检查一份凭证,请用 GET /ping,不要从一个 401 里推断任何东西。

5. 签发、轮换和吊销一份凭证

凭证由您运营商账户上已登录的所有者或管理员生成——绝不会通过这个 API。一把能生成 API 密钥的 API 密钥,就是一把能给自己提权的密钥,所以这里没有任何一条接口会签发它。员工账号既不能签发,也不能吊销。

它背后的两个调用是 api_credential_issueapi_credential_revoke,通过 PostgREST、用一个已登录用户的令牌发起,而不是用 API 凭证。之所以把它们写在这里,是因为有工程团队的运营商会想直接驱动它们;而签发一把密钥的寻常办法,是控制台里 Account → API keys 下面那个页面。

签发

curl -X POST '<project rest url>/rpc/api_credential_issue' \
  -H "apikey: <anon key>" \
  -H "Authorization: Bearer <a signed-in owner or manager’s JWT>" \
  -H 'Content-Type: application/json' \
  -d '{
        "p_label": "nightly sync",
        "p_scopes": ["read_supply", "write_supply", "read_bookings"],
        "p_expires_in_days": 365
      }'
参数类型说明
p_labelstring,1–80 个字符半年之后您会怎么称呼这把密钥。必填。
p_scopes权限范围数组一组互不重复、且不为空的权限范围。必填。
p_expires_in_daysinteger 1–3650,或 nullnull 表示永不过期。选填。
[{
  "credential_id": "01a01a73-a650-710e-9be1-08ffc77b4696",
  "key_id":        "pk_api_3f9c…",
  "secret":        "pk_sec_a71b…",
  "label":         "nightly sync",
  "scopes":        ["read_supply", "write_supply", "read_bookings"],
  "created_at":    "2026-08-19T14:22:07.113904Z",
  "expires_at":    "2027-08-19T14:22:07.113904Z"
}]
响应——也是 secret 唯一一次被返回。

没有 p_expires_at,将来也不会有。过期是一个天数,由服务器按它自己的时钟去换算;这个 API 在任何一条接口上都不接受调用方给出的时间戳。

轮换

没有轮换这个调用,因为零停机的轮换不过就是按正确顺序发出的两个调用:

  1. 用同一组权限范围签发第二份凭证。
  2. 把它部署到您的系统里,确认流量确实走通了——用新密钥调一次 GET /ping,然后盯着它上面的 last_used_at
  3. 把旧的那一份吊销。

从第一步到第三步之间,两份凭证都是有效的。没有任何限制拦着您同时持有两份。

吊销

curl -X POST '<project rest url>/rpc/api_credential_revoke' \
  -H "apikey: <anon key>" \
  -H "Authorization: Bearer <a signed-in owner or manager’s JWT>" \
  -H 'Content-Type: application/json' \
  -d '{"p_credential_id": "01a01a73-a650-710e-9be1-08ffc77b4696"}'

吊销是立即且永久的。被吊销的凭证不会被恢复——它只会被替换。吊销两次会返回同一行,带着最初那个 revoked_at,因为一把密钥不会死两次。一个并不属于您的 id 返回 [],和一个从来不存在的 id 完全一样。

6. 权限范围

一份凭证带着一组权限范围。一共有三个:

权限范围它允许做什么
read_supply列出您的停车场、它们的 Parkena 价格区间,以及每个停车场的封闭时段——包括每段时段上的汇总计数 overlapping_bookings。只是一个计数,绝不是一笔预订:没有编号、没有姓名、没有车牌跟着它。
write_supply创建和修改停车场、它们的 Parkena 价格,以及它们的封闭时段清单。
read_bookings拉取您的 Parkena 预订。

需要多少就给多少。一个只推送库存的同步任务不需要 read_bookings;一个只拉取预订的报表任务绝不该持有 write_supply

权限范围的检查不是建议性的。它是同一个数据库调用的一个参数,而正是这个调用确定了这次请求可以碰哪一家运营商的行,所以没有哪条通往您数据的路径能绕开它。缺少某条接口所需权限范围的凭证,得到的是那个统一的 401——和一个错误的 secret 得到的答复一模一样。

这里故意没有 write_bookings。Parkena 里没有任何东西允许一台机器创建、修改或取消一笔预订,所以一个以这项能力命名的权限范围,在您读来像是一道边界,实际却什么也不是。

7. 接口

方法路径需要的权限范围
GET/ping任意有效凭证
GET/lotsread_supply
PUT/lots/{external_id}write_supply
PUT/lots/{external_id}/rateswrite_supply
GET/lots/{external_id}/blocked-periodsread_supply
PUT/lots/{external_id}/blocked-periodswrite_supply
GET/bookingsread_bookings

{external_id} 是您自己给这个停车场起的标识——您的系统管它叫什么,它就是什么。它对我们是不透明的:我们从不解析它,它也不必是一个 UUID。如果它里面有 / 或空格,请做百分号编码。Parkena 内部的 id 从不发给您,也从不接受您发来。

未知的查询参数会被拒绝,而不是被忽略,每一条接口都是如此。一个打错了又被悄悄丢掉的参数,就是一个您以为生效、其实没有生效的筛选条件。

GET /ping

检查一份凭证,并告诉您它可以做什么。对接不通的时候,就用这条接口——这个 API 上其他任何一次拒绝,都被刻意做成没法告诉您六件事里出的是哪一件。它不建立任何运营商上下文,也不读任何一行。

curl "$BASE/ping" -H "Authorization: Bearer $KEY.$SECRET"
#
{
  "ok": true,
  "key_id": "pk_api_3f9c…",
  "scopes": ["read_supply", "write_supply", "read_bookings"],
  "server_time": "2026-08-19T15:12:09.870151Z"
}

server_time 是我们的时钟,UTC。它只作参考——您永远不会把时间发回给我们。

GET /lots

您的停车场,每一个都带着它的 Parkena 价格区间。

查询参数类型默认值说明
afterstring从这个 external_id 之后接着取。用上一页给的 next_after
limitinteger 1–20050要得更多会被拒绝,不会被悄悄调小。

分页走的是您自己的 external_id,按升序——不是偏移量,所以并发写入不会让分页边界移位。

curl "$BASE/lots?limit=50" -H "Authorization: Bearer $KEY.$SECRET"
#
{
  "lots": [
    {
      "external_id": "LOT-1",
      "name": "Terminal Park",
      "timezone": "Europe/Berlin",
      "handover": "self",
      "online_bookable": true,
      "capacity": 250,
      "latitude": "52.520000",
      "longitude": "13.405000",
      "city": "Berlin",
      "country_code": "DE",
      "features": ["indoor", "valet"],
      "arrival": null,
      "media": null,
      "min_advance_days": 1,
      "min_stay_days": 1,
      "max_stay_days": 60,
      "cancellation": {
        "free_until_hours_before_check_in": 48,
        "penalty_percent_after": "50.00",
        "no_show_forfeits_full": true
      },
      "rates": {
        "channel": "parkena",
        "currency": "EUR",
        "base_price": "12.50",
        "floor_price": "9.00",
        "ceiling_price": "18.00",
        "min_first_day_price": "11.00",
        "dynamic": false,
        "valid_from": null,
        "valid_to": null
      }
    }
  ],
  "next_after": null
}

next_after 只在这一页装满了的时候才出现。一直跟着它走,直到它是 null,您就把名下整批停车场不多不少地看过了一遍。对于还没有 Parkena 价格的停车场,ratesnull

PUT /lots/{external_id}

创建或更新一个停车场。一个请求,一个停车场——没有批量接口,而接口本身的形状就是强制这一点的东西。数组形式的请求体会被 too_many_lots 拒掉。

PUT 是一份完整表述。少一个键的意思是 null,不是“保持原样”。每次都把整个停车场发过来。另一种做法会让您没办法清空一个字段,还会把一个打错的键变成永久而无声的空操作。

字段类型必填说明
external_idstring如果带了,它必须和路径里的那个相同。会被检查,但不会被使用。
namestring,≤200
timezonestringIANA 名称,例如 Europe/Berlin。每一次跨天边界的计算都要经它来解。
handoverstringselfattended
online_bookableboolean您的销售开关。之所以必填,正是因为给它一个默认值,会在第一次不完整的推送时把一个正在售卖的停车场下架。
capacityinteger 0–1000000总车位数。不是今晚空着的车位——见下面的警告。
latitudenumber 或 string四舍五入到小数点后 6 位。必须和 longitude 一起发。
longitudenumber 或 string四舍五入到小数点后 6 位。必须和 latitude 一起发。
citystring,≤120
country_codestringISO 3166-1 alpha-2,例如 DE
featuresstring 数组见下面的清单。顺序无所谓——我们会替它们排好序。
arrivalobject自由格式的到达指引。
mediaobject自由格式的媒体引用。
min_advance_daysinteger 0–365默认为 0
min_stay_daysinteger 1–365
max_stay_daysinteger 1–365三个都止步于 365,因为那就是 Parkena 为一次停留报价所能覆盖的最远期限。更大的值会被接受,但永远不会被兑现。
cancellationobject 或 null三个键要么都给,要么都不给——见下文。

features 接受:indoorsecuritycamerasgate_automationplate_recognitionev_chargingdisabled_accessoversize_vehiclevalet

cancellation 是一个嵌套对象,而且是全有或全无:

"cancellation": {
  "free_until_hours_before_check_in": 48,
  "penalty_percent_after": "50.00",
  "no_show_forfeits_full": true
}

三个里只给一个或两个,是 422 incomplete_cancellation_policy。一份有免费取消窗口、却没说清罚金的政策,不是一份只知道一半的政策,而是一个无法回答的退款问题。想表达“没有政策”,请发 null 或者干脆不带这个键。

capacity 是总车位数,不是可用车位

如果您每晚的数据推送把“今晚还剩几个车位”塞进 capacity,那么您既是在告诉 Parkena 您的停车场缩水了,又会让您的上架审核每一个晚上都作废一次,因为 capacity 属于要审核的内容。

实时可用车位是另一套机制,不属于 v1。这个 API 察觉不到这个错误,因为数字就是数字。

curl -X PUT "$BASE/lots/LOT-1" \
  -H "Authorization: Bearer $KEY.$SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
        "name": "Terminal Park",
        "timezone": "Europe/Berlin",
        "handover": "self",
        "online_bookable": true,
        "capacity": 250,
        "latitude": 52.5200,
        "longitude": 13.4050,
        "city": "Berlin",
        "country_code": "DE",
        "features": ["valet", "indoor"],
        "min_advance_days": 1,
        "min_stay_days": 1,
        "max_stay_days": 60,
        "cancellation": {
          "free_until_hours_before_check_in": 48,
          "penalty_percent_after": "50.00",
          "no_show_forfeits_full": true
        }
      }'
一次完整的推送。
{
  "external_id": "LOT-1",
  "result": "created",
  "changed": ["name", "timezone", "handover", "…"],
  "listing_review_superseded": false,
  "lot": { "…": "the car park as it now stands" }
}
停车场是新建出来的时候是 201,其他情况是 200
字段含义
resultcreatedupdatedunchanged
changed真正有差别的那些字段名。unchanged 时为空。
listing_review_superseded如果这次写入让您的 Parkena 上架审核作废,就是 true——见 §8。
lot存下来的停车场,回读出来的样子。

result: "unchanged" 的意思是一条语句都没有发出——没有行锁、没有写入、没有触发器。这是每晚重推一遍的正常且预期的结果,也正是它让这个 API 不会每晚把您下架。

PUT /lots/{external_id}/rates

为一个停车场设定 Parkena 价格区间。价目方案不是一个价格——它是您愿意在 Parkena 渠道上成交的那个区间:一个下限、一个上限,以及两者之间的一个基准价。

这条接口只写 parkena 这一个渠道。您自己的直销定价是您自己的,这个 API 碰不到它。

请求体接受两种形状。一个区间:

{
  "floor_price": "9.00",
  "base_price": "12.50",
  "ceiling_price": "18.00",
  "min_first_day_price": "11.00",
  "dynamic": false
}

或者一个固定价格,它会展开成 下限 = 基准价 = 上限:

{ "fixed_price": "12.50" }
字段类型必填说明
fixed_pricedecimal两种形状二选一不能和区间字段一起用。
floor_pricedecimal随区间一起必须 ≤ base_price
base_pricedecimal随区间一起必须落在下限和上限之间。
ceiling_pricedecimal随区间一起必须 ≥ base_price
min_first_day_pricedecimal 或 null必须落在区间之内。
currencystringISO 4217。默认取您的结算货币,而且不能是别的。
dynamicboolean默认为 false
valid_fromYYYY-MM-DD 或 null一个日历日期。时间戳会被拒绝,不会被截断。
valid_toYYYY-MM-DD 或 null不得早于 valid_from

钱是字符串,而且是被拒绝,不是被四舍五入。请发 "12.50",不要发 12.345。金额带两位小数;出现第三位就是 422 invalid_body,因为一个不是您亲手写下的价格,不是一个您同意了的价格。坐标正相反——它们是测量值,所以会被四舍五入。

同时发 fixed_price 和一个区间,是 422。去猜您到底想说哪一个,正是一个停车场最后被定在自己区间错误那一端的原因。

{
  "external_id": "LOT-1",
  "result": "updated",
  "changed": ["floor_price"],
  "envelope_widened": true,
  "rates": {
    "channel": "parkena",
    "currency": "EUR",
    "base_price": "12.50",
    "floor_price": "7.00",
    "ceiling_price": "18.00",
    "min_first_day_price": "11.00",
    "dynamic": false,
    "valid_from": null,
    "valid_to": null
  }
}
新建时是 201,其他情况是 200

envelope_widened 在这次写入把区间往外推的时候是 true——下限往下、上限往上、货币改了,或者 dynamic 翻了——以及此前根本没有 Parkena 价格区间的时候。往外放宽,正是可能让您付出一次上架审核代价的那件事。

这是一个保守的信号,与其把它说过头,我们宁可把话讲明白:我们比对的是上一刻还生效的那个区间,而不是审核人当初批准的那个,因为这个 API 刻意读不到您的审核状态。所以在什么都没有作废的情况下,它也可能报 true。但在确实有东西作废的时候,它不会报 false

GET /lots/{external_id}/blocked-periods

这个停车场的封闭清单:它在 Parkena 上被撤出售卖的每一段日期区间,不论是谁写下的——您的同步和控制台写的是同一份清单。一段封闭时段只拦住停留碰到它的新 Parkena 销售,除此之外什么都不做:它不取消任何东西,也碰不到您自己的直销。两个日期都含当天——ends_on 是最后一个被封闭的日子,不是它的后一天;一段 starts_on 等于 ends_on 的时段,封的恰好就是那一天。

curl "$BASE/lots/LOT-1/blocked-periods" -H "Authorization: Bearer $KEY.$SECRET"
#
{
  "external_id": "LOT-1",
  "blocked_periods": [
    { "starts_on": "2026-11-02",
      "ends_on":   "2026-11-08",
      "reason":    "resurfacing",
      "overlapping_bookings": 2 },
    { "starts_on": "2026-12-24",
      "ends_on":   "2026-12-26",
      "reason":    null,
      "overlapping_bookings": 0 }
  ]
}
时段按最早的在前返回。reason 是您自己的标注,没写过就是 null

overlapping_bookings 是一个透明度计数:这个停车场有多少笔预订——每个渠道、除 cancelled 之外的每种状态——的停留碰到了这段时段。两头的比较都含当天,按停车场自己的日历日来算,所以一笔在封闭首日早上取车离场的预订照样算数,一笔在封闭末日傍晚入场的也一样。请注意“除 cancelled 之外”包括了什么:一次已经完成的停留同样算数。这个计数回答的是“这些日期里卖出去过什么”,历史也算在内——它不是一次封闭会困住多少辆将要到来的车,所以它可能比控制台的警告面板读起来更高,后者问的是那个更窄的问题。它是一个计数,仅此而已:响应里没有任何一笔预订的编号、姓名、车牌或日期。

PUT /lots/{external_id}/blocked-periods

用请求体里的清单,替换这个停车场的整份封闭清单。没有“加一段时段”的调用,也没有指名单独一段时段的办法——一个只会合并的同步,就是一个永远不会删除的同步,一段在您系统里已经解除的封闭会永远留在 Parkena 上。至多 100 条;每个日期都得是 2020-01-01..2032-12-31 之内真实存在的日历日;ends_on 绝不早于 starts_on;任何两条都不得重叠——含当天,所以共享一天的两条就是相撞。

您发来的清单,就是存在的那份清单

这里的 PUT 是一份完整表述,和 PUT /lots/{external_id} 同一条规则——而在这条接口上,这条规则有一个值得加重语气写出来的后果:在控制台里录入的封闭,属于同一份清单。如果有人周二在控制台里敲进一段封闭,而您的每晚同步周二夜里只推送它自己的那些时段,这次同步就会把那个人的封闭移除——无声地、并且是正确地,因为您告诉过我们,您发来的清单就是完整的清单。

一台占有这条接口的机器,就占有整本日历。要么把这条接口读回来,把控制台录入的封闭一并放进您自己的系统,要么和您自己的员工讲定封闭归哪个系统管。API 不会来当裁判。

curl -X PUT "$BASE/lots/LOT-1/blocked-periods" \
  -H "Authorization: Bearer $KEY.$SECRET" \
  -H 'Content-Type: application/json' \
  -d '{
        "blocked_periods": [
          { "starts_on": "2026-11-02", "ends_on": "2026-11-08", "reason": "resurfacing" },
          { "starts_on": "2026-12-24", "ends_on": "2026-12-26" }
        ]
      }'
一次整份替换。第二段时段没带 reason,这是允许的。
字段类型必填说明
starts_onYYYY-MM-DD第一个被封闭的日子,含当天。一个日历日期——时间戳会被拒绝,不会被截断。
ends_onYYYY-MM-DD最后一个被封闭的日子,含当天——不是它的后一天。不得早于 starts_on
reasonstring ≤200,或 null一条标注,会显示在控制台里。空白会被拒绝;null 和不带这个键都表示“没有原因”。原样存储,从不修剪——改了一个空格,就是改了一段时段。

这条接口上每一次内容层面的拒绝都是同一个码,422 {"error":"invalid_blocked_periods"},由 field 用您自己请求体的坐标指名出错的那一条——blocked_periods[3].ends_on。两种最外层的错误保留这个 API 其余部分给它们的码:多出来的顶层键是 unknown_field,缺了 blocked_periods 这个键是 missing_field

错在哪里`field`
超过 100 条,或者 blocked_periods 不是一个数组blocked_periods
不是一个真实存在的日历日(2026-02-30)、是一个时间戳,或者落在 2020..2032 之外那一条的 starts_on / ends_on
ends_on 早于 starts_on那一条的 ends_on
两条互相重叠(含当天——共享一天就算)起始较晚那一条的 starts_on
reason 是空白、不是字符串,或者超过 200 个字符那一条的 reason

与已售出预订重叠的时段不会被拒绝。一次机器同步不该卡在一辆上周就卖出去的车上;那些预订照旧成立——封闭只拦住新的销售。作为替代,响应带上每段时段的 overlapping_bookings 计数(语义见上文,已完成的停留也算在内),好让您的系统、以及系统背后的人,确切看到哪些封闭里已经有卖进去的车。

{
  "external_id": "LOT-1",
  "result": "updated",
  "added": 1,
  "removed": 1,
  "blocked_periods": [
    { "starts_on": "2026-11-02",
      "ends_on":   "2026-11-08",
      "reason":    "resurfacing",
      "overlapping_bookings": 2 },
    { "starts_on": "2026-12-24",
      "ends_on":   "2026-12-26",
      "reason":    null,
      "overlapping_bookings": 0 }
  ]
}
停车场此前一段时段都没有的时候是 201,其他情况是 200
字段含义
resultcreated(原本一段都没有,现在有了)、updated(其余任何发生了写入的情况——包括一次把清单清空的 PUT []),或 unchanged
added / removed这次写入实际挪动的行数。unchanged 时两个都是 0
blocked_periods存下来的清单,在写入之后重新读出,带着新算的 overlapping_bookings 计数——是数据库现在实际持有的东西,绝不是把您的请求体回显一遍。

§2 那条“先比对差异,再发更新”的规则,在这里以清单的形式照样成立:result: "unchanged" 的意思是,存着的清单和您的请求体本来就说着同一件事,于是一条语句都没有发出。每晚重推一份没变的封闭清单,代价是一次读取。

GET /bookings

您的 Parkena 预订,以一个拉取游标的形式给出。这个游标就是事实的来源:一个登记过的 webhook 回调地址可以收到一条带签名的 booking.changed 提示,让您早一点来拉取——见 §13——但提示不携带任何预订数据,而您在这个游标上建起来的东西,一样也不会白费。

查询参数类型默认值说明
sincestring您上一次调用返回的那个 next 令牌。想“从头开始”就不要带它。
limitinteger 1–500200要得更多会被拒绝,不会被悄悄调小。
curl "$BASE/bookings?since=$CURSOR" -H "Authorization: Bearer $KEY.$SECRET"
#
{
  "bookings": [
    {
      "reference": "PK0000000040",
      "external_reference": "OPS-9911",
      "lot_external_id": "LOT-1",
      "lot_name": "Terminal Park",
      "timezone": "Europe/Berlin",
      "check_in_local": "2026-09-01T08:00:00",
      "check_out_local": "2026-09-05T19:30:00",
      "parking_days": 5,
      "status": "confirmed",
      "payment_status": "paid",
      "currency": "EUR",
      "total": "56.00",
      "channel": "direct",
      "flight_number": "LH401",
      "customer": {
        "first_name": "Ada",
        "last_name": "Lovelace",
        "email": "[email protected]",
        "phone": "+49301234567"
      },
      "vehicle_plates": ["BXY4242"],
      "created_at": "2026-08-19T15:18:01.099213Z",
      "cancelled_at": null
    }
  ],
  "next": "eyJ2IjoxLCJ0IjoiMjAyNi0wOC0xOVQxNToxMzowOC44MjQxMDhaIiwi…",
  "has_more": false
}
字段说明
referenceParkena 的预订编号。这是去重用的键。
external_reference您自己的那个字符串,如果当初设过的话。原样回传,我们绝不拿它当键。
check_in_local / check_out_local停车场自己的当地时钟,不带任何时区偏移。请按 timezone 去读它们。
total一个十进制字符串,不是浮点数。
statuspendingconfirmedchecked_incompletedcancelled
payment_statuspendingpaidpartialrefundedexpirednot_required

怎样正确地轮询

cursor = load_saved_cursor()          # null on the first run
loop:
    r = GET /bookings?since=<cursor>&limit=200
    for b in r.bookings:
        upsert_into_your_system(b, key = b.reference)   # NOT external_reference
    cursor = r.next
    save_cursor(cursor)
    if not r.has_more: sleep(60)

三条规则,每一条都是承重的:

  1. reference 去重。投递是至少一次,绝不是恰好一次。您会不止一次看到同一笔预订,那是正确的行为,不是故障。
  2. 在您把这一批稳妥存好之后再保存 next,不要提前保存。如果您的进程在一批中途挂了,没保存的游标会把它重投一遍——而第 1 条规则让这件事无害。
  3. 不要自己造游标。它是我们签发的一个令牌。不存在 ?since=<an instant I chose> 这种用法,这个缺失是有意的:一个填了未来某个时刻的合作方,会悄无声息地再也收不到自己的预订,而第一个症状会是一辆车停在道闸前,而他们的系统从没听说过它。超出我们水位线的游标会被夹回到水位线上,所以一个被篡改的令牌,最多也只能把行重投一遍。

您为什么可能马上又看到同一笔预订:我们交回给您的游标,永远不会指到 now() − 5 minutes 之后。比这更新的预订照样会返回给您——您要的是现在就拿到预订,而不是五分钟以后——我们只是不把游标提交到它们身上。这不是为谨慎而谨慎。一个数据库事务是按它开始的时间打时间戳的,所以一个慢事务提交出来的行,时间戳可能早于一行您已经拿到手的记录;没有这段滞后,一个直接跳到最新那一行的游标,会永久而无声地把它跨过去。

8. 什么会让您付出一次重新审核

§2 给的是规则;这里是它背后的字段清单。在 PUT /lots/{external_id} 上改动下面任何一个,都会让待审或已批准的停车场信息作废,而响应会用 "listing_review_superseded": true 明说这一点:

name · timezone · handover · capacity · latitude · longitude · city · country_code · features · arrival · media · min_advance_days · min_stay_days · max_stay_days · cancellation_free_until_hours_before_check_in · cancellation_penalty_percent_after · cancellation_no_show_forfeits_full

什么不会

  • online_bookable。它是您的销售开关,不是要审核的内容。把它关掉会立刻让停车场停售,而不让任何东西作废——这也正是它在每一次 PUT 上都是必填字段的原因。
  • 价格接口上任何没有把区间往外推的改动。抬高您的下限或者压低您的上限,是待在一个您已经许下的承诺之内。压低下限、抬高上限、改货币,或者翻转 dynamic,则是许下一个新的承诺,可能让审核作废——envelope_widened 会告诉您是什么时候。
  • valid_from / valid_to。到期和续期是实时读取的,所以改动一个有效窗口,会把停车场从 parkena.com 上撤下来、再放回去,不经过人。

两个值得知道的坑

  • 特征的顺序对您无所谓,但它曾经对我们有所谓。我们在存储之前会给 features 排序,所以 ["valet","indoor"]["indoor","valet"] 是同一次推送。您不需要自己排。
  • 坐标精度无所谓。我们在比对之前会四舍五入到小数点后六位,所以每晚发 52.5200001 并不会让 latitude 永远被报成有改动。

没有删除

这个 API 删不掉一个停车场,也删不掉一个价格,而且没有任何授权会让它做到。在数据接入的代码里,先 deleteinsert 是最常见的 upsert 写法,而放到这批数据上,它是灾难性的:停车场会拿到一个新身份,并且把它的上架历史、它的价格和它的可用性一起带走——出自一个没有人在盯的任务。移除一个停车场,是人在控制台里刻意去做的事,那里的确认框会把随之一起消失的东西一一列出来。一个曾经收过预订的停车场根本不能被移除,谁都不行:预订是一份财务记录,它要被保留,而当初为它收下这笔预订的停车场也一样。要让一个停车场停售,请设 "online_bookable": false

9. 错误

每一个错误都是 JSON,带一个稳定的、机器可读的 error 码:

{ "error": "invalid_price_envelope", "field": "floor_price" }

field 出现的时候,是您自己的字段名,原样回传。我们绝不返回数据库消息、约束名或表名——那些是我们自己的东西,我们会改名,而您的对接不该因为我们改名就坏掉。请按 error 来分支。

凭证

状态码错误码含义
401unauthorized见下文——它涵盖六种不同的情形,并且拒绝说出是哪一种。

请求的形状

状态码错误码含义
404not_found没有这条路由。绝不用来表示某个资源不存在。
405method_not_allowed路径对,动词不对。Allow 响应头会写明我们要的动词。
413payload_too_large请求体超过 64 KiB。
415unsupported_media_typeContent-Type 不是 application/json
422invalid_json请求体不是可解析的 JSON。
422invalid_bodyJSON 本身没毛病,但形状不对,或者某个值没法用。
422missing_field缺了一个必填字段。
422unknown_field一个我们不认识的字段。被拒绝,不是被忽略。
422field_belongs_to_another_route一个确实存在、但要在别处设定的字段。今天唯一的一个是 rates——见 §2。
422invalid_query一个错误的或未知的查询参数。
422invalid_cursor那个 since 令牌不是我们发出去的。
422too_many_lots数组形式的请求体。一个请求,一个停车场。

内容

状态码错误码含义
422invalid_timezone不是一个 IANA 时区名称。
422invalid_coordinates超出范围,或者只给了纬度没给经度。
422invalid_country_code不是 ISO 3166-1 alpha-2。
422invalid_stay_boundsmin_stay_days / max_stay_days 互相矛盾。
422incomplete_cancellation_policy取消政策的三个键只给了一个或两个。
422invalid_price_envelope下限、基准价和上限的顺序不对。
422invalid_first_day_pricemin_first_day_price 落在区间之外。
422invalid_validity_windowvalid_to 早于 valid_from
422currency_is_not_settlement_currency不是您的结算货币。
422invalid_blocked_periods一条封闭时段条目没法用;field 会用您自己请求体的坐标把它指名出来(blocked_periods[3].ends_on)。拒绝清单的表格在 §7。
409conflict一次真正的竞态:两次推送同时创建了同一个 external_id。重试即可。

我们这边的

状态码错误码含义
429rate_limited超过了某项限制。请遵守 Retry-After
503try_again一次短暂的数据库冲突。重试;Retry-After 会带上。
500internal_error我们的错。它完整地记在我们的日志里。重试是合理的。

为什么那个 401 不肯多说

unauthorized 涵盖下面全部情形,并且拒绝把它们区分开:

  1. 没有 Authorization 请求头,或者有一个我们解析不了的。
  2. 一个不对应任何凭证的 key_id
  3. 一个确实存在的 key_id,配了错误的 secret。
  4. 一份已吊销、已过期,或者所属运营商账户已被停用的凭证。
  5. 一份有效的凭证,但缺少这条接口所要求的权限范围。
  6. 一份有效的凭证,指名了一个并不属于它名下停车场的 external_id

第 2 到第 5 种情形,即便在我们自己的进程内部也无法区分——数据库对它们的应答完全一致,做的功也一样,这样就没人能把运营商名单枚举出来,也没人能摸清一把被盗密钥上还有哪些能力仍然有效。

第 6 种情形让您少了一点舒服,我们把原因讲出来,而不是只丢下一句断言:一份只持有 write_supply 的凭证,是列不出停车场的。如果一个未知的 external_id 会回 lot_not_found,那这份凭证就正好拿到了一个能用的枚举工具,可以摸清它的权限范围本来不让它看的那批停车场——而这个工具是用一次拒绝造出来的。所以它拿到的是同一个 401。

GET /ping 就是这件事的答案。它告诉您密钥是活的,以及它可以做什么,用不着您对着一个 401 去猜。如果 ping 成功而某条接口回了 401,那么您面对的要么是一个您并不持有的权限范围,要么是一个不属于您的 external_id——请对着 GET /lots 把两样都核一遍。

10. 速率限制、大小限制,以及我们记录什么

限制项取值
请求体64 KiB
每个请求的停车场数1
GET /lots 每页1–200,默认 50
GET /bookings 每页1–500,默认 200
每次 PUT …/blocked-periods 的封闭时段数100——一份清单,一个停车场

速率限制是令牌桶——一份会持续回填的突发额度。

计在谁头上突发额度回填
全部请求来源地址2404 次 / 秒
失败的认证来源地址20每 3 秒 1 次
凭证1202 次 / 秒
写(突发)凭证601 次 / 秒
写(每小时)凭证10001000 次 / 小时

被拒绝时是 429,带一个以整秒为单位的 Retry-After 响应头。被拒绝并不消耗令牌,所以重试不会把您自己的恢复推得更远。

写被限了两道,而每小时那一道才是要紧的。一把外泄的写密钥把一整批停车场清空,看上去和一次正当的每晚同步一模一样——同一份凭证、同一条接口、同一种形状、同一个深夜时段——所以光靠突发限制分辨不出来,因为真正的同步本身也是一次突发。每小时的上限,限定了一把被盗密钥在有人可能看上一眼之前,最多能改写掉多大一批停车场。一家有 1000 个停车场、每晚各推一次的运营商,装得进这个上限;如果您的规模更大,跟我们说一声,我们会把它调高,而不是让您绕着它走。

封闭时段的两条接口花的是和其他一切相同的桶:GET 花一个读令牌,PUT 是一次写入,两个写入桶都要计——一次每晚的封闭同步,和您的停车场推送一样计入同一个每小时 1000 次的上限。这是刻意的,因为一把外泄的密钥把整批停车场撤出售卖,恰恰就是这个上限存在要去限住的那种形状。

失败的认证计在来源地址头上,绝不计在出示的那个 key_id 头上。计在密钥头上会变成一次冲着您来的拒绝服务:key_id 是不保密的那一半,按设计就会出现在日志和配置文件里,所以任何读到它的人,用几十个错误的 secret 就能把您锁在自己的对接之外。

请注意,一个未知的 external_id 同样会消耗失败额度,因为它返回的是和其他一切一样的 401。一个把这两者区别对待的限流器,等于用 429 造出了一个探测存在与否的工具。如果您手上的映射关系过期了,请对着 GET /lots 去对账,而不是去试探。

我们记录什么

每一个请求都会在我们这边产生一行日志;每一次真的改了东西的写入还会再产生一行,带着发生变化的那些字段名,以及这次改动有没有让您付出一次审核。只有字段名,绝没有值——这行日志回答的是“昨晚这批停车场上发生了什么”,您的价格不在里面。您的 key_id 在里面,这也是您可以来问我们、究竟是您哪一个对接做了什么的依据。您的 secret 无论以任何形式都不在里面。

11. 在您的停车场能卖出去之前

通过这个 API 推送一个停车场,会把它建出来,但一个新的停车场并不会在 API 返回 201 的那一刻就在 parkena.com 上开卖。要求里有一部分是这个 API 能提供的内容,另一部分是需要人在控制台里确认的决定。

一次完整的 PUT /lots/{external_id} 加上一次 PUT …/rates,就满足了容量、价格、地理位置、城市 / 国家和取消政策这几项要求。仍然欠着、并且只能在控制台里做的是:

  • 确认时区和预订规则——它们是推导出来并带了默认值的,而一个看着合理的错误答案会悄无声息地把预订算错价,所以由人来确认一次。
  • 到达指引和一张主图,给公开的停车场信息用。
  • 接受 Parkena 上架协议,整个账户接受一次。
  • 把停车场提交审核。

最后这一条是刻意的:把一个停车场提交给人来审核,是您就自己的生意做出的一项声明,而一台握着密钥的机器不该能代您做出它。

控制台会显示每一项要求、它是否已经满足,以及它保护的是什么。这不是我们打算在 v1 里去掉的限制。

12. v1 不做什么

直说,因为一个建立在我们从未做出的假设之上的对接,比一个建立在写明了的空白之上的对接更糟。

  • 没有实时可用车位数字。现在您可以用 PUT /lots/{external_id}/blocked-periods 把整段日期区间撤出售卖——见 §7——但仍然没有办法把“今晚还剩几个车位”作为数字推送,而 capacity 是总车位数——把实时可用量放进去,会谎报您的停车场,并且让它每晚被重新审核一次。按数字计的可用车位日历不在 v1 里。
  • webhook 里不装数据。一个登记过的回调地址收到的是 §13 那条带签名的 booking.changed 提示——一个预订编号,别无其他——而拉取游标仍然是事实的来源,和这份清单在提示存在之前许下的承诺一字不差。仍然没有的是:按事件装数据的载荷(预订数据从不装进 webhook 里)、顺序保证(提示会归并、会重试;先后次序是游标的事),以及重放 API(没有什么可重放——把游标再拉一遍就是)。一个对接必须在提示全关的情况下照样工作,因为一条五次投递都失败的提示会安静地死掉,而且是故意如此。
  • 不写预订。您没法通过这个 API 创建、修改、取消一笔预订,也没法为它办理入场或退款。既没有相应的权限范围,背后也没有相应的授权。
  • 没有支付。没有扣款、没有退款、没有打款数据、没有佣金数字。预订数据流里带的是这笔交易的总价和货币,关于钱怎么走的一个字都没有。
  • 不做 OTA 或渠道管理器分销。这个 API 只写 parkena 这一个渠道。它不是渠道管理器,也不会推给别的任何人。
  • 没有删除。见 §8。
  • 没有批量接口。一个请求,一个停车场。
  • 没有提交上架和审核状态。您没法通过 API 提交审核,也读不到自己的审核状态。listing_review_supersededenvelope_widened 是仅有的两个跟审核沾边的信号,而后者是刻意保守的。
  • 没有租户选择。这个 API 解析的任何请求体或查询串里,都没有 tenant_id。一个请求作用在哪一家运营商身上,只从凭证解析出来,别无他处——一个由请求给出的运营商 id,就等于一把跨租户的写入密钥。
  • 任何地方都不接受调用方给出的时间戳。没有 as_of,没有 watermark,没有 updated_since。您可以发的日期只有 valid_fromvalid_to,它们是您就自己的价格所声明的日历日。其余一切都按我们的时钟。

13. Webhook:`booking.changed` 提示

每个运营商账户可以登记一个 HTTPS 回调地址,每当您的一笔预订被创建或发生变化——任何渠道、任何字段——我们都会向它 POST 一条带签名的提示。在围绕它设计任何东西之前,先读下面这条警告。

提示不是数据。游标才是数据。

一条提示的整个请求体,就是一个事件名、一个预订编号和一个时间戳。没有状态、没有日期、没有旅客、没有金额——没有任何您的系统能直接据以行动的东西,也没有任何会在传输途中变陈旧的东西。对一条提示唯一正确的回应,就是您的对接本来就在做的那件事:带着存好的游标去拉 GET /bookings

一个回调地址宕了一整天的合作方,丢的是延迟,绝不是数据——下一次轮询时,游标会把一切重新投递一遍。如果您的对接在提示全关的情况下活不下去,那它就是建错了。

正是这种分工,让 §12 不再写着“没有 webhook”:我们当初拒绝发布的,是一个装着预订本身的 webhook,因为一个悄无声息失败的 webhook,就是一笔您从没听说过、可车照样开到您道闸前的预订。而一条提示可以悄无声息地失败,不让您损失分毫。

投递

POST /your/endpoint HTTP/1.1
Content-Type: application/json
User-Agent: Parkena-Webhooks/1
Parkena-Signature: t=1767139200,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
#
{"event":"booking.changed","booking_reference":"PK0000000040","occurred_at":"2026-08-27T09:15:12.114532Z"}
v1 只有一个事件——booking.changed,创建、修改、取消共用这一个名字,因为提示不说发生了什么;那是游标说的。
  • booking_reference 和预订数据流里带的 reference 是同一个——把它喂给您的去重,就像您给游标的行去重一样。
  • occurred_at 是这次变化在我们这边被记录下来的时刻,不是这次尝试被发出的时刻。重试会原样重发它。

同一笔预订的连续变化,在它的提示还没送达时会归并成一条:一分钟里改五次,只产生一次通知,而这是正确的,恰恰因为提示不携带任何状态——不管它代表多少次写入,您下一次拉游标看到的都是最终那一行。投递是至少一次,和这个 API 上的其他一切一样:同一次变化您可能收到两条提示,而按 booking_reference 去重让这件事毫无代价。

登记一个回调地址,以及它必须满足的规则

回调地址在控制台里登记,就在签发凭证的那个 Account → API keys 页面上,由所有者或管理员来做——不通过这个 API,理由和密钥不通过它一样。签名 secret(whsec_ 后面跟 64 个十六进制字符)在您的浏览器里生成,并且只显示一次:我们存下它用来签名,但没有任何控制台界面、任何查询能把它读回来。v1 里每个账户同一时间只能有一个处于启用状态的回调地址。回调地址从不被编辑——换一个 URL 或者轮换一个 secret,就是一个新的回调地址(先把旧的停用);停用是它诞生之后控制台提供的唯一开关。

  • 只走 HTTPS,只走 443 端口。http://,以及任何不是 443 的显式端口,永远不会被尝试。
  • 要一个主机名,不要一个地址。IP 字面量(v4 或 v6)、localhost,以及任何落在我们自己平台域名上的东西,都会被拒绝。
  • 重定向永远不被跟随。一次重定向就是一个没人审过的第二个 URL;这次尝试直接失败。
  • 5 秒超时,而且除了状态码,您的响应永远不会被读。快点应答,活儿放到之后再干——正确的形状是“入队,然后返回 204”。

2xx 契约、重试,以及死亡

超时之内的任何 2xx 都算送达。其余一切——4xx、5xx、超时、连接被拒——都按一套固定的退避重试,第五次尝试失败之后,这条提示就死了:最后一次的 HTTP 状态和失败原因会被记录下来,在控制台里看得到,此后不再有任何尝试。而预订本身,一如既往,在游标上等着您。停用一个回调地址,会让它还没送达的提示在下一次清扫时被杀掉,而不是过后再投递给一个您已经关掉的地址。

已失败的尝试次数下一次尝试
11 分钟后
25 分钟后
330 分钟后
42 小时后
5没有了——这条提示已死,最后的状态和原因已被记录

校验签名

每一次投递都带一个 Parkena-Signature 请求头:t=<unix-seconds>,v1=<hex HMAC-SHA-256>。被签名的载荷是字面拼接的 t + . + 原始请求体——请对您收到的那些字节签名,绝不要对解析后再序列化一遍的 JSON 签名。这刻意采用了 Stripe 给它自己的 webhook 用的那套一模一样的方案,连 whsec_ 这个 secret 前缀也一样,所以任何一个您已经在跑的 Stripe webhook 校验器——或者 Parkena 自己仓库里公开的那一个——不用改动就能校验这些投递。

# header: "t=1767139200,v1=5257a869…"   secret: "whsec_…" exactly as shown once
const pairs = Object.fromEntries(header.split(",").map((p) => p.split("=")));
const expected = hexHmacSha256(secret, `${pairs.t}.${rawBody}`);
const fresh = Math.abs(nowSeconds() - Number(pairs.t)) <= 300;
const ok = fresh && timingSafeEqual(pairs.v1, expected);
它的全部,写成一份草图。

三个匆忙的实现会做错的细节:用恒定时间的相等比较,不要用 ===,这样响应耗时就不会泄露一个伪造签名对了多少;拒绝比几分钟更旧的 t——我们建议 300 秒——正是它让一条被截获的投递日后重放起来一文不值;还有,如果您是正经解析这个请求头而不是简单粗暴地切分,请对出现的每一对 v1= 都校验一遍,任何一对匹配就接受——正是它让将来某次签名 secret 的轮换不会在窗口中间把您弄断。

一条没通过您校验的提示,不是一次 Parkena 的投递。回它一个 401,别的什么都不要做——尤其不要按它说的节奏去拉游标。拉游标永远是安全的;拒绝,是为了不让一个没通过认证的调用方来支配您系统的节拍。

14. 拿到访问权,以及拿到帮助

密钥由所有者或管理员在控制台的 Account → API keys 下面签发。没有沙盒,试点的准入是和我们一家一家运营商谈定的——如果您在这里读到的东西,和您本来就在跑的系统合得上,那写信的时候就说这件事。

写信到 [email protected]。已经在跑的对接需要支持时:请附上您的 key_id——绝不要附 secret——以及您要问的那个请求的 external_id 和时间戳。两样都在我们的日志里,合起来能定位到唯一一个请求。

来问问试点的事。

告诉我们您的系统是什么,以及您想让它推送什么。我们会老实告诉您 v1 覆不覆盖得了——§12 是它做不到的事的完整清单,一条条写出来,就是为了让您在动手做任何东西之前,就能决定不做。