docs
集成指南

Webhook 重发

概述

Webhook 重发端点允许您请求手动重新发送特定交易的通知。这在以下场景中很有用:

  • 原始 webhook 发送时您的服务器不可用
  • 您需要重新处理特定交易
  • 您想临时使用不同的 URL 测试集成

POST /api/resend-webhook/:transactionIdentifier

需要在 Authorization header 中提供账户的 Bearer 令牌。

此端点不会更改您账户的 webhook 配置。提供的 URL 仅用于本次特定重发。

重发适用于交易事件(CashIn、CashOut、CashInReversal 和 CashOutReversal)。事件会根据交易类型自动选择。MED 事件不能通过此端点重发。


工作原理

交易标识

端点接受三种类型的标识符:

类型描述范围
数字 ID内部交易 ID(webhooks 中的 transactionId 字段)全局
外部 ID您在创建时提供的标识符(externalId 字段)每个账户唯一
End-to-End IDBACEN PIX 标识符(endToEndId 字段,格式:E/D + 32 个字符)每笔交易唯一

系统在您的账户中同时搜索所有标识符类型。实际上不存在歧义:数字 id 为纯数字;e2eId 以 'E' 或 'D' 开头后跟 32 个字母数字字符;externalId 为您提供的任意字符串。

使用哪个标识符? 使用 FyPay 返回的数字 transactionId、您在创建交易时提供的 externalId,或 webhooks 中收到的 PIX endToEndId。三者同样有效。

同步处理

Webhook 重发是同步处理的。这意味着:

  • 请求等待 webhook 交付完成
  • 结果通过 HTTP 状态码返回(200、502、504)
  • 响应时间取决于您服务器的延迟(每个 URL 超时 10 秒)
flowchart TD
    A[Resend Request] --> B{URL provided?}
    B -->|Yes| C[Use temporary URL]
    B -->|No| D{Webhook configured?}
    D -->|Yes| E[Use configured URLs]
    D -->|No| F[Error 400: No URL]
    C --> G[Send Webhook HTTP]
    E --> G
    G --> H{Server response}
    H -->|2xx| I[HTTP 200: Success]
    H -->|4xx/5xx| J[HTTP 502: Bad Gateway]
    H -->|Timeout| K[HTTP 504: Gateway Timeout]
    I --> L[Record audit log]
    J --> L
    K --> L
    L --> M[Return result]

与自动 webhooks(使用带重试的队列)不同,手动重发是立即执行的,并在同一请求中返回结果。

重发与原始 webhook 走相同的发送流程:payload 会根据交易的当前状态重新构建,并使用账户当前的 HMAC 密钥在 X-Avista-Signature header 中签名。尚未在面板中生成密钥的账户收到的重发不带该 header,与自动 webhook 一致。


使用场景

1. 重发到已配置的 URL

如果您的账户已配置 webhook,只需调用端点,无需请求体:

curl -X POST https://api.fypay.me/api/resend-webhook/external-teste-001 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json"

2. 使用临时 URL 重发

要使用不同的 URL 测试或重发到备用端点:

curl -X POST https://api.fypay.me/api/resend-webhook/external-teste-001 \
  -H "Authorization: Bearer YOUR_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://my-backup-server.com/webhooks/fypay"
  }'

临时 URL 不会被持久化。下一次自动 webhook 将发送到账户配置的 URL。临时 URL 必须是公共地址(http 或 https);私有或内部地址会被拒绝并返回 400。

如果请求体中未提供 url,重发会发送到为该事件配置的所有活跃 URL,每个 URL 使用各自的自定义 headers 和 payload 版本。如果提供了 url,重发使用为该事件配置的第一个活跃 URL 的自定义 headers 和 payload 版本。


响应

成功 (200)

Webhook 已成功发送到所有目标 URL。当存在多个 URL 时,消息会注明数量(例如 "Webhook resent successfully to 2 URLs"),其余字段对应第一个 URL。

{
  "message": "Webhook resent successfully",
  "webhookLogId": 12345,
  "sentAt": "2024-01-15T10:30:00.000Z",
  "statusCode": 200
}

错误:未配置 URL (400)

{
  "statusCode": 400,
  "message": "No webhook URL available. Configure a webhook for this operation type or provide an override URL.",
  "error": "Bad Request"
}

