跳到正文

API

本页内容

错误处理

一致地处理 Merchant API 错误:响应封装、HTTP 状态分组、机器可读错误码、请求限制与重试指引。

API 使用标准 HTTP 状态码。成功响应直接返回资源体。错误遵循 RFC 9457 Problem Details for HTTP APIs(application/problem+json),并带两个扩展成员:用于程序化判断的顶层 code,以及单字段校验错误上的顶层 field(snake_case)。当多个字段同时出错时,fielderrors[] 数组替代(Paymos 扩展成员,§3.2),承载逐字段的明细。

程序化处理和本地化时判断 code——绝不要判断 titledetail 文本。字符串字段可能在版本之间改写;code 才是稳定的契约。

{
  "type": "https://paymos.io/docs/errors/codes#insufficient_balance",
  "title": "Conflict",
  "status": 409,
  "detail": "Merchant balance is insufficient.",
  "code": "insufficient_balance"
}

每个错误响应都遵循 RFC 9457 Problem Details for HTTP APIs,以 Content-Type: application/problem+json 返回。

单错误(多数响应)

{
  "type": "https://paymos.io/docs/errors/codes#insufficient_balance",
  "title": "Conflict",
  "status": 409,
  "detail": "Merchant balance is insufficient.",
  "code": "insufficient_balance"
}

多字段校验

仅在多个字段同时校验失败时发出。顶层 code 恒为 validation_failed;逐字段明细在 errors[] 中:

{
  "type": "https://paymos.io/docs/errors/codes#validation_failed",
  "title": "Bad Request",
  "status": 400,
  "detail": "Validation failed.",
  "code": "validation_failed",
  "errors": [
    { "code": "field_required", "field": "currency", "message": "Currency is required." },
    { "code": "field_must_be_positive", "field": "amount", "message": "Amount must be > 0." }
  ]
}

字段参考

字段 类型 RFC 说明
type string (URI) RFC 9457 §3.1.1 指向具体 code 文档行的深链。每个错误码稳定不变——发布后永不更改。
title string RFC 9457 §3.1.3 简短的 HTTP 状态标题("Bad Request""Conflict"……)。
status integer RFC 9457 §3.1.2 HTTP 状态码(与响应状态一致)。
detail string RFC 9457 §3.1.4 本次具体错误的可读英文说明。
code string 扩展(§3.2) 稳定的机器可读标识符——见错误码。集成中以此为准做判断
field string 扩展(§3.2) 单字段校验错误的线上字段名(snake_case)。非字段错误时整个键省略——JSON 中不存在该键,而不是 null。仅在 errors[] 不存在时出现。
errors array 扩展(§3.2) 仅在多个字段同时出错时出现。每个条目为 { code, field, message }

为什么要结构化错误码(而不只是 message

messagedetail 文本可能在版本之间演进、被本地化或补充上下文。code 是稳定的契约——一个错误码发布后,其语义永不改变。程序化判断和本地化都基于 code

import { RateLimitError, ValidationError } from '@paymos/sdk';

try {
  await paymos.withdrawals.create(request);
} catch (error) {
  if (error instanceof ValidationError) {
    for (const item of error.errors) highlight(item.field, item.message);
  } else if (error instanceof RateLimitError) {
    scheduleRetry(error.retryAfterSeconds);
  }
  throw error;
}
use paymos::{ApiErrorKind, Error};

match paymos.withdrawals().create(&request).await {
    Ok(withdrawal) => process(withdrawal),
    Err(Error::Api(error)) if error.kind == ApiErrorKind::Validation => {
        for item in &error.errors {
            highlight(item.field.as_deref(), &item.message);
        }
    }
    Err(Error::Api(error)) if error.kind == ApiErrorKind::RateLimit => {
        schedule_retry(error.retry_after);
    }
    Err(error) => return Err(error),
}

错误类型

每个状态都随错误封装返回,带 title 和机器可读的 code。一个状态可能覆盖多种场景——按 code 分支,而不是按状态码。

HTTP 状态码 title 说明
400 Bad Request 输入校验失败或请求体无效。单字段失败在顶层携带 code + field;多字段失败填充 errors[]
401 Unauthorized HMAC 凭证缺失、格式错误、过期或无效。具体原因见 codeinvalid_credentialstimestamp_expiredauthorization_malformed 等)。
403 Forbidden 已认证,但密钥类型、环境或作用域不允许该操作。具体原因见 codemerchant_suspendedwhitelist_required 等)。
404 Not Found 资源不存在、对调用方不可见(属于其他商户的资源返回 not_found,绝不返回 forbidden),或没有端点匹配请求路径(route_not_found)。
405 Method Not Allowed 路径存在,但不支持此 HTTP 方法。携带 method_not_allowed 错误码。
409 Conflict 请求与当前状态冲突——非法状态迁移、资源已存在,或某个守卫阻止了操作。具体原因见 codeinsufficient_balancewithdrawal_quota_exceededinvoice_cannot_be_cancelled 等)。
410 Gone 资源不再可用(例如已过期或已取消的账单)。
413 Payload Too Large 请求体超过大小限制(1 MiB)。携带 payload_too_large 错误码。
415 Unsupported Media Type Content-Type 不是 application/json。携带 unsupported_media_type 错误码。
429 Too Many Requests 超过按商户计算的限流。重试前读取 Retry-After 请求头(秒)。
500 Internal Server Error 我们这边出了故障。以指数退避重试。
503 Service Unavailable 临时依赖或处理故障(exchange_rate_unavailableacceptance_disabledoutbound_frozen 等)。以退避方式重试。

客户端逻辑始终由 code(而非 HTTP 状态)驱动——多个不同的业务场景共用一个 HTTP 状态,线上错误码用来区分它们。

限流

API 默认允许每个商户每秒 30 次请求POST /v1/invoices 有更严格的限制(5 次/秒)。限流按商户、按一秒窗口计数——你持有的每个 API 密钥共用同一份预算——两者都可按商户配置。超过限制时会收到 429 Too Many Requests 响应,并携带 Retry-After: 1 请求头。

{
  "type": "https://paymos.io/docs/errors/codes#rate_limited",
  "title": "Too Many Requests",
  "status": 429,
  "detail": "Rate limit exceeded (30 req/sec). Retry after 1 second.",
  "code": "rate_limited"
}