purser文档
purser文档
首页价格博客English登录控制台

开始

purser 文档10 分钟上线

接入渠道

网站聊天窗口App 客服页邮件亚马逊买家消息WhatsApp、Instagram 和 Messenger

AI 客服

知识库术语表帮助中心AI 工具SOP(标准流程)导购与销售AI 怎么回答试聊与发布话题与质检

收件箱与团队

收件箱语音消息团队与分流规则、宏与结束对话市场(多国家、多币种)通知与公告学习中心顾客

更多功能

退款补偿与改单产品反馈

账号与计费

套餐与账单账号与数据

开发者

身份令牌:告诉 purser 顾客是谁REST API 与 Node SDK退款接口(Actions API)Webhooks用 MCP 连接你自己的 AI 助手

退款接口(Actions API)

自研商城实现一个签名的 HTTPS 接口,purser 通过它询问退款金额、发起退款;包括请求格式、签名校验、幂等、重试和结果回报。

purser 不经手钱,也不保存任何支付凭证。退款时,它调用你们自己的一个接口,由你们的系统去退。本页是这个接口的技术规格;AI 什么时候提出退款、谁来批准、额度和冻结,见 退款。

整个流程里 purser 会调你的接口三种请求:

类型什么时候你要做什么
ping控制台里点「测试连接」回 { "ok": true }
refund.previewAI 准备提出退款、或客服发起退款时,先问金额只计算,不动钱,返回金额或拒绝
refund.create顾客确认、并且自动执行或客服批准之后真正退款(或只登记申请),返回结果

退款金额永远来自你的 refund.preview,不是模型。

在控制台开通

「设置 → 退款与补偿」(店主和管理员):

  1. 「退款接口地址」:你服务器上的一个地址,必须是 https://;
  2. 「生成密钥」:签名密钥只显示这一次,复制到服务器的环境变量里(例如 PURSER_ACTIONS_SECRET)。之后只显示最后 4 位;
  3. 「测试连接」:purser 发一个签名的 ping,结果会显示在页面上;
  4. 「怎么退」:「直接退款」(接口收到就退)或「只提交退款申请」(接口只登记,处理完再回报结果);
  5. 打开「启用」。

要换密钥,点「换新密钥」:新密钥同样只显示一次,旧密钥立刻失效,你们的服务器要同时更新。

退款动作需要 Growth 或 Scale 套餐(测试工作区可以直接用),见 套餐与账单。

请求格式

POST <退款接口地址>
content-type: application/json
user-agent: purser-actions/1
purser-signature: t=<unix 秒>,v1=<hex HMAC-SHA256(密钥, "<t>.<原始 body>")>
purser-idempotency-key: <幂等键>
{
  "id": "01K…",
  "type": "refund.create",
  "workspace": "<工作区 ID>",
  "createdAt": 1790000000000,
  "idempotencyKey": "<幂等键>",
  "data": {
    "actionId": "01K…",
    "orderId": "ord_2001",
    "customerId": "cus_mia",
    "lines": [{ "itemId": "li_1", "quantity": 1 }],
    "reason": "defect",
    "amount": 1599,
    "currency": "USD",
    "mode": "execute"
  }
}
字段说明
id这一次投递的 ID,每次重试都不同
typeping、refund.preview、refund.create、coupon.issue、points.grant、order.update_address 或 order.cancel(见下文)
workspace工作区 ID,和 GET /api/v1/ping 返回的 orgId 相同
createdAt投递时间,毫秒
idempotencyKey幂等键,和请求头 purser-idempotency-key 相同
data.actionIdpurser 这笔退款的 ID,回报结果时用它
data.orderId你系统里的订单 ID(推送订单时的 id)
data.customerId你系统里的顾客 ID,可能为 null
data.channel这单的下单渠道(推送订单时的 channel),没传过时为 null;所有类型的请求都带
data.lines要退的商品行(itemId 是订单商品行的 id);为空表示由你按订单和原因决定
data.reasondefect、not_received、return、other 之一,客服填了备注时后面接 : <备注>
data.amount、data.currency只在 refund.create 里有:顾客(或客服)确认过的金额,最小货币单位
data.modeexecute(直接退款)或 request(只登记申请),对应控制台里的「怎么退」

