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 }第二条响应不是一句您可以不理的提醒:那个停车场已经离开了 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_id 和 secret 是同一份凭证的两半,用一个点连起来。第一个点把它们分开;再往后的点都属于 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_issue 和 api_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_label | string,1–80 个字符 | 半年之后您会怎么称呼这把密钥。必填。 |
p_scopes | 权限范围数组 | 一组互不重复、且不为空的权限范围。必填。 |
p_expires_in_days | integer 1–3650,或 null | null 表示永不过期。选填。 |
[{
"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"
}]没有 p_expires_at,将来也不会有。过期是一个天数,由服务器按它自己的时钟去换算;这个 API 在任何一条接口上都不接受调用方给出的时间戳。
轮换
没有轮换这个调用,因为零停机的轮换不过就是按正确顺序发出的两个调用:
- 用同一组权限范围签发第二份凭证。
- 把它部署到您的系统里,确认流量确实走通了——用新密钥调一次
GET /ping,然后盯着它上面的last_used_at。 - 把旧的那一份吊销。
从第一步到第三步之间,两份凭证都是有效的。没有任何限制拦着您同时持有两份。
吊销
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 | /lots | read_supply |
PUT | /lots/{external_id} | write_supply |
PUT | /lots/{external_id}/rates | write_supply |
GET | /lots/{external_id}/blocked-periods | read_supply |
PUT | /lots/{external_id}/blocked-periods | write_supply |
GET | /bookings | read_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 价格区间。
| 查询参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
after | string | — | 从这个 external_id 之后接着取。用上一页给的 next_after。 |
limit | integer 1–200 | 50 | 要得更多会被拒绝,不会被悄悄调小。 |
分页走的是您自己的 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 价格的停车场,rates 是 null。
PUT /lots/{external_id}
创建或更新一个停车场。一个请求,一个停车场——没有批量接口,而接口本身的形状就是强制这一点的东西。数组形式的请求体会被 too_many_lots 拒掉。
PUT 是一份完整表述。少一个键的意思是 null,不是“保持原样”。每次都把整个停车场发过来。另一种做法会让您没办法清空一个字段,还会把一个打错的键变成永久而无声的空操作。
| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
external_id | string | 否 | 如果带了,它必须和路径里的那个相同。会被检查,但不会被使用。 |
name | string,≤200 | 是 | — |
timezone | string | 是 | IANA 名称,例如 Europe/Berlin。每一次跨天边界的计算都要经它来解。 |
handover | string | 是 | self 或 attended。 |
online_bookable | boolean | 是 | 您的销售开关。之所以必填,正是因为给它一个默认值,会在第一次不完整的推送时把一个正在售卖的停车场下架。 |
capacity | integer 0–1000000 | 否 | 总车位数。不是今晚空着的车位——见下面的警告。 |
latitude | number 或 string | 否 | 四舍五入到小数点后 6 位。必须和 longitude 一起发。 |
longitude | number 或 string | 否 | 四舍五入到小数点后 6 位。必须和 latitude 一起发。 |
city | string,≤120 | 否 | — |
country_code | string | 否 | ISO 3166-1 alpha-2,例如 DE。 |
features | string 数组 | 否 | 见下面的清单。顺序无所谓——我们会替它们排好序。 |
arrival | object | 否 | 自由格式的到达指引。 |
media | object | 否 | 自由格式的媒体引用。 |
min_advance_days | integer 0–365 | 否 | 默认为 0。 |
min_stay_days | integer 1–365 | 否 | — |
max_stay_days | integer 1–365 | 否 | 三个都止步于 365,因为那就是 Parkena 为一次停留报价所能覆盖的最远期限。更大的值会被接受,但永远不会被兑现。 |
cancellation | object 或 null | 否 | 三个键要么都给,要么都不给——见下文。 |
features 接受:indoor、security、cameras、gate_automation、plate_recognition、ev_charging、disabled_access、oversize_vehicle、valet。
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。| 字段 | 含义 |
|---|---|
result | created、updated 或 unchanged。 |
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_price | decimal | 两种形状二选一 | 不能和区间字段一起用。 |
floor_price | decimal | 随区间一起 | 必须 ≤ base_price。 |
base_price | decimal | 随区间一起 | 必须落在下限和上限之间。 |
ceiling_price | decimal | 随区间一起 | 必须 ≥ base_price。 |
min_first_day_price | decimal 或 null | 否 | 必须落在区间之内。 |
currency | string | 否 | ISO 4217。默认取您的结算货币,而且不能是别的。 |
dynamic | boolean | 否 | 默认为 false。 |
valid_from | YYYY-MM-DD 或 null | 否 | 一个日历日期。时间戳会被拒绝,不会被截断。 |
valid_to | YYYY-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_on | YYYY-MM-DD | 是 | 第一个被封闭的日子,含当天。一个日历日期——时间戳会被拒绝,不会被截断。 |
ends_on | YYYY-MM-DD | 是 | 最后一个被封闭的日子,含当天——不是它的后一天。不得早于 starts_on。 |
reason | string ≤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。| 字段 | 含义 |
|---|---|
result | created(原本一段都没有,现在有了)、updated(其余任何发生了写入的情况——包括一次把清单清空的 PUT []),或 unchanged。 |
added / removed | 这次写入实际挪动的行数。unchanged 时两个都是 0。 |
blocked_periods | 存下来的清单,在写入之后重新读出,带着新算的 overlapping_bookings 计数——是数据库现在实际持有的东西,绝不是把您的请求体回显一遍。 |
§2 那条“先比对差异,再发更新”的规则,在这里以清单的形式照样成立:result: "unchanged" 的意思是,存着的清单和您的请求体本来就说着同一件事,于是一条语句都没有发出。每晚重推一份没变的封闭清单,代价是一次读取。
GET /bookings
您的 Parkena 预订,以一个拉取游标的形式给出。这个游标就是事实的来源:一个登记过的 webhook 回调地址可以收到一条带签名的 booking.changed 提示,让您早一点来拉取——见 §13——但提示不携带任何预订数据,而您在这个游标上建起来的东西,一样也不会白费。
| 查询参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
since | string | — | 您上一次调用返回的那个 next 令牌。想“从头开始”就不要带它。 |
limit | integer 1–500 | 200 | 要得更多会被拒绝,不会被悄悄调小。 |
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
}| 字段 | 说明 |
|---|---|
reference | Parkena 的预订编号。这是去重用的键。 |
external_reference | 您自己的那个字符串,如果当初设过的话。原样回传,我们绝不拿它当键。 |
check_in_local / check_out_local | 停车场自己的当地时钟,不带任何时区偏移。请按 timezone 去读它们。 |
total | 一个十进制字符串,不是浮点数。 |
status | pending、confirmed、checked_in、completed、cancelled。 |
payment_status | pending、paid、partial、refunded、expired、not_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)三条规则,每一条都是承重的:
- 按
reference去重。投递是至少一次,绝不是恰好一次。您会不止一次看到同一笔预订,那是正确的行为,不是故障。 - 在您把这一批稳妥存好之后再保存
next,不要提前保存。如果您的进程在一批中途挂了,没保存的游标会把它重投一遍——而第 1 条规则让这件事无害。 - 不要自己造游标。它是我们签发的一个令牌。不存在
?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 删不掉一个停车场,也删不掉一个价格,而且没有任何授权会让它做到。在数据接入的代码里,先 delete 再 insert 是最常见的 upsert 写法,而放到这批数据上,它是灾难性的:停车场会拿到一个新身份,并且把它的上架历史、它的价格和它的可用性一起带走——出自一个没有人在盯的任务。移除一个停车场,是人在控制台里刻意去做的事,那里的确认框会把随之一起消失的东西一一列出来。一个曾经收过预订的停车场根本不能被移除,谁都不行:预订是一份财务记录,它要被保留,而当初为它收下这笔预订的停车场也一样。要让一个停车场停售,请设 "online_bookable": false。
9. 错误
每一个错误都是 JSON,带一个稳定的、机器可读的 error 码:
{ "error": "invalid_price_envelope", "field": "floor_price" }field 出现的时候,是您自己的字段名,原样回传。我们绝不返回数据库消息、约束名或表名——那些是我们自己的东西,我们会改名,而您的对接不该因为我们改名就坏掉。请按 error 来分支。
凭证
| 状态码 | 错误码 | 含义 |
|---|---|---|
401 | unauthorized | 见下文——它涵盖六种不同的情形,并且拒绝说出是哪一种。 |
请求的形状
| 状态码 | 错误码 | 含义 |
|---|---|---|
404 | not_found | 没有这条路由。绝不用来表示某个资源不存在。 |
405 | method_not_allowed | 路径对,动词不对。Allow 响应头会写明我们要的动词。 |
413 | payload_too_large | 请求体超过 64 KiB。 |
415 | unsupported_media_type | Content-Type 不是 application/json。 |
422 | invalid_json | 请求体不是可解析的 JSON。 |
422 | invalid_body | JSON 本身没毛病,但形状不对,或者某个值没法用。 |
422 | missing_field | 缺了一个必填字段。 |
422 | unknown_field | 一个我们不认识的字段。被拒绝,不是被忽略。 |
422 | field_belongs_to_another_route | 一个确实存在、但要在别处设定的字段。今天唯一的一个是 rates——见 §2。 |
422 | invalid_query | 一个错误的或未知的查询参数。 |
422 | invalid_cursor | 那个 since 令牌不是我们发出去的。 |
422 | too_many_lots | 数组形式的请求体。一个请求,一个停车场。 |
内容
| 状态码 | 错误码 | 含义 |
|---|---|---|
422 | invalid_timezone | 不是一个 IANA 时区名称。 |
422 | invalid_coordinates | 超出范围,或者只给了纬度没给经度。 |
422 | invalid_country_code | 不是 ISO 3166-1 alpha-2。 |
422 | invalid_stay_bounds | min_stay_days / max_stay_days 互相矛盾。 |
422 | incomplete_cancellation_policy | 取消政策的三个键只给了一个或两个。 |
422 | invalid_price_envelope | 下限、基准价和上限的顺序不对。 |
422 | invalid_first_day_price | min_first_day_price 落在区间之外。 |
422 | invalid_validity_window | valid_to 早于 valid_from。 |
422 | currency_is_not_settlement_currency | 不是您的结算货币。 |
422 | invalid_blocked_periods | 一条封闭时段条目没法用;field 会用您自己请求体的坐标把它指名出来(blocked_periods[3].ends_on)。拒绝清单的表格在 §7。 |
409 | conflict | 一次真正的竞态:两次推送同时创建了同一个 external_id。重试即可。 |
我们这边的
| 状态码 | 错误码 | 含义 |
|---|---|---|
429 | rate_limited | 超过了某项限制。请遵守 Retry-After。 |
503 | try_again | 一次短暂的数据库冲突。重试;Retry-After 会带上。 |
500 | internal_error | 我们的错。它完整地记在我们的日志里。重试是合理的。 |
为什么那个 401 不肯多说
unauthorized 涵盖下面全部情形,并且拒绝把它们区分开:
- 没有
Authorization请求头,或者有一个我们解析不了的。 - 一个不对应任何凭证的
key_id。 - 一个确实存在的
key_id,配了错误的 secret。 - 一份已吊销、已过期,或者所属运营商账户已被停用的凭证。
- 一份有效的凭证,但缺少这条接口所要求的权限范围。
- 一份有效的凭证,指名了一个并不属于它名下停车场的
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——一份清单,一个停车场 |
速率限制是令牌桶——一份会持续回填的突发额度。
| 桶 | 计在谁头上 | 突发额度 | 回填 |
|---|---|---|---|
| 全部请求 | 来源地址 | 240 | 4 次 / 秒 |
| 失败的认证 | 来源地址 | 20 | 每 3 秒 1 次 |
| 读 | 凭证 | 120 | 2 次 / 秒 |
| 写(突发) | 凭证 | 60 | 1 次 / 秒 |
| 写(每小时) | 凭证 | 1000 | 1000 次 / 小时 |
被拒绝时是 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_superseded和envelope_widened是仅有的两个跟审核沾边的信号,而后者是刻意保守的。 - 没有租户选择。这个 API 解析的任何请求体或查询串里,都没有
tenant_id。一个请求作用在哪一家运营商身上,只从凭证解析出来,别无他处——一个由请求给出的运营商 id,就等于一把跨租户的写入密钥。 - 任何地方都不接受调用方给出的时间戳。没有
as_of,没有watermark,没有updated_since。您可以发的日期只有valid_from和valid_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"}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 状态和失败原因会被记录下来,在控制台里看得到,此后不再有任何尝试。而预订本身,一如既往,在游标上等着您。停用一个回调地址,会让它还没送达的提示在下一次清扫时被杀掉,而不是过后再投递给一个您已经关掉的地址。
| 已失败的尝试次数 | 下一次尝试 |
|---|---|
| 1 | 1 分钟后 |
| 2 | 5 分钟后 |
| 3 | 30 分钟后 |
| 4 | 2 小时后 |
| 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 是它做不到的事的完整清单,一条条写出来,就是为了让您在动手做任何东西之前,就能决定不做。
