API Console
test
Sign in with BES

接入 Messaging

Messaging 用平台账号按人查发送地址、检查开通、回写投递回执,并按事件清理缓存。

环境登录地址带证书调用的接口地址
localhttps://id.local.globalbes.sghttps://id.local.globalbes.sg:19443
testhttps://id.test.globalbes.sghttps://id.test.globalbes.sg:9443
staging待定待定
prod待定待定

Sign in to send requests to see which of these steps are done. Sign in

1. 拿到证书
1.1

生成私钥和证书请求

在哪里做:Messaging 服务器

生成 Messaging 私钥和证书请求,把证书请求送到客户端 CA 签发。

CERT_DIR="$HOME/bes-certs/messaging"
mkdir -p "$CERT_DIR"
SERVICE_ACCOUNT_ID=$(uuidgen | tr A-Z a-z)
openssl req -new -newkey ec -pkeyopt ec_paramgen_curve:P-256 -nodes \
  -subj "/CN=Messaging" \
  -addext "subjectAltName=URI:urn:bes:service:$SERVICE_ACCOUNT_ID" \
  -addext "basicConstraints=critical,CA:FALSE" \
  -addext "keyUsage=critical,digitalSignature" \
  -addext "extendedKeyUsage=clientAuth" \
  -keyout "$CERT_DIR/messaging-service-key.pem" -out "$CERT_DIR/messaging-service.csr"
  • SERVICE_ACCOUNT_ID 每次新生成,写进证书唯一的 URI SAN urn:bes:service:<uuid>;登记平台账号后它就是账号的 service_account_id。
  • 私钥 messaging-service-key.pem 放进 Messaging 部署的密钥存储。
1.2

签发证书

在哪里做:客户端 CA 所在的机器

用客户端 CA 给证书请求签名,得到证书 messaging-service.pem。证书拷回 Messaging 服务器,和私钥一起调 Control;登记平台账号时也要用它。

前提:OpenSSL 3.0 及以上,openssl version 显示 OpenSSL 3.x。Mac 自带的 /usr/bin/openssl 是 LibreSSL,不认 -copy_extensions:先执行 brew install openssl@3,再执行 export PATH="$(brew --prefix openssl@3)/bin:$PATH"。

本环境(test)

  • CA_DIR:待定,test 环境的客户端 CA 位置未定。
  • 签发前把 messaging-service.csr 拷到客户端 CA 所在机器的 ~/bes-certs/messaging/;签发后把 messaging-service.pem 拷回 Messaging 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/messaging"
mkdir -p "$CERT_DIR"
CA_DIR="待定"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/messaging-service.csr" \
  -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
  -CAserial "$CERT_DIR/messaging-service.srl" -CAcreateserial -copy_extensions copy \
  -out "$CERT_DIR/messaging-service.pem"
其他环境

local

本地的客户端 CA 在本机,证书请求不用拷,在本机执行:

CERT_DIR="$HOME/bes-certs/messaging"
mkdir -p "$CERT_DIR"
CA_DIR="${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/control"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/messaging-service.csr" \
  -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
  -CAserial "$CERT_DIR/messaging-service.srl" -CAcreateserial -copy_extensions copy \
  -out "$CERT_DIR/messaging-service.pem"

staging

  • CA_DIR:待定,staging 环境的客户端 CA 位置未定。
  • 签发前把 messaging-service.csr 拷到客户端 CA 所在机器的 ~/bes-certs/messaging/;签发后把 messaging-service.pem 拷回 Messaging 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/messaging"
mkdir -p "$CERT_DIR"
CA_DIR="待定"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/messaging-service.csr" \
  -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
  -CAserial "$CERT_DIR/messaging-service.srl" -CAcreateserial -copy_extensions copy \
  -out "$CERT_DIR/messaging-service.pem"

prod

  • CA_DIR:待定,prod 环境的客户端 CA 位置未定。
  • 签发前把 messaging-service.csr 拷到客户端 CA 所在机器的 ~/bes-certs/messaging/;签发后把 messaging-service.pem 拷回 Messaging 服务器的同一目录,和私钥放在一起。
CERT_DIR="$HOME/bes-certs/messaging"
mkdir -p "$CERT_DIR"
CA_DIR="待定"
openssl x509 -req -days 825 -sha256 -in "$CERT_DIR/messaging-service.csr" \
  -CA "$CA_DIR/client-ca.pem" -CAkey "$CA_DIR/client-ca-key.pem" \
  -CAserial "$CERT_DIR/messaging-service.srl" -CAcreateserial -copy_extensions copy \
  -out "$CERT_DIR/messaging-service.pem"
