PayZuDocs

无需反复查询支付是否到账,我们会在状态变更的瞬间通知您的服务器,如果您的系统未响应,会以间隔时间递增的方式重试最多 40 次。

什么是 webhook(callback)

webhook(也称为 callback)是当事件发生时,PayZu 向您的服务器发送POST 请求。与常规 API(由您调用 PayZu)相反,这里是反向的:由 PayZu 调用您。

设想一笔 Pix 收款。您创建了它,向客户展示了 QR,现在需要知道客户何时付款。有两个选择:

  1. 轮询,每隔 X 秒不停地问"付了吗?付了吗?"(成本高、慢、不必要)。
  2. webhook,让 PayZu 在款项到账时立即通知您(即时、高效、推荐)。

如何配置

您可以通过两种方式接收通知:

  • 已注册的 webhook(推荐):在 POST /user/webhooks 中注册一个持久化 URL,配置 HMAC 密钥和事件筛选。同一个 URL 适用于所有交易。
  • 每笔交易的 callbackUrl:在每笔创建的交易 body 的 callbackUrl 字段中传入 URL:
{
  "amount": 99.90,
  "callbackUrl": "https://your-site.com/webhooks/payzu",
  "clientReference": "pedido-2025-001"
}

每当该交易状态发生变化时(PENDING → COMPLETED,COMPLETED → REFUNDED 等),PayZu 都会向该 URL 发送 callback。

在您的服务器上创建一个公开的 endpoint

在互联网上可访问的位置,接收带 JSON 的 POST 请求。例如:https://your-site.com/webhooks/payzuhttps://api.your-company.com/payzu/callback

本地开发时,使用 ngrokCloudflare Tunnel 等隧道工具暴露 localhost

创建交易时传入 URL

在每个 POST /pixPOST /withdrawPOST /internal-transfer 中包含 callbackUrl 字段。所有交易可以使用相同的 URL。

实现 handler

接收 POST,读取 JSON,处理并在 5 秒内响应 2xx。参见 接收 Pix · 步骤 3 中的示例。

PayZu 发送 Content-Type: application/json。其他投递头信息见 投递的头信息

事件

POST /user/webhooksevents 字段定义哪些变化触发通知。留空则接收全部。

有七个事件对应交易的 status,每个值对应一个:

事件触发时机payload 中的 status
TRANSACTION_PENDING收款单已创建等待支付,或 Pix 支付进入处理中。内部转账不会经过 PENDINGPENDING
TRANSACTION_COMPLETED支付已确认。收款场景下,客户已付款;Pix 支付场景下,款项已发出。COMPLETED
TRANSACTION_CANCELED交易在完成前被取消,由手动操作或规则触发。CANCELED
TRANSACTION_WAITING_FOR_REFUND退款进入处理队列,通常在 MED 被接受后。WAITING_FOR_REFUND
TRANSACTION_REFUNDED款项已退还给付款方。REFUNDED
TRANSACTION_EXPIRED收款单超过 expiresIn 未被支付。EXPIRED
TRANSACTION_ERROR交易在处理中失败。ERROR

如果交易的 status 在队列处理事件之前发生变化,已注册 webhook 的投递 会被丢弃:没有发送、没有历史记录、也没有重试。在快速 Pix 中, TRANSACTION_PENDING 通常不会到达,因此不要要求先收到前一事件才 接受 TRANSACTION_COMPLETED

三个事件不对应 status

事件触发时机
INFRACTION_CHANGED与您的某笔交易关联的一个 MED 违规 被开启、状态被变更或被关闭。body 是交易的内容,附加 infraction 对象:交易 id 在 id,违规 id 在 infraction.id
TRANSACTION_SUSPECTED_FRAUD保留。目前没有服务发出此事件。
TRANSACTION_SUSPECTED_FRAUD_REVERSAL保留。目前没有服务发出此事件。

这两个可疑欺诈事件可以订阅,但目前没有任何投递会产生这些事件。 只订阅这两个事件的 webhook 什么也收不到。

重试系统

PayZu 的 webhook 拥有强大的重试系统,能够在临时故障下也确保 投递成功。PayZu 会以指数退避加抖动的方式最多重试 40 次发送 同一个 callback,从而更均匀地分散负载,避免请求峰值。

响应时间: webhook 必须在 5 秒内返回 2xx(例如 200204)。2xx 范围以外的响应(包括 4xx)和超时同样会 触发重试。

安全性

为保障完整性和安全性,请限制对您 webhook endpoint 的访问。 向支持团队索取 PayZu Processamento 的官方 IP,仅接受来自该来源 的 callback。

投递的头信息

