PAYATHON 2026

如何使用 Zoho Books API 生成支付链接并通过 SMS 发送发票?

支付小周

结论

仅靠 Zoho Books API,通常无法完整实现“生成支付链接 → 通过 SMS 发送 → 支付回调 → 创建已付款发票 → 再通过 SMS 发送发票”这一整套流程。

实际应用中,需要组合使用三类服务:

  1. 由支付网关或支付链接服务生成付款页面;
  2. 由 SMS 服务商发送支付链接和发票链接;
  3. 通过 Zoho Books API 创建发票,并在确认收款后登记 Customer Payment。

如果账户已经启用 Zoho Books 支持的在线支付网关,可以先创建发票,再把发票的在线付款页面链接发给客户。不过,链接字段和相关能力会受到地区、版本、网关及组织配置的影响,具体应以当前 API 响应和官方文档为准。

推荐流程

更稳妥的做法是在客户付款前创建发票,具体顺序如下:

  1. 在 Zoho Books 中创建草稿或待付款发票;
  2. 使用支付服务创建支付链接,并将 invoice_id 写入支付订单的 metadata;
  3. 通过第三方 SMS 服务发送支付链接;
  4. 接收支付服务发送的 webhook;
  5. 验证 webhook 签名,确认交易确实成功;
  6. 调用 Zoho Books Customer Payments API,将付款应用到对应发票;
  7. 获取发票 PDF 或客户可以访问的发票页面;
  8. 通过 SMS 发送安全的发票下载链接。

这套流程可以让支付订单、Zoho Books 发票和后续退款保持稳定的对应关系,也能减少收款后无法匹配客户或发票的问题。

创建 Zoho Books 发票

以下示例省略了 OAuth 获取过程。实际调用时,需要使用有效的 Zoho OAuth access token,并根据数据中心调整 API 域名。

curl --request POST \
  'https://www.zohoapis.com/books/v3/invoices?organization_id=YOUR_ORGANIZATION_ID' \
  --header 'Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "customer_id": "YOUR_CUSTOMER_ID",
    "date": "2026-08-31",
    "line_items": [
      {
        "item_id": "YOUR_ITEM_ID",
        "quantity": 1,
        "rate": 299.00
      }
    ],
    "notes": "Payment order: order_20260831_001"
  }'

收到响应后,保存其中的 invoice_id、发票编号和应付金额。不要只靠备注里的订单号进行匹配,最好同时在自己的数据库中保存以下对应关系:

internal_order_id
zoho_invoice_id
payment_order_id
customer_id
expected_amount
currency
payment_status

生成并发送支付链接

支付链接应由实际负责收款的支付服务生成,例如已经接入的支付网关、托管收银台或支付链接产品。创建支付订单时,需要把 Zoho Books 的 invoice_id 写入 metadata。

下面是与厂商无关的示意代码。实际使用时,需要将字段名称替换为所选支付服务的真实 API 字段:

const paymentLink = await paymentProvider.createPaymentLink({
  amount: 29900,
  currency: "CNY",
  description: "Invoice INV-000123",
  metadata: {
    internal_order_id: "order_20260831_001",
    zoho_invoice_id: "YOUR_INVOICE_ID"
  },
  callback_url: "https://example.com/webhooks/payment"
});

await smsProvider.send({
  to: "+8613800000000",
  message: `您的订单待支付,请打开:${paymentLink.url}`
});

金额单位必须按照支付服务的定义处理。有些接口使用元,有些使用分等最小货币单位,因此不能根据示例直接认定 29900 代表 299.00。

支付成功后登记收款

收到 webhook 后,先验证签名,再调用支付服务的查询接口,核实交易状态、金额、币种和商户订单号。确认无误后,调用 Zoho Books Customer Payments API,将款项应用到对应发票。

curl --request POST \
  'https://www.zohoapis.com/books/v3/customerpayments?organization_id=YOUR_ORGANIZATION_ID' \
  --header 'Authorization: Zoho-oauthtoken YOUR_ACCESS_TOKEN' \
  --header 'Content-Type: application/json' \
  --data '{
    "customer_id": "YOUR_CUSTOMER_ID",
    "payment_mode": "Online Payment",
    "amount": 299.00,
    "date": "2026-08-31",
    "reference_number": "PAYMENT_PROVIDER_TRANSACTION_ID",
    "invoices": [
      {
        "invoice_id": "YOUR_INVOICE_ID",
        "amount_applied": 299.00
      }
    ]
  }'