1.3

登记平台账号

在哪里做:Control 服务器

执行 bes-control seed-trusted-caller,得到 Messaging 的平台账号,状态 active。之后 Messaging 带这张证书调查询接口,Control 按这个账号放行。

  • 账号带 Control 的 channel-gateway 角色(role),命令输出应为 role=channel-gateway;列表不返回角色码,自动检查按显示名、product_id: null 和状态判断。
  • --display-name 写 Messaging:测试台按这个名字找 Messaging 的平台账号。
  • 重复执行保留同一个账号。

本环境(test)

  • 在 Control 服务器上执行 bes-control 的方式:待定,test 环境的 Control 部署方式未定。
  • messaging-service.pem 拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --role channel-gateway --certificate "待定/messaging-service.pem" --display-name Messaging
其他环境

local

本地 Control 在容器 control-api 里,先把证书拷进 ~/.local/share/bes/local/stack/secrets/control/,每次执行时它被复制成容器里的 /run/bes/control/messaging-service.pem。在本机执行:

BES_INFRA="$HOME/Projects/bes-infra"   # bes-infra 仓的路径
cp "$HOME/bes-certs/messaging/messaging-service.pem" "${XDG_DATA_HOME:-$HOME/.local/share}/bes/local/stack/secrets/control/messaging-service.pem"
"$BES_INFRA/envs/local/dev" stack compose exec -T control-api /bin/sh /opt/bes/stack-entrypoint.sh \
  bes-control seed-trusted-caller --role channel-gateway --certificate /run/bes/control/messaging-service.pem --display-name Messaging

staging

  • 在 Control 服务器上执行 bes-control 的方式:待定,staging 环境的 Control 部署方式未定。
  • messaging-service.pem 拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --role channel-gateway --certificate "待定/messaging-service.pem" --display-name Messaging

prod

  • 在 Control 服务器上执行 bes-control 的方式:待定,prod 环境的 Control 部署方式未定。
  • messaging-service.pem 拷到 Control 服务器上的路径:待定。
bes-control seed-trusted-caller --role channel-gateway --certificate "待定/messaging-service.pem" --display-name Messaging --confirm-production

做完这一部分,手里有:Messaging 的证书、私钥和平台账号。查询时带证书和私钥。

2. 发送通知并回写结果
2.1

查一个人的地址

在哪里做:Messaging 代码

发通知前按 person_id 查此人在用的地址,得到地址列表。用其中的地址发送。

GETPerson Resolve

程序里发这个请求(id 用前面步骤返回的):

带:证书

请求:

GET https://id.test.globalbes.sg:9443/internal/control/v1/people/5a9d3c1b-7e24-4f60-b8a1-c03e6d2f9475/channel-identities/resolve?channel_type=email

返回:

HTTP/1.1 200 OK

{
  "person_id": "5a9d3c1b-7e24-4f60-b8a1-c03e6d2f9475",
  "display_name": "Marcus Lim",
  "channel_identities": [
    {
      "channel_identity_id": "6b1f4e9a-8c30-4d72-a5e6-f09c3b7d1e24",
      "channel_type": "email",
      "channel_identifier": "marcus.lim@topaz.example.com",
      "status": "active",
      "verified_at": null
    }
  ]
}

按 person_id 查询 active 人员的全部 active 地址,任选符合消息渠道的地址发普通通知。verified_at: null 表示未验证,该地址同样可以发通知。凭据和邀请消息使用 Control 指定的投递地址。

错误:

HTTP 错误码 处理
404 PERSON_NOT_FOUND 不发送:没有这个人。
2.2

查开通

在哪里做:Messaging 代码

发通知前检查所选租户、项目的 whatsapp-channel 开通状态。

GETEntitlement Resolve

程序里发这个请求(id 用前面步骤返回的):

带:证书

请求:

GET https://id.test.globalbes.sg:9443/internal/control/v1/entitlements/resolve?tenant_id=7c0e2a5e-3b1f-4d6a-9a53-1f2f6d0c8a11&project_id=e2f7a1c4-0d35-4b8e-a6c9-7d18f4b2a903&product_id=b41d9a70-6c2e-4a0f-8e7d-52a9c3f0d614

返回:

HTTP/1.1 200 OK

null