当 url 不是有效 URL 或指向私有地址时,也会返回 400。令牌缺失或无效时返回 401。

错误:交易未找到 (404)

{
  "statusCode": 404,
  "message": "Transaction not found",
  "error": "Not Found"
}

错误:目标返回错误 (502)

目标服务器返回了 2xx 以外的状态码,或连接失败。存在多个 URL 时,只要有一个失败就会返回 502;各字段对应第一个失败的 URL。

{
  "statusCode": 502,
  "message": "Webhook failed with status 500",
  "webhookLogId": 12345,
  "sentAt": "2024-01-15T10:30:00.000Z"
}

即使在错误情况下,webhook 也会记录在审计日志中。使用 webhookLogId 进行追踪。

错误:超时 (504)

目标服务器未在时限内响应(10 秒)。

{
  "statusCode": 504,
  "message": "Timeout after 10000ms",
  "webhookLogId": 12345,
  "sentAt": "2024-01-15T10:30:00.000Z"
}

如果频繁遇到超时,请检查您的服务器是否能在 10 秒内响应。


速率限制

此端点每个账户的速率限制为每分钟 60 次请求,以防止滥用。

如果超出限制,您将收到 429 Too Many Requests 错误。请等待 Retry-After header 指示的时间后再重试。

{
  "statusCode": 429,
  "message": "ThrottlerException: Too Many Requests"
}

审计

所有手动重发都会被记录以用于审计和追溯:

信息描述
发送类型标记为手动重发
使用的 URL记录是临时 URL 还是已配置的 URL
结果HTTP 状态和响应时间
标识符用于追踪的唯一日志 ID

使用响应中返回的 webhookLogId 与支持日志进行关联(如需要)。


集成示例

const axios = require('axios');

async function resendWebhook(transactionId, overrideUrl = null) {
  const config = {
    headers: {
      'Authorization': `Bearer ${process.env.FYPAY_TOKEN}`,
      'Content-Type': 'application/json'
    }
  };

  const body = overrideUrl ? { url: overrideUrl } : {};

  try {
    const response = await axios.post(
      `https://api.fypay.me/api/resend-webhook/${transactionId}`,
      body,
      config
    );

    console.log('Webhook resent:', response.data);
    return response.data;
  } catch (error) {
    console.error('Error resending webhook:', error.response?.data);
    throw error;
  }
}

// Usage
resendWebhook('external-teste-001');
resendWebhook('external-teste-001', 'https://backup.mysite.com/webhook'); // With temporary URL
import requests
import os

def resend_webhook(transaction_id: str, override_url: str = None):
    headers = {
        'Authorization': f'Bearer {os.environ["FYPAY_TOKEN"]}',
        'Content-Type': 'application/json'
    }

    body = {'url': override_url} if override_url else {}

    response = requests.post(
        f'https://api.fypay.me/api/resend-webhook/{transaction_id}',
        json=body,
        headers=headers
    )

    response.raise_for_status()
    return response.json()

# Usage
result = resend_webhook('external-teste-001')
print(f"Webhook resent: {result}")

# With temporary URL
result = resend_webhook('external-teste-001', 'https://backup.mysite.com/webhook')
using System.Net.Http;
using System.Text;
using System.Text.Json;

public class FyPayClient
{
    private readonly HttpClient _client;
    private readonly string _token;

    public FyPayClient(string token)
    {
        _client = new HttpClient();
        _token = token;
        _client.DefaultRequestHeaders.Add("Authorization", $"Bearer {_token}");
    }

    public async Task<ResendWebhookResponse> ResendWebhookAsync(
        string transactionId,
        string overrideUrl = null)
    {
        var url = $"https://api.fypay.me/api/resend-webhook/{transactionId}";

        var body = overrideUrl != null
            ? JsonSerializer.Serialize(new { url = overrideUrl })
            : "{}";

        var content = new StringContent(body, Encoding.UTF8, "application/json");

        var response = await _client.PostAsync(url, content);
        response.EnsureSuccessStatusCode();

        var json = await response.Content.ReadAsStringAsync();
        return JsonSerializer.Deserialize<ResendWebhookResponse>(json);
    }
}

后续步骤

本页目录