On this page
Authentication
Authenticate every Merchant API request with HMAC-SHA256: required headers, canonical payload construction, timestamps, and replay protection.
Every Paymos API request carries an HMAC-SHA256 signature over a canonical string. You generate the credentials in the dashboard — see API Keys for credential types, scopes, and lifecycle.
Signing scheme
Every API request carries two required headers and one optional header:
| Header | Description |
|---|---|
Authorization |
HMAC-SHA256 {apiKeyId}:{base64signature} — your API key ID (e.g. pk_live_...) and signature |
X-Request-Timestamp |
Current Unix timestamp (seconds) |
X-Correlation-Id |
(optional) Your correlation ID for request tracing |
Signing payload
The signature is Base64(HMAC-SHA256(api_secret, string_to_sign)) where:
string_to_sign = timestamp + "\n" + METHOD + "\n" + path + "\n" + query + "\n" + bodyHash
| Component | Description |
|---|---|
timestamp |
Same value as X-Request-Timestamp |
METHOD |
HTTP method in uppercase (POST, GET) |
path |
URL-decoded request path, without the query string (/v1/invoices) |
query |
Query string including ? (empty string if none) |
bodyHash |
Hex-encoded SHA-256 of request body (lowercase). Empty string if there is no body — do NOT hash an empty string |
When there is no body, bodyHash is the empty string. Do not hash the empty string: SHA-256("") returns a fixed non-empty digest, so the server computes a different string_to_sign than you did and rejects the signature.
Signature construction
Example: POST /v1/invoices with a JSON body
1709000000\nPOST\n/v1/invoices\n\n<sha256hex of body>
Here the query component is empty (no query parameters), and bodyHash is the lowercase hex SHA-256 digest of the request body.
Example: GET /v1/invoices/inv_74BPZFhr9qy9Uz2fbRkdJX
1709000000\nGET\n/v1/invoices/inv_74BPZFhr9qy9Uz2fbRkdJX\n\n
There is no query string and no request body, so the last two components are empty strings.
Anti-replay protection
The X-Request-Timestamp must be within an acceptable time window of the server clock (default ±5 minutes). Requests outside the window are rejected as unauthorized (timestamp_expired).
If your server clock may drift, read the server clock from the unauthenticated time endpoint and correct your offset before signing:
GET /v1/time
{ "server_time": 1739280600 }
server_time is the current server time in Unix seconds. No authentication is required. Compare it against your own clock and adjust the X-Request-Timestamp you send accordingly.
Secret rotation grace period
When you rotate your API secret, the previous secret remains valid for a grace period. During this window, signatures computed with either the current or previous secret are accepted. A new rotation cannot be initiated until the current grace period expires.
SDK example
The official SDK builds the canonical string and signs every request. Application code only supplies credentials and typed request fields.
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);