ping 的 data 是空对象 {}。

客服在收件箱里先看金额时,refund.preview 可能在这笔退款还没建立时就发出,这时 data.actionId 是空字符串。

你要返回什么

都是 JSON,状态码 200:

类型成功拒绝
ping{ "ok": true }—
refund.preview{ "ok": true, "amount": 2500, "currency": "USD", "note": "…" }{ "ok": false, "code": "final_sale", "message": "…" }
refund.create{ "ok": true, "status": "succeeded", "refundId": "re_…" }{ "ok": false, "code": "…", "message": "…" }
  • amount 必须是非负整数(最小货币单位),currency 是三位大写代码。不符合时当作「没有回答」。
  • refund.create 的 status 只能是 succeeded 或 pending。pending 表示你接受了,稍后回报结果(「只提交退款申请」模式下就应该回 pending)。
  • 拒绝时 code 是你自己定的机器可读的原因,message 会记录下来给客服看。拒绝就是最终结果,不会重试。

补偿券:coupon.issue

在「设置 → 退款与补偿」里,补偿方案选「自研商城接口」时,客服发补偿,purser 会向同一个接口发 coupon.issue,请你们的系统为这位顾客生成一张一次性券码(补偿怎么用见 补偿):

{
  "type": "coupon.issue",
  "idempotencyKey": "<幂等键>",
  "data": {
    "actionId": "01K…",
    "customerId": "cus_mia",
    "orderId": "ord_2001",
    "templateRef": "tpl_sorry_10",
    "valueType": "fixed",
    "amount": 1000,
    "currency": "USD",
    "minSubtotal": null,
    "expiresAt": 1792600000000,
    "reason": "包裹晚到 5 天"
  }
}
字段说明
templateRef方案里填的你们系统的券模板 ID,没填时为 null
valueType、amountfixed:减 amount(最小货币单位);percent:打 amount% 折扣
minSubtotal最低消费(最小货币单位),没有时为 null
expiresAt券码应该失效的时间(毫秒)

成功返回 { "ok": true, "code": "SORRY-8K2X" }(可带 expiresAt),拒绝返回 { "ok": false, "code": "…", "message": "…" }。同一个幂等键必须返回同一个券码:purser 在没收到回答时会用同一个键重试。

顾客下单用了 purser 发的券码时,调用 POST /api/v1/coupons/redeemed(密钥 sk_…,body {"codes": ["SORRY-8K2X"]}),控制台会显示「已使用」。

积分与改单:points.grant、order.update_address、order.cancel

补偿方案选「积分」时,以及没有连接 Shopify 的店铺在收件箱里改地址、取消订单时,purser 也调用这个接口:

{ "type": "points.grant", "idempotencyKey": "<幂等键>",
  "data": { "actionId": "01K…", "customerId": "cus_mia", "orderId": "ord_2001", "points": 300, "reason": "包裹晚到 5 天" } }

{ "type": "order.update_address", "idempotencyKey": "<幂等键>",
  "data": { "actionId": "01K…", "orderId": "ord_2001", "customerId": "cus_mia", "reason": "顾客搬家了",
            "address": { "name": "Mia", "line1": "9 New Ave", "line2": "Apt 3", "city": "Austin",
                         "province": "TX", "postalCode": "78701", "country": "US", "phone": "+1 512 555 0100" } } }

{ "type": "order.cancel", "idempotencyKey": "<幂等键>",
  "data": { "actionId": "01K…", "orderId": "ord_2001", "customerId": "cus_mia", "reason": "重复下单" } }
类型你们要做什么
points.grant给这位顾客加 points 积分。可以返回 { "ok": true, "balance": 1300 }(加完后的余额,可选)
order.update_address把这张还没发货的订单改寄到 address。country 是两位国家代码,line2、province、phone 可能没有
order.cancel取消这张还没发货的订单,全额原路退款,库存放回

