Ir al contenido

API

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);