头信息
Content-Typeapplication/json
User-AgentCallback-Service/1.0
X-Callback-Attempt本次投递的尝试次数。
X-Callback-Event触发投递的事件。仅在已注册的 webhook 中出现。
X-Callback-SignatureHMAC 签名。只要投递有密钥就会出现:已注册 webhook 的密钥,或账户的 callback 密钥。

INFRACTION_CHANGED 到达时交易的 status 保持不变,因此当您订阅 多个事件时,X-Callback-Event 是区分投递的依据。

HMAC 验证

每次签名的投递都带有 X-Callback-Signature。签名所用的密钥取决于目标:

投递目标签名密钥创建位置
已注册的 webhookwebhook 密钥POST /user/webhooks 中的 generateSecret: true,或 POST /user/webhooks/{id}/rotate-secret
交易的 callbackUrl账户的 callback 密钥POST /v1/user/callbacks/secret,通过 PATCH /v1/user/callbacks/secret/rotate 轮换

发送到交易 callbackUrl 的投递,只有在账户配置了 callback 密钥的情况下 才会签名。没有该密钥就没有签名:请创建密钥或通过来源 IP 保护 endpoint。

在处理 body 之前先验证签名:

读取 X-Callback-Signature 头。其值为 t=<timestamp>, v1=<签名>, 其中发送时间以秒(Unix)为单位,签名为 64 个字符的十六进制。

将 timestamp 与请求的原始 body 用 . 拼接形成基础字符串, 组成 <timestamp>.<body>

使用目标的密钥(webhook 密钥或账户的 callback 密钥) 对该字符串生成 HMAC SHA-256,并以恒定时间与 v1 的值比较。 如果不一致,则拒绝该投递。

同时拒绝超出容忍窗口的 timestamp。每次尝试都在发送时刻签名, 因此重试的 timestamp 始终是最新的。

Node.js 示例,使用 crypto.timingSafeEqual 以恒定时间比较签名:

const crypto = require("node:crypto");

const TOLERANCE_SECONDS = 300;

function verifyCallbackSignature(request, webhookSecret) {
  const header = request.headers["x-callback-signature"];
  if (typeof header !== "string") return false;

  const parts = Object.fromEntries(
    header.split(",").map((part) => part.trim().split("=")),
  );
  const timestamp = Number(parts.t);
  const signature = parts.v1;

  if (!Number.isInteger(timestamp) || !/^[0-9a-f]{64}$/i.test(signature ?? "")) {
    return false;
  }

  const age = Math.abs(Math.floor(Date.now() / 1000) - timestamp);
  if (age > TOLERANCE_SECONDS) return false;

  const expected = crypto
    .createHmac("sha256", webhookSecret)
    .update(`${timestamp}.${request.rawBody}`)
    .digest("hex");

  return crypto.timingSafeEqual(
    Buffer.from(expected, "hex"),
    Buffer.from(signature, "hex"),
  );
}

请在任何 JSON 解析之前,对原始请求 body 计算 HMAC,与接收时完全一致。

payload 字段

标识

字段类型描述
idstring交易 ID
clientReferencestring您提供的外部引用
virtualAccountstring虚拟子账户(最多 50 字符)。会在 callback 中返回,用于关联门店、分店、marketplace。
callbackUrlstring配置用于接收此 webhook 的 URL

状态与金额

字段类型描述
statusstringPENDINGCOMPLETEDCANCELEDWAITING_FOR_REFUNDREFUNDEDEXPIREDERROR
typestringDEPOSITWITHDRAWCOMMISSION
methodstringPIXBANK_SLIPINTERNAL_TRANSFER
amountnumber金额(BRL)
serviceFeeChargednumber收取的手续费

COMMISSION 标识贷记到您账户的佣金入账,会伴随 TRANSACTION_COMPLETED 到达。

生成的收款单(存款)

字段类型描述
qrCodeTextstringPix 复制粘贴代码
qrCodeUrlstringQR Code 图像的 URL
qrCodeBase64stringBase64 格式的 QR Code 图像
generatedNamestring引用名称
generatedDocumentstringCPF 或 CNPJ
generatedEmailstring与交易关联的邮箱

付款方

字段类型描述
payerNamestring付款方姓名
payerDocumentstring付款方证件
payerInstitutionIspbstring付款方银行的 ISPB
payerInstitutionNamestring付款方银行的名称
payerAccountNumberstring付款方的 PayZu 账户(6 位数字)。当由 PayZu 账户付款时填充:Pix 支付和内部转账。

收款方