成功返回 { "ok": true },做不了返回 { "ok": false, "code": "…", "message": "…" }(比如订单刚发货:"code": "fulfilled"),purser 会把 message 显示给客服。同一个幂等键只能生效一次。

校验签名

purser-signature 的格式是 t=<unix 秒>,v1=<十六进制签名>。签名是用签名密钥对字符串 "<t>.<原始 body>" 做 HMAC-SHA256:

  • 密钥就是控制台给你的完整字符串(以 whsec_ 开头,前缀也算在内);
  • 必须用收到的原始 body 计算,不要先解析 JSON 再序列化;
  • t 和你服务器当前时间相差超过 5 分钟(300 秒) 就拒绝;
  • 用常量时间比较。

验签失败请回 401 和 { "ok": false, "code": "bad_signature" }。purser 会把这笔记为失败,不会重试。

Node.js(Express)示例,不依赖 SDK:

import express from "express";
import { createHmac, timingSafeEqual } from "node:crypto";

function verifyPurserSignature(secret, rawBody, header, nowS = Math.floor(Date.now() / 1000)) {
  if (!secret || !header) return false;
  const parts = Object.fromEntries(
    header.split(",").map((p) => {
      const i = p.indexOf("=");
      return [p.slice(0, i).trim(), p.slice(i + 1).trim()];
    }),
  );
  const t = Number(parts.t);
  if (!Number.isFinite(t) || Math.abs(nowS - t) > 300) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  const given = String(parts.v1 ?? "");
  return given.length === expected.length && timingSafeEqual(Buffer.from(given), Buffer.from(expected));
}

const app = express();

// 用 text() 拿原始 body,验签之后再解析
app.post("/purser/actions", express.text({ type: "application/json" }), async (req, res) => {
  if (!verifyPurserSignature(process.env.PURSER_ACTIONS_SECRET, req.body, req.get("purser-signature"))) {
    return res.status(401).json({ ok: false, code: "bad_signature" });
  }
  const delivery = JSON.parse(req.body);
  const { type, idempotencyKey, data } = delivery;
  try {
    if (type === "ping") return res.json({ ok: true });
    if (type === "refund.preview") {
      const order = await db.orders.get(data.orderId);
      if (order.finalSale) return res.json({ ok: false, code: "final_sale", message: "Final sale" });
      return res.json({ ok: true, amount: order.total, currency: order.currency });
    }
    if (type === "refund.create") {
      // 同一个幂等键只退一次
      const done = await db.refunds.findByKey(idempotencyKey);
      if (done) return res.json({ ok: true, status: "succeeded", refundId: done.id });
      const refundId = await refundOrder(data.orderId, data.amount, data.currency, idempotencyKey);
      await db.refunds.save({ key: idempotencyKey, id: refundId });
      return res.json({ ok: true, status: "succeeded", refundId });
    }
    return res.status(400).json({ ok: false, code: "unknown_type" });
  } catch (e) {
    // 5xx = 让 purser 稍后用同一个幂等键重试
    return res.status(500).json({ ok: false, code: "error" });
  }
});

db 和 refundOrder 换成你们自己的实现。

幂等与重试

  • refund.create 的幂等键就是这笔退款的 actionId,每次重试都一样(id 每次不同)。你的系统对同一个幂等键最多退一次,重复收到时返回第一次的结果。这样 purser 重试永远不会让顾客被退两次钱。
  • 每次请求最多等 10 秒。purser 不跟随跳转。
  • 超时、连不上、非 2xx 且不是上面那种 { "ok": false, … } 拒绝、2xx 但不是合法的 JSON 回答(或 status 不认识):都算「没有回答」。
  • refund.create 没有回答时会退避重试,最多重试 5 次(第一次间隔 20 秒,之后按指数增长)。全部失败后这笔退款记为失败,客服能在「退款」页看到原因。
  • ping 和 refund.preview 不重试:没有回答就是这一次失败(测试连接显示失败;AI 不会提出这笔退款;客服看到错误)。

回报结果

refund.create 回了 pending(包括「只提交退款申请」模式),处理完之后用 API 密钥(sk_…,见 REST API 与 Node SDK)调:

