En esta página
Autenticación
Autentica cada petición de la Merchant API con HMAC-SHA256: cabeceras obligatorias, cadena canónica, marcas de tiempo y protección contra reenvíos.
Cada petición a la API de Paymos lleva una firma HMAC-SHA256 sobre una cadena canónica. Las credenciales se generan en el panel: los tipos, alcances y ciclo de vida están en Claves de API.
Esquema de firma
Cada petición a la API lleva dos cabeceras obligatorias y una opcional:
| Cabecera | Descripción |
|---|---|
Authorization |
HMAC-SHA256 {apiKeyId}:{base64signature} — el ID de tu clave de API (p. ej. pk_live_...) y la firma |
X-Request-Timestamp |
Marca de tiempo Unix actual (segundos) |
X-Correlation-Id |
(opcional) Tu ID de correlación para trazar la petición |
Cadena que se firma
La firma es Base64(HMAC-SHA256(api_secret, string_to_sign)), donde:
string_to_sign = timestamp + "\n" + METHOD + "\n" + path + "\n" + query + "\n" + bodyHash
| Componente | Descripción |
|---|---|
timestamp |
El mismo valor que X-Request-Timestamp |
METHOD |
Método HTTP en mayúsculas (POST, GET) |
path |
Ruta de la petición decodificada de URL, sin la cadena de consulta (/v1/invoices) |
query |
Cadena de consulta incluido el ? (cadena vacía si no hay) |
bodyHash |
SHA-256 del cuerpo de la petición en hexadecimal minúsculo. Cadena vacía si no hay cuerpo: NO calcules el hash de una cadena vacía |
Si no hay cuerpo, bodyHash es la cadena vacía. No calcules el hash de la cadena vacía: SHA-256("") devuelve un hash fijo y no vacío, así que el servidor compone un string_to_sign distinto del tuyo y rechaza la firma.
Construcción de la firma
Ejemplo: POST /v1/invoices con cuerpo JSON
1709000000\nPOST\n/v1/invoices\n\n<sha256hex of body>
Aquí el componente query está vacío (no hay parámetros de consulta) y bodyHash es el SHA-256 del cuerpo de la petición en hexadecimal minúsculo.
Ejemplo: GET /v1/invoices/inv_74BPZFhr9qy9Uz2fbRkdJX
1709000000\nGET\n/v1/invoices/inv_74BPZFhr9qy9Uz2fbRkdJX\n\n
No hay cadena de consulta ni cuerpo de petición, así que los dos últimos componentes son cadenas vacías.
Protección contra reenvíos
X-Request-Timestamp debe caer dentro de una ventana aceptable respecto al reloj del servidor (±5 minutos por defecto). Las peticiones fuera de la ventana se rechazan como no autorizadas (timestamp_expired).
Si el reloj de tu servidor puede desviarse, lee la hora del servidor en el endpoint de tiempo sin autenticación y corrige tu desfase antes de firmar:
GET /v1/time
{ "server_time": 1739280600 }
server_time es la hora actual del servidor en segundos Unix. No requiere autenticación. Compárala con tu propio reloj y ajusta el X-Request-Timestamp que envías.
Periodo de gracia al rotar el secreto
Cuando rotas tu secreto de API, el anterior sigue siendo válido durante un periodo de gracia. Mientras dura esa ventana se aceptan las firmas calculadas con el secreto actual y con el anterior. No puedes iniciar una nueva rotación hasta que venza el periodo de gracia en curso.
Ejemplo con el SDK
El SDK oficial construye la cadena canónica y firma cada petición. Tu código solo aporta las credenciales y los campos tipados de la petición.
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);