从环境配置取得 whatsapp-channel 的 product_id(值待定),按目标租户、项目查询,租户级产品省略 project_id。返回 null 时提示开通;有效时按 ends_at 控制缓存期限。

本环境(test)

Control 网关:https://id.test.globalbes.sg:9443;证书目录 ~/bes-certs/assistant/。

其他环境

local

Control 网关:https://id.local.globalbes.sg:19443;证书目录 ~/bes-certs/assistant/。

staging

Control 网关:待定;证书目录 ~/bes-certs/assistant/。

prod

Control 网关:待定;证书目录 ~/bes-certs/assistant/。

错误:

HTTP 错误码 处理
404 PRODUCT_NOT_FOUND 核对环境中登记的产品 ID。
403 ACCESS_DENIED 核对平台账号的授权解析权限。
请求示例
curl --fail-with-body --cert "$HOME/bes-certs/assistant/assistant-service.pem" --key "$HOME/bes-certs/assistant/assistant-service-key.pem" --get "https://id.test.globalbes.sg:9443/internal/control/v1/entitlements/resolve" --data-urlencode 'tenant_id=7c0e2a5e-3b1f-4d6a-9a53-1f2f6d0c8a11' --data-urlencode 'project_id=e2f7a1c4-0d35-4b8e-a6c9-7d18f4b2a903' --data-urlencode 'product_id=b41d9a70-6c2e-4a0f-8e7d-52a9c3f0d614'
2.3

回写回执

在哪里做:Messaging 代码

收到通道回执后把投递状态写回 Control,同一回执重试沿用 request_ref。

POSTDelivery Status Record

程序里发这个请求(id 用前面步骤返回的):

带:证书

请求:

POST https://id.test.globalbes.sg:9443/internal/messaging/v1/delivery-status
Content-Type: application/json

{
  "request_ref": "7804c8bc-e978-45e3-95b3-b36f405d5594",
  "message_id": "2c0598e5-d12a-4f71-b65b-65f85c4d7b31",
  "status": "delivered",
  "occurred_at": "2026-10-06T08:00:00+00:00",
  "batch_id": "1e29bb58-b018-4ee1-9c1d-53261eecf87f",
  "recipient_ref": "first",
  "reason": null,
  "provider_error_code": null
}

返回:

HTTP/1.1 200 OK

{
  "updated": true
}

message_id 使用发送受理返回的标识。每个状态变化用独立 request_ref,重试保持原值;把通道时间放入 occurred_at。

错误:

HTTP 错误码 处理
409 IDEMPOTENCY_CONFLICT 同一回执重试保留原参数;后续状态使用新的 request_ref。
2.4

接收事件并清缓存

在哪里做:Messaging 代码

接收 Control 事件,持久化去重即返回 2xx,再本地应用与重试。

按事件契约验证签名与 audience,使用 event_id 去重,持久化即 2xx,应用失败本地重试;发送方仅在未持久化时重试投递。

  • channel_access.revoked:丢掉该 channel_identity_id 的地址、认人和会话身份缓存;按 person_id 清除关联范围。
  • person.status_changed:丢掉该 person_id 的地址、认人、授权和会话身份缓存。
  • entitlement.changed:丢掉对应产品、租户、项目的开通结果和包含该范围的 contexts。

本环境(test)

在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 messaging-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。

其他环境

local

在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 messaging-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。

staging

在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 messaging-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。

prod

在 Control 的 BES_CONTROL_EVENT_CONSUMERS 中保留其他消费者,并登记本服务:HTTPS endpoint 待定,audience 为 messaging-api,event_types 使用上面的列表。接收地址部署方式待定。验收时逐种送达事件,确认相应缓存被清除。

订阅配置
{
  "messaging": {
    "endpoint": "待定",
    "audience": "messaging-api",
    "event_types": [
      "channel_access.revoked",
      "person.status_changed",
      "entitlement.changed"
    ]
  }
}

做完这一部分,手里有:按人取得地址,检查开通并记录投递结果,订阅三种事件维护缓存。

3. 身份上下文名称
3.1

展示上下文名称

在哪里做:Messaging 代码

显示 Control 返回的租户与项目名称;租户级项目名为 null。

身份上下文的显示名称来自 identity.resolve:每项包含 tenant_name 与 project_name;租户级 project_name 为 null。示例:{product_id: "<产品 UUID>", tenant_id: "<租户 UUID>", tenant_name: "Acme Construction", project_name: null}。

做完这一部分,手里有:按 Control 当前上下文展示租户与项目名称。