全额付款登记成功后,发票通常会变为已付款状态。不要直接修改发票状态来代替创建 Customer Payment,否则应收账款、付款记录和对账信息可能不完整。

如果业务必须采用“先收款、后开票”的方式,也可以在 webhook 确认付款成功后创建发票,然后立即创建 Customer Payment,并将款项应用到这张发票。不过,付款前必须保存好客户、商品、税率、币种和开票主体等信息。

通过 SMS 发送发票

SMS 适合发送短文本和 URL,不适合直接发送 PDF 附件。Zoho Books 的邮件发送能力也不等同于 SMS 发送能力,所以通常需要自行完成以下处理:

  • 获取 Zoho Books 提供的客户可访问发票链接;
  • 或下载发票 PDF,并保存到受保护的对象存储;
  • 生成带有效期的签名 URL;
  • 通过 SMS 服务商发送该 URL。

示意代码:

const invoiceUrl = await createTemporaryInvoiceUrl({
  invoiceId: "YOUR_INVOICE_ID",
  expiresInSeconds: 86400
});

await smsProvider.send({
  to: "+8613800000000",
  message: `您的付款已收到。发票请在 24 小时内下载:${invoiceUrl}`
});

不要把需要 Zoho OAuth 登录才能访问的 API 地址直接发给客户。客户无法使用应用的 access token,在 URL 中携带 token 还会造成严重的凭据泄露风险。

Webhook 处理示例

app.post("/webhooks/payment", async (req, res) => {
  const event = verifyPaymentWebhook(req);

  if (event.type !== "payment.succeeded") {
    return res.sendStatus(200);
  }

  const payment = await paymentProvider.getPayment(event.payment_id);

  if (
    payment.status !== "succeeded" ||
    payment.amount !== 29900 ||
    payment.currency !== "CNY"
  ) {
    return res.sendStatus(400);
  }

  const zohoInvoiceId = payment.metadata.zoho_invoice_id;

  if (await hasProcessedPayment(payment.id)) {
    return res.sendStatus(200);
  }

  await createZohoCustomerPayment({
    customerId: "YOUR_CUSTOMER_ID",
    invoiceId: zohoInvoiceId,
    amount: 299.00,
    referenceNumber: payment.id
  });

  await markPaymentAsProcessed(payment.id);

  const invoiceUrl = await createTemporaryInvoiceUrl({
    invoiceId: zohoInvoiceId,
    expiresInSeconds: 86400
  });

  await smsProvider.send({
    to: payment.customer_phone,
    message: `付款成功,发票下载地址:${invoiceUrl}`
  });

  res.sendStatus(200);
});

在生产环境中,数据库状态更新、Zoho Books 调用和短信发送都应设计成可重试任务。短信发送失败时,不应回滚已经确认的收款记录。

注意事项

防止重复记账

支付平台可能多次推送同一个 webhook。应为支付交易 ID 设置唯一约束,并在调用 Zoho Books 前检查是否已经创建过对应的 Customer Payment。

不要只相信 webhook 内容

必须验证 webhook 签名,并通过支付服务的服务端接口重新查询交易。至少要核对以下信息:

  • 支付状态;
  • 商户订单号;
  • 金额;
  • 币种;
  • 收款账户;
  • 是否已经退款或撤销。

处理手续费和部分付款

Customer Payment 中应用到发票的金额,通常应是客户实际偿还的应收金额,而不是扣除网关手续费后的银行入账净额。手续费的记账方式需要按照组织的会计科目和对账规则处理。

如果支持分期或部分付款,amount_applied 只能填写本次实际应用到该发票的金额。

保护发票链接

发票中可能包含姓名、地址、税号和交易明细。短信里的下载地址应使用 HTTPS,并设置有效期,同时避免使用可预测的连续编号。必要时,还应增加一次性验证码或身份校验。

核实当前接口能力

Zoho Books 的在线支付能力会因数据中心、地区、订阅方案和已连接的支付网关而异。如果创建或查询发票的响应中包含官方托管付款页面 URL,可以直接通过 SMS 服务发送该 URL。如果响应中没有这个地址,就需要使用外部支付服务生成链接。无论采用哪种方式,SMS 都应由独立的短信服务发送。

备注:内容仅供参考。