跳到正文

API

本页内容

身份认证

用 HMAC-SHA256 认证每个 Merchant API 请求:必需请求头、规范化载荷构造、时间戳与防重放保护。

每个 Paymos API 请求都携带对一段规范化字符串计算的 HMAC-SHA256 签名。凭证在控制台生成——凭证类型、作用域与生命周期见 API 密钥

签名方案

每个 API 请求携带两个必需请求头和一个可选请求头:

请求头 说明
Authorization HMAC-SHA256 {apiKeyId}:{base64signature}——你的 API key ID(如 pk_live_...)和签名
X-Request-Timestamp 当前 Unix 时间戳(秒)
X-Correlation-Id (可选) 你自己的关联 ID,用于请求追踪

签名载荷

签名为 Base64(HMAC-SHA256(api_secret, string_to_sign)),其中:

string_to_sign = timestamp + "\n" + METHOD + "\n" + path + "\n" + query + "\n" + bodyHash
组成部分 说明
timestamp X-Request-Timestamp 相同的值
METHOD 大写 HTTP 方法(POSTGET
path URL 解码后的请求路径,不含查询字符串(/v1/invoices
query 包含 ? 的查询字符串(没有则为空字符串)
bodyHash 请求体的十六进制 SHA-256(小写)。没有请求体时为空字符串——不要对空字符串做哈希

没有请求体时,bodyHash 就是空字符串。不要对空字符串做哈希:SHA-256("") 返回一个固定的非空摘要,服务器计算出的 string_to_sign 会与你的不同,从而拒绝签名。

签名构造

示例:带 JSON 请求体的 POST /v1/invoices

1709000000\nPOST\n/v1/invoices\n\n<sha256hex of body>

此处 query 部分为空(无查询参数),bodyHash 为请求体的小写十六进制 SHA-256 摘要。

示例:GET /v1/invoices/inv_74BPZFhr9qy9Uz2fbRkdJX

1709000000\nGET\n/v1/invoices/inv_74BPZFhr9qy9Uz2fbRkdJX\n\n

没有查询字符串也没有请求体,因此最后两个部分为空字符串。

防重放保护

X-Request-Timestamp 必须在服务器时钟的可接受时间窗口内(默认 ±5 分钟)。超出窗口的请求会以未授权(timestamp_expired)被拒绝。

如果你的服务器时钟可能漂移,先从未认证的时间端点读取服务器时钟,校正偏移后再签名:

GET /v1/time
{ "server_time": 1739280600 }

server_time 为当前服务器时间(Unix 秒)。无需认证。将它与你自己的时钟比较,并相应调整发送的 X-Request-Timestamp

Secret 轮换宽限期

轮换 API secret 后,旧 secret 在宽限期内仍然有效。此窗口内,用当前 secret 或旧 secret 计算的签名都会被接受。当前宽限期结束前无法发起新的轮换。

SDK 示例

官方 SDK 会构造规范化字符串并为每个请求签名。应用代码只需提供凭证和类型化的请求字段。

API_KEY_ID="pk_live_xxxxxxxxxxxx"
API_SECRET="sk_live_xxxxxxxxxxxx"
BODY='{"project_id":"prj_xxxxxxxxxxxx","amount":"100.00","currency":"USD","external_order_id":"order-123","client_id":"customer-456"}'

TS=$(date +%s)
BODY_HASH=$(printf '%s' "$BODY" | openssl dgst -sha256 -hex | sed 's/^.* //')
SIGNATURE=$(printf '%s\n%s\n%s\n%s\n%s' "$TS" POST /v1/invoices '' "$BODY_HASH" \
  | openssl dgst -sha256 -hmac "$API_SECRET" -binary | base64)

curl -sS https://api.paymos.io/v1/invoices \
  -H "Authorization: HMAC-SHA256 $API_KEY_ID:$SIGNATURE" \
  -H "X-Request-Timestamp: $TS" \
  -H "Content-Type: application/json" \
  -d "$BODY"
import { Paymos, externalOrderId } from '@paymos/sdk';

const paymos = new Paymos({
  apiKey: process.env.PAYMOS_API_KEY,
  apiSecret: process.env.PAYMOS_API_SECRET,
});

const invoice = await paymos.invoices.create({
  projectId: 'prj_xxxxxxxxxxxx',
  amount: '100.00',
  currency: 'USD',
  externalOrderId: externalOrderId('order'),
  clientId: 'customer-456',
});

console.log(invoice.invoiceId, invoice.paymentUrl);
import os

from paymos import Paymos

paymos = Paymos(
    api_key=os.environ["PAYMOS_API_KEY"],
    api_secret=os.environ["PAYMOS_API_SECRET"],
)
invoice = paymos.invoices.create(
    project_id="prj_xxxxxxxxxxxx",
    amount="100.00",
    currency="USD",
    external_order_id="order-123",
    client_id="customer-456",
)

print(invoice["invoice_id"], invoice["payment_url"])
<?php
use Paymos\Client;
use Paymos\ClientConfig;
use Paymos\IdempotencyKey;

$paymos = new Client(new ClientConfig(
    getenv('PAYMOS_API_KEY'),
    getenv('PAYMOS_API_SECRET')
));
$invoice = $paymos->invoices()->create(array(
    'project_id' => 'prj_xxxxxxxxxxxx',
    'amount' => '100.00',
    'currency' => 'USD',
    'external_order_id' => IdempotencyKey::externalOrderId('order'),
    'client_id' => 'customer-456',
));

echo $invoice['invoice_id'] . ' ' . $invoice['payment_url'];
package main

import (
	"context"
	"fmt"
	"os"

	paymos "github.com/Paymos-labs/go-sdk/v2"
)

func main() {
	client, err := paymos.NewClient(os.Getenv("PAYMOS_API_KEY"), os.Getenv("PAYMOS_API_SECRET"))
	if err != nil {
		panic(err)
	}
	invoice, err := client.Invoices.Create(context.Background(), paymos.CreateInvoiceParams{
		ProjectID:       "prj_xxxxxxxxxxxx",
		Amount:          "100.00",
		Currency:        "USD",
		ExternalOrderID: "order-123",
		ClientID:        stringPointer("customer-456"),
	})
	if err != nil {
		panic(err)
	}
	fmt.Println(invoice.InvoiceID, invoice.PaymentURL)
}

func stringPointer(value string) *string { return &value }
using Paymos;

using var paymos = new PaymosClient(
    Environment.GetEnvironmentVariable("PAYMOS_API_KEY")!,
    Environment.GetEnvironmentVariable("PAYMOS_API_SECRET")!);

var invoice = await paymos.Invoices.CreateAsync(new CreateInvoiceRequest(
    ProjectId: "prj_xxxxxxxxxxxx",
    Amount: "100.00",
    Currency: "USD",
    ExternalOrderId: "order-123",
    ClientId: "customer-456"));

Console.WriteLine($"{invoice.InvoiceId} {invoice.PaymentUrl}");
import io.paymos.CreateInvoiceRequest;
import io.paymos.Invoice;
import io.paymos.PaymosClient;

PaymosClient paymos = new PaymosClient(
    System.getenv("PAYMOS_API_KEY"),
    System.getenv("PAYMOS_API_SECRET"));

Invoice invoice = paymos.invoices.create(
    CreateInvoiceRequest.builder()
        .projectId("prj_xxxxxxxxxxxx")
        .amount("100.00")
        .currency("USD")
        .externalOrderId("order-123")
        .clientId("customer-456")
        .build());

System.out.println(invoice.invoiceId() + " " + invoice.paymentUrl());
require 'paymos'

paymos = Paymos::Client.new(
  api_key: ENV.fetch('PAYMOS_API_KEY'),
  api_secret: ENV.fetch('PAYMOS_API_SECRET')
)
invoice = paymos.invoices.create(
  project_id: 'prj_xxxxxxxxxxxx',
  amount: '100.00',
  currency: 'USD',
  external_order_id: 'order-123',
  client_id: 'customer-456'
)

puts "#{invoice.invoice_id} #{invoice.payment_url}"
use paymos::{CreateInvoiceRequest, PaymosClient};

let paymos = PaymosClient::new(
    std::env::var("PAYMOS_API_KEY")?,
    std::env::var("PAYMOS_API_SECRET")?,
)?;
let invoice = paymos
    .invoices()
    .create(&CreateInvoiceRequest {
        project_id: "prj_xxxxxxxxxxxx".to_owned(),
        amount: "100.00".to_owned(),
        currency: "USD".to_owned(),
        external_order_id: "order-123".to_owned(),
        network: None,
        allow_multiple_payments: None,
        customer_fee_percent: None,
        client_id: Some("customer-456".to_owned()),
    })
    .await?;

println!("{} {}", invoice.invoice_id, invoice.payment_url);