字段类型描述
receiverNamestring收款方姓名
receiverDocumentstring收款方证件
receiverInstitutionIspbstring收款方银行的 ISPB
receiverInstitutionNamestring收款方银行的名称
receiverAccountNumberstring收款方的 PayZu 账户(6 位数字)。当由 PayZu 账户收款时填充:存款和内部转账。

通过 Pix 密钥的支付

字段类型描述
withdrawPixKeystring支付所使用的 Pix 密钥
withdrawPixTypestringcpfcnpjphoneemailevp

结算与退款

字段类型描述
endToEndIdstringPix 的 EndToEnd ID
paidAtstring支付时间戳(ISO 8601)
cancellationReasonstring取消原因
refundEndToEndIdstring退款的 EndToEnd ID
refundAmountstring退款金额
refundStatusstringPENDINGCOMPLETEDCANCELED
refundReasonstring退款原因
refundDescriptionstring退款描述
refundedAtstring退款时间戳(ISO 8601)

时间戳

字段类型描述
createdAtstring创建时间戳(ISO 8601)
updatedAtstring更新时间戳(ISO 8601)

违规(Pix 争议)

字段类型描述
infractionobject违规开启时的详细信息(参见 MED

最佳实践

  • 快速响应:在 5 秒内返回 2xx。将繁重的处理放在 队列/worker 中,不要在 handler 内做。
  • 幂等性:使用 id 加事件进行去重,而不仅仅是 id + status。 同一个 callback 可能到达多次(重试、连续变化),并且 INFRACTION_CHANGED 不会改变 status。参见 callback 去重
  • 使用 clientReference:在创建交易时传入外部标识符。 会在 callback 中返回,便于与您的订单关联。
  • 按 IP 限制:仅接受来自 PayZu 官方 IP 的 callback。
  • 返回 2xx 以结束投递2xx 范围以外的任何响应, 包括 4xx,以及任何超时都会进入同样的最多 40 次重试循环。要 停止重发,请返回 2xx 并在您这边处理错误。
  • 在日志中掩码 payerDocument:不做掩码就打印 payload 会带来 LGPD 风险。

测试与重发

本地测试

通过 ngrokCloudflare Tunnel 暴露您的 localhost,然后手动发送 payload:

curl -X POST https://your-tunnel.ngrok.io/webhooks/payzu \
  -H "Content-Type: application/json" \
  -d '{
    "id": "PAYZU20260811K7M2X9QP4T000000",
    "type": "DEPOSIT",
    "status": "COMPLETED",
    "amount": 99.90,
    "clientReference": "order-1234",
    "virtualAccount": "loja-rj-01",
    "paidAt": "2026-08-11T10:46:26.986Z"
  }'
await fetch('https://your-tunnel.ngrok.io/webhooks/payzu', {
  method: 'POST',
  headers: { 'Content-Type': 'application/json' },
  body: JSON.stringify({
    id: 'PAYZU20260811K7M2X9QP4T000000',
    type: 'DEPOSIT',
    status: 'COMPLETED',
    amount: 99.90,
    clientReference: 'order-1234',
    virtualAccount: 'loja-rj-01',
    paidAt: '2026-08-11T10:46:26.986Z',
  }),
});
import requests

requests.post(
    'https://your-tunnel.ngrok.io/webhooks/payzu',
    headers={'Content-Type': 'application/json'},
    json={
        'id': 'PAYZU20260811K7M2X9QP4T000000',
        'type': 'DEPOSIT',
        'status': 'COMPLETED',
        'amount': 99.90,
        'clientReference': 'order-1234',
        'virtualAccount': 'loja-rj-01',
        'paidAt': '2026-08-11T10:46:26.986Z',
    },
)

重发真实的 callback

重发的 endpoint 取决于目标 URL 的配置位置。

在交易中传入的 callbackUrl

两者仅覆盖已填写 callbackUrl 的交易,不会为已注册 webhook 生成投递。

已注册的 webhook:

按 webhook 重发会针对每对交易与事件重新处理一次投递, 将 300 起的响应和无响应视为失败。status 已与当前交易状态 不匹配的事件在重发时依然会被丢弃。

响应在 enqueued 中返回,包含 count(接受重发的总数)、truncateditemsitems 列表在 500 条时截断。当 truncatedtrue 时, 重发依然覆盖所有 count,只是响应中的列表被截断。

200 表示已接受重发,而非已入队投递:入队发生在 响应之后。而且该路由不再返回 count: 0200。如果 {webhookId} 没有活动的 webhook,会返回 404 PZW300;在期间 没有失败的 callback,则返回 404 PZW310

检查历史记录

PayZu 会保存所有投递尝试。用于排查失败很有用:

下一步

本页内容