Skip to content

API

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