curl -X POST https://askpurser.com/api/v1/actions/$ACTION_ID/status \
  -H "Authorization: Bearer $PURSER_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "status": "succeeded", "refundId": "re_123" }'
字段说明
statussucceeded 或 failed(必填)
refundId你系统里的退款号(可选)
message失败原因(可选),会记录给客服看

返回 { "id": "…", "status": "…" };这个工作区里没有这笔退款时返回 404。只有还在等结果(pending,或还在发送中)的退款会被更新,已经结束的退款保持原状,返回它当前的状态。顾客会在原来的对话里收到结果。

退款的状态

控制台「退款」页、MCP 的 list_refunds 工具里看到的状态:

状态含义
awaiting_customer金额已经给顾客看了,等顾客确认
awaiting_approval超出额度、币种不是额度币种、自动退款被冻结,或这类问题的档位不到 R4,等客服批准
executing正在调你的接口(含重试)
pending你的接口接受了,等你回报结果
succeeded / failed你的接口给出的结果(或全部重试失败)
declined顾客说不用了
rejected客服拒绝了
cancelled / expired没人及时处理,或对话已经往下走了

用 SDK 写(发布后)

@purser-ai/node 里的 actionsHandler 会替你验签、按类型分发、返回正确的 JSON;你的函数抛异常时它回 500,purser 就会重试。这个包还没有发布到 npm,发布之前请用上面的写法。

import { actionsHandler } from "@purser-ai/node";

const handle = actionsHandler({
  secret: process.env.PURSER_ACTIONS_SECRET,
  refund: {
    // 只计算,不动钱。金额用最小货币单位。
    async preview({ orderId, lines, reason }) {
      const order = await db.orders.get(orderId);
      if (order.finalSale) return { refused: "final_sale", message: "Final sale" };
      return { amount: order.total, currency: order.currency };
    },
    // 真退款。同一个 idempotencyKey 只能退一次。
    async create({ orderId, amount, currency, idempotencyKey, mode }) {
      const done = await db.refunds.findByKey(idempotencyKey);
      if (done) return { status: "succeeded", refundId: done.id };
      const refundId = await refundOrder(orderId, amount, currency, idempotencyKey);
      await db.refunds.save({ key: idempotencyKey, id: refundId });
      return { status: "succeeded", refundId };
    },
  },
});

// Cloudflare Workers / Bun / Deno / Hono 这类 fetch 风格的服务器:
export default { fetch: (request) => handle(request) };

补偿券、积分和改单也一样,给哪个就处理哪个,没给的类型会回 unsupported:

actionsHandler({
  secret: process.env.PURSER_ACTIONS_SECRET,
  coupon: { async issue({ idempotencyKey, customerId, amount }) { return { code: await makeCode(idempotencyKey) }; } },
  points: { async grant({ customerId, points, idempotencyKey }) { return { balance: await addPoints(customerId, points, idempotencyKey) }; } },
  order: {
    async updateAddress({ orderId, address }) { await shipTo(orderId, address); return {}; },
    async cancel({ orderId }) { await cancelAndRefund(orderId); return {}; },
  },
});

verifyActionSignature(secret, body, header) 也单独导出,可以只用它验签。

安全要点

  • 金额永远来自你的 refund.preview;refund.create 里的 amount 就是顾客确认过的那个数;
  • AI 只会为已登录顾客自己的订单、在你的退货政策允许的期限内提出退款;同一个订单同时只有一笔,已经通过 purser 退过的不会再提;
  • 控制台「试聊」里顾客可以点确认,但永远不会调 refund.create。

REST API 与 Node SDK

用密钥从你的服务器把顾客、商品、订单、物流和历史工单推给 purser,以及 Node SDK 的用法。

Webhooks

对话开始、转人工、结束、被评分,退款或补偿完成,收到新反馈时,purser 把签过名的事件 POST 到你自己系统的地址。

本页内容

在控制台开通请求格式你要返回什么补偿券:coupon.issue积分与改单:points.grant、order.update_address、order.cancel校验签名幂等与重试回报结果退款的状态用 SDK 写(发布后)安全要点