Authentication

Sign every API request with an RSA key pair using HTTP Message Signatures (RFC 9421).


BEEM authenticates every API request with an RSA digital signature over the request itself, following the HTTP Message Signatures standard (RFC 9421).

This is stronger than a static bearer token: the signature covers the HTTP method, target URL, request body and a timestamp, so a captured request cannot be tampered with or replayed.

📘

Three things you need before your first call

  1. An RSA key pair generated locally.
  2. The public key registered against an API client in the Portal.
  3. The clientId that the Portal returns once the public key is saved.

How it works

ComponentLivesPurpose
Private keyIn your application (vault / HSM / secret manager)Signs each outbound request. Never leaves your environment.
Public keyRegistered against an API client in the BEEM PortalUsed by BEEM to verify your signature.
clientIdIssued by the Portal when the public key is savedIdentifies which registered key signed a given request. Sent in the Signature-Input header as keyid.

Each request carries four signature-related headers: Content-Digest, Date, Signature-Input, and Signature. BEEM verifies them on every call.




Step 1 — Generate an RSA key pair

Run the two commands below on the machine that will host your integration. The private key never leaves that environment.

  1. Generate the private key. Keep this file secret — anyone with it can sign requests as you.
   openssl genrsa -out api-client-key.pem 2048
  1. Extract the public key. This is the only file you will upload to BEEM.
   openssl rsa -in api-client-key.pem -pubout -out api-client-pub.pem
🚧

Treat api-client-key.pem like a password

Store it in a secrets manager (AWS Secrets Manager, HashiCorp Vault, 1Password, etc.) and restrict file permissions on disk with chmod 600 api-client-key.pem. If the private key is ever exposed, rotate it immediately by registering a new public key in the Portal and removing the old one.


Step 2 — Register the public key in the Portal

The public key is registered against an API client on your account. Each client carries its own public key, IP allowlist and role assignment.

  1. Sign in to the Account Portal.

  2. Go to Settings → Clients.

  3. Click +Create a Client to create a new client (or click Edit on an existing client to attach the key to it).

    Settings → Clients. Click + Create a client to start a new one.


    Settings → Clients. Click the pencil icon to edit an existing client.

  4. Complete the fields:

    • Name — a label that identifies the integration (e.g. prod-checkout).

    • Network whitelist — the IP addresses allowed to use this client. Click Add for each. To allow all IPs, enter 0.0.0.0.

      Network whitelist. Each address must be in CIDR format.

    • Public key — paste the contents of api-client-public-key.pem, including the -----BEGIN PUBLIC KEY----- and -----END PUBLIC KEY----- lines.

      Paste the full PEM block, including the BEGIN and END lines.

    • Assign roles to user — pick the roles that scope what this client can do (e.g. read-only, payments, withdrawals).

      Assign roles, confirm Enabled is on, then click Add.

  5. Click Add (or Update, if editing).

⚠️

Restrict the IP allow list whenever possible

0.0.0.0 allows traffic from anywhere. Use it only for early sandbox testing. Production clients should list the static egress IPs of your servers.


Step 3 — Get your clientId

The clientId is the identifier BEEM uses to look up the public key when verifying a signature. It is sent on every request as the keyid parameter inside Signature-Input.

  1. In the Portal, go to Settings → Clients.

  2. Find the client created in the previous step.

  3. Click the copy icon next to its clientId and store it alongside the private key.

    The clientId is shown in the Overview side panel and is also visible in the Client Id column of the Clients list.

🚧

No access to Settings → Clients?

If your role does not allow you to manage clients, send the public key and the IP addresses to your Solutions Manager and ask them to register the client and return the clientId.


Step 4 — Sign each request

Every signed request must include four headers:

HeaderWhat it contains
Content-DigestThe SHA-256 digest of the request body, base64-encoded. Only present on requests with a body.
DateThe current time in HTTP-date format (RFC 7231). Used to mitigate replay attacks.
Signature-InputThe list of signed components, the creation timestamp, the keyid (your clientId), and the signature algorithm (rsa-v1_5-sha256).
SignatureThe base64-encoded RSA signature over the canonical signature base.

The signed components are:

  • @method — the HTTP method (POST, GET, …).
  • @target-uri — the full request URL.
  • content-digest — the digest header above (omitted when there is no body).
  • date — the date header above.

Example signed request

POST /api/transaction HTTP/1.1
Host: api.sandbox.layer1.com
Date: Tue, 27 May 2025 10:21:54 GMT
Content-Type: application/json
Content-Digest: sha-256=:W6ph5Mm5Pz8GgiULbPgzG37mj9g=:
Signature-Input: sig=("@method" "@target-uri" "content-digest" "date");created=1716792114;keyid="client-123";alg="rsa-v1_5-sha256"
Signature: sig=:MEUCIQDn...==:


Reference signers

The classes below provide the same interface in six languages. The constructor takes (privateKey, clientId); the single public method buildHeaders(url, payload, method) returns the four signature headers (Content-Digest, Date, Signature-Input, Signature). All six produce byte-identical signatures for the same input, so you can use any of them to call the same BEEM endpoints.

📘

Private key format

Every signer expects the private key in PKCS#8 format (the PEM block starts with -----BEGIN PRIVATE KEY-----). If your version of OpenSSL produced a PKCS#1 key (-----BEGIN RSA PRIVATE KEY-----), convert it once with:

openssl pkcs8 -topk8 -nocrypt -in api-client-key.pem -out api-client-key-pkcs8.pem
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.MessageDigest;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Locale;
import java.util.Map;

public class HttpSigner {
    private static final String SIGNATURE_ALGORITHM = "rsa-v1_5-sha256";
    private static final String DIGEST_ALGORITHM = "sha-256";
    private static final DateTimeFormatter HTTP_DATE =
        DateTimeFormatter.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.US)
                         .withZone(ZoneOffset.UTC);

    private final PrivateKey privateKey;
    private final String clientId;

    public HttpSigner(String base64PrivateKey, String clientId) throws Exception {
        this.clientId = clientId;
        this.privateKey = loadPrivateKey(base64PrivateKey);
    }

    private PrivateKey loadPrivateKey(String rawKey) throws Exception {
        String cleanKey = rawKey
            .replace("-----BEGIN PRIVATE KEY-----", "")
            .replace("-----END PRIVATE KEY-----", "")
            .replaceAll("\\s+", "");
        byte[] keyBytes = Base64.getDecoder().decode(cleanKey);
        PKCS8EncodedKeySpec spec = new PKCS8EncodedKeySpec(keyBytes);
        KeyFactory kf = KeyFactory.getInstance("RSA");
        return kf.generatePrivate(spec);
    }

    public Map<String, String> buildHeaders(String url, String payload, String method) throws Exception {
        Map<String, String> headers = new LinkedHashMap<>();

        String contentDigest = null;
        if (payload != null && !payload.isEmpty()) {
            contentDigest = createDigest(DIGEST_ALGORITHM, payload);
            headers.put("Content-Digest", contentDigest);
        }

        long now = Instant.now().getEpochSecond();
        String dateHeader = HTTP_DATE.format(Instant.ofEpochSecond(now));
        headers.put("Date", dateHeader);

        String signatureParameters = createSignatureParameters(contentDigest, now);
        headers.put("Signature-Input", "sig=" + signatureParameters);

        StringBuilder base = new StringBuilder();
        base.append("\"@method\": ").append(method.toUpperCase(Locale.US)).append("\n");
        base.append("\"@target-uri\": ").append(url).append("\n");
        if (contentDigest != null) {
            base.append("\"content-digest\": ").append(contentDigest).append("\n");
        }
        base.append("\"date\": ").append(dateHeader).append("\n");
        base.append("\"@signature-params\": ").append(signatureParameters);

        String signature = sign(base.toString());
        headers.put("Signature", "sig=:" + signature + ":");

        return headers;
    }

    private String createSignatureParameters(String contentDigest, long createdTimestamp) {
        StringBuilder components = new StringBuilder("(\"@method\" \"@target-uri\"");
        if (contentDigest != null) {
            components.append(" \"content-digest\"");
        }
        components.append(" \"date\"");

        return String.format(
            "%s);created=%d;keyid=\"%s\";alg=\"%s\"",
            components,
            createdTimestamp,
            clientId,
            SIGNATURE_ALGORITHM
        );
    }

    private String sign(String signatureBase) throws Exception {
        Signature signer = Signature.getInstance("SHA256withRSA");
        signer.initSign(privateKey);
        signer.update(signatureBase.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(signer.sign());
    }

    private String createDigest(String digestAlgorithm, String data) throws Exception {
        return String.format("%s=:%s:", digestAlgorithm, getDigest(digestAlgorithm, data));
    }

    private String getDigest(String algorithm, String data) throws Exception {
        MessageDigest md = MessageDigest.getInstance(algorithm.toUpperCase(Locale.US));
        byte[] hash = md.digest(data.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(hash);
    }
}
class HttpSigner {
    private const SIGNATURE_ALGORITHM = 'rsa-v1_5-sha256';
    private const DIGEST_ALGORITHM = 'sha-256';

    private $privateKey;
    private $clientId;

    public function __construct(string $base64PrivateKey, string $clientId) {
        $this->clientId = $clientId;
        $this->privateKey = $this->loadPrivateKey($base64PrivateKey);
    }

    private function loadPrivateKey(string $key) {
        $preparedKey = $this->prepareKey($key);
        $privateKey = openssl_pkey_get_private($preparedKey);

        if ($privateKey === false) {
            throw new RuntimeException('Failed to load private key: ' . openssl_error_string());
        }

        return $privateKey;
    }

    private function prepareKey(string $rawKey): string {
        if (strpos($rawKey, '-----BEGIN PRIVATE KEY-----') !== false) {
            return $rawKey;
        }

        $cleanKey = preg_replace('/\s+/', '', $rawKey);
        return "-----BEGIN PRIVATE KEY-----\n" .
               chunk_split($cleanKey, 64, "\n") .
               "-----END PRIVATE KEY-----\n";
    }

    public function buildHeaders(string $url, ?string $payload, string $method): array {
        $headers = [];

        $contentDigest = null;
        if (!empty($payload)) {
            $contentDigest = $this->createDigest(self::DIGEST_ALGORITHM, $payload);
            $headers['Content-Digest'] = $contentDigest;
        }

        $now = time();
        $dateHeader = gmdate('D, d M Y H:i:s', $now) . ' GMT';
        $headers['Date'] = $dateHeader;

        $signatureParameters = $this->createSignatureParameters($contentDigest, $now);
        $headers['Signature-Input'] = 'sig=' . $signatureParameters;

        $signatureBase = sprintf(
            "\"@method\": %s\n\"@target-uri\": %s\n%s\"date\": %s\n\"@signature-params\": %s",
            strtoupper($method),
            $url,
            $contentDigest === null ? '' : "\"content-digest\": " . $contentDigest . "\n",
            $dateHeader,
            $signatureParameters
        );

        $signature = $this->sign($signatureBase);
        $headers['Signature'] = sprintf('sig=:%s:', $signature);

        return $headers;
    }

    private function createSignatureParameters(?string $contentDigest, int $createdTimestamp): string {
        $components = '("@method" "@target-uri"';
        if ($contentDigest !== null) {
            $components .= ' "content-digest"';
        }
        $components .= ' "date"';

        return sprintf(
            '%s);created=%d;keyid="%s";alg="%s"',
            $components,
            $createdTimestamp,
            $this->clientId,
            self::SIGNATURE_ALGORITHM
        );
    }

    private function sign(string $signatureBase): string {
        $signature = '';
        $success = openssl_sign(
            $signatureBase,
            $signature,
            $this->privateKey,
            OPENSSL_ALGO_SHA256
        );

        if (!$success) {
            throw new RuntimeException('Failed to sign request: ' . openssl_error_string());
        }

        return base64_encode($signature);
    }

    private function createDigest(string $digestAlgorithm, string $data): string {
        return sprintf('%s=:%s:', $digestAlgorithm, $this->getDigest($digestAlgorithm, $data));
    }

    private function getDigest(string $algorithm, string $data): string {
        $phpAlgorithm = str_replace('-', '', $algorithm);
        $hash = hash($phpAlgorithm, $data, true);
        return base64_encode($hash);
    }
}
using System;
using System.Collections.Generic;
using System.Globalization;
using System.Security.Cryptography;
using System.Text;

public class HttpSigner
{
    private const string SignatureAlgorithm = "rsa-v1_5-sha256";
    private const string DigestAlgorithm = "sha-256";

    private readonly RSA _privateKey;
    private readonly string _clientId;

    public HttpSigner(string base64PrivateKey, string clientId)
    {
        _clientId = clientId;
        _privateKey = LoadPrivateKey(base64PrivateKey);
    }

    private static RSA LoadPrivateKey(string rawKey)
    {
        string cleanKey = rawKey
            .Replace("-----BEGIN PRIVATE KEY-----", "")
            .Replace("-----END PRIVATE KEY-----", "")
            .Replace("\r", "").Replace("\n", "").Replace(" ", "");
        byte[] keyBytes = Convert.FromBase64String(cleanKey);
        var rsa = RSA.Create();
        rsa.ImportPkcs8PrivateKey(keyBytes, out _);
        return rsa;
    }

    public Dictionary<string, string> BuildHeaders(string url, string payload, string method)
    {
        var headers = new Dictionary<string, string>();

        string contentDigest = null;
        if (!string.IsNullOrEmpty(payload))
        {
            contentDigest = CreateDigest(DigestAlgorithm, payload);
            headers["Content-Digest"] = contentDigest;
        }

        long now = DateTimeOffset.UtcNow.ToUnixTimeSeconds();
        string dateHeader = DateTimeOffset.FromUnixTimeSeconds(now)
            .ToString("ddd, dd MMM yyyy HH:mm:ss 'GMT'", CultureInfo.InvariantCulture);
        headers["Date"] = dateHeader;

        string signatureParameters = CreateSignatureParameters(contentDigest, now);
        headers["Signature-Input"] = "sig=" + signatureParameters;

        var sb = new StringBuilder();
        sb.Append($"\"@method\": {method.ToUpperInvariant()}\n");
        sb.Append($"\"@target-uri\": {url}\n");
        if (contentDigest != null)
        {
            sb.Append($"\"content-digest\": {contentDigest}\n");
        }
        sb.Append($"\"date\": {dateHeader}\n");
        sb.Append($"\"@signature-params\": {signatureParameters}");

        string signature = Sign(sb.ToString());
        headers["Signature"] = $"sig=:{signature}:";

        return headers;
    }

    private string CreateSignatureParameters(string contentDigest, long createdTimestamp)
    {
        var sb = new StringBuilder("(\"@method\" \"@target-uri\"");
        if (contentDigest != null)
        {
            sb.Append(" \"content-digest\"");
        }
        sb.Append(" \"date\"");

        return $"{sb});created={createdTimestamp};keyid=\"{_clientId}\";alg=\"{SignatureAlgorithm}\"";
    }

    private string Sign(string signatureBase)
    {
        byte[] data = Encoding.UTF8.GetBytes(signatureBase);
        byte[] signature = _privateKey.SignData(data, HashAlgorithmName.SHA256, RSASignaturePadding.Pkcs1);
        return Convert.ToBase64String(signature);
    }

    private static string CreateDigest(string digestAlgorithm, string data)
    {
        using var sha = SHA256.Create();
        byte[] hash = sha.ComputeHash(Encoding.UTF8.GetBytes(data));
        return $"{digestAlgorithm}=:{Convert.ToBase64String(hash)}:";
    }
}
import base64
import time
from email.utils import formatdate
from hashlib import sha256

from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding


class HttpSigner:
    SIGNATURE_ALGORITHM = "rsa-v1_5-sha256"
    DIGEST_ALGORITHM = "sha-256"

    def __init__(self, base64_private_key, client_id):
        self.client_id = client_id
        self.private_key = self._load_private_key(base64_private_key)

    @staticmethod
    def _load_private_key(raw_key):
        if "-----BEGIN PRIVATE KEY-----" not in raw_key:
            clean = "".join(raw_key.split())
            raw_key = (
                "-----BEGIN PRIVATE KEY-----\n"
                + "\n".join(clean[i:i + 64] for i in range(0, len(clean), 64))
                + "\n-----END PRIVATE KEY-----\n"
            )
        return serialization.load_pem_private_key(raw_key.encode("utf-8"), password=None)

    def build_headers(self, url, payload, method):
        headers = {}

        content_digest = None
        if payload:
            content_digest = self._create_digest(self.DIGEST_ALGORITHM, payload)
            headers["Content-Digest"] = content_digest

        now = int(time.time())
        date_header = formatdate(now, usegmt=True)
        headers["Date"] = date_header

        signature_parameters = self._create_signature_parameters(content_digest, now)
        headers["Signature-Input"] = "sig=" + signature_parameters

        parts = [
            f'"@method": {method.upper()}',
            f'"@target-uri": {url}',
        ]
        if content_digest is not None:
            parts.append(f'"content-digest": {content_digest}')
        parts.append(f'"date": {date_header}')
        parts.append(f'"@signature-params": {signature_parameters}')
        signature_base = "\n".join(parts)

        signature = self._sign(signature_base)
        headers["Signature"] = f"sig=:{signature}:"
        return headers

    def _create_signature_parameters(self, content_digest, created_timestamp):
        components = '("@method" "@target-uri"'
        if content_digest is not None:
            components += ' "content-digest"'
        components += ' "date"'
        return (
            f'{components});'
            f'created={created_timestamp};'
            f'keyid="{self.client_id}";'
            f'alg="{self.SIGNATURE_ALGORITHM}"'
        )

    def _sign(self, signature_base):
        signature = self.private_key.sign(
            signature_base.encode("utf-8"),
            padding.PKCS1v15(),
            hashes.SHA256(),
        )
        return base64.b64encode(signature).decode("ascii")

    @staticmethod
    def _create_digest(digest_algorithm, data):
        digest = base64.b64encode(sha256(data.encode("utf-8")).digest()).decode("ascii")
        return f"{digest_algorithm}=:{digest}:"
const crypto = require('crypto');

class HttpSigner {
  static SIGNATURE_ALGORITHM = 'rsa-v1_5-sha256';
  static DIGEST_ALGORITHM = 'sha-256';

  constructor(base64PrivateKey, clientId) {
    this.clientId = clientId;
    this.privateKey = HttpSigner.#loadPrivateKey(base64PrivateKey);
  }

  static #loadPrivateKey(rawKey) {
    if (!rawKey.includes('-----BEGIN PRIVATE KEY-----')) {
      const clean = rawKey.replace(/\s+/g, '');
      const chunks = clean.match(/.{1,64}/g).join('\n');
      rawKey = `-----BEGIN PRIVATE KEY-----\n${chunks}\n-----END PRIVATE KEY-----\n`;
    }
    return crypto.createPrivateKey({ key: rawKey, format: 'pem' });
  }

  buildHeaders(url, payload, method) {
    const headers = {};

    let contentDigest = null;
    if (payload) {
      contentDigest = this.#createDigest(HttpSigner.DIGEST_ALGORITHM, payload);
      headers['Content-Digest'] = contentDigest;
    }

    const now = Math.floor(Date.now() / 1000);
    const dateHeader = new Date(now * 1000).toUTCString();
    headers['Date'] = dateHeader;

    const signatureParameters = this.#createSignatureParameters(contentDigest, now);
    headers['Signature-Input'] = `sig=${signatureParameters}`;

    const parts = [
      `"@method": ${method.toUpperCase()}`,
      `"@target-uri": ${url}`,
    ];
    if (contentDigest !== null) parts.push(`"content-digest": ${contentDigest}`);
    parts.push(`"date": ${dateHeader}`);
    parts.push(`"@signature-params": ${signatureParameters}`);
    const signatureBase = parts.join('\n');

    const signature = this.#sign(signatureBase);
    headers['Signature'] = `sig=:${signature}:`;
    return headers;
  }

  #createSignatureParameters(contentDigest, createdTimestamp) {
    let components = '("@method" "@target-uri"';
    if (contentDigest !== null) components += ' "content-digest"';
    components += ' "date"';
    return `${components});created=${createdTimestamp};keyid="${this.clientId}";alg="${HttpSigner.SIGNATURE_ALGORITHM}"`;
  }

  #sign(signatureBase) {
    const signer = crypto.createSign('RSA-SHA256');
    signer.update(signatureBase);
    return signer.sign(this.privateKey, 'base64');
  }

  #createDigest(digestAlgorithm, data) {
    const hash = crypto.createHash('sha256').update(data).digest('base64');
    return `${digestAlgorithm}=:${hash}:`;
  }
}

module.exports = HttpSigner;
require 'base64'
require 'digest'
require 'openssl'

class HttpSigner
  SIGNATURE_ALGORITHM = 'rsa-v1_5-sha256'
  DIGEST_ALGORITHM = 'sha-256'

  def initialize(base64_private_key, client_id)
    @client_id = client_id
    @private_key = load_private_key(base64_private_key)
  end

  def build_headers(url, payload, method)
    headers = {}

    content_digest = nil
    if payload && !payload.empty?
      content_digest = create_digest(DIGEST_ALGORITHM, payload)
      headers['Content-Digest'] = content_digest
    end

    now = Time.now.to_i
    date_header = Time.at(now).utc.strftime('%a, %d %b %Y %H:%M:%S GMT')
    headers['Date'] = date_header

    signature_parameters = create_signature_parameters(content_digest, now)
    headers['Signature-Input'] = "sig=#{signature_parameters}"

    parts = [
      "\"@method\": #{method.upcase}",
      "\"@target-uri\": #{url}"
    ]
    parts << "\"content-digest\": #{content_digest}" if content_digest
    parts << "\"date\": #{date_header}"
    parts << "\"@signature-params\": #{signature_parameters}"
    signature_base = parts.join("\n")

    signature = sign(signature_base)
    headers['Signature'] = "sig=:#{signature}:"
    headers
  end

  private

  def load_private_key(raw_key)
    unless raw_key.include?('-----BEGIN PRIVATE KEY-----')
      clean = raw_key.gsub(/\s+/, '')
      raw_key = "-----BEGIN PRIVATE KEY-----\n" +
                clean.scan(/.{1,64}/).join("\n") +
                "\n-----END PRIVATE KEY-----\n"
    end
    OpenSSL::PKey::RSA.new(raw_key)
  end

  def create_signature_parameters(content_digest, created_timestamp)
    components = '("@method" "@target-uri"'
    components += ' "content-digest"' if content_digest
    components += ' "date"'
    "#{components});created=#{created_timestamp};keyid=\"#{@client_id}\";alg=\"#{SIGNATURE_ALGORITHM}\""
  end

  def sign(signature_base)
    signature = @private_key.sign(OpenSSL::Digest::SHA256.new, signature_base)
    Base64.strict_encode64(signature)
  end

  def create_digest(digest_algorithm, data)
    hash = Base64.strict_encode64(Digest::SHA256.digest(data))
    "#{digest_algorithm}=:#{hash}:"
  end
end

Runtime requirements

LanguageRequirement
JavaJDK 8 or later. No external dependencies — uses javax.crypto from the standard library.
PHPPHP 7.4 or later. Requires the openssl extension (enabled by default in most distributions).
C#.NET 5 or later (for RSA.ImportPkcs8PrivateKey). For .NET Framework, use BouncyCastle to import the key.
PythonPython 3.7+ and the cryptography package: pip install cryptography.
Node.js / JavaScriptNode.js 16+. Uses the built-in crypto module — no npm install needed.
RubyRuby 2.5+. Uses standard library openssl, base64, digest.

Using the signer

Once you have an HttpSigner instance, every API call is the same shape: build the headers, attach them to your HTTP client, send the request. The example below makes a POST /api/transaction call to BEEM Production.

import java.net.URI;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.file.Files;
import java.nio.file.Paths;
import java.util.Map;

public class SignedRequestExample {
    public static void main(String[] args) throws Exception {
        String privateKey = new String(Files.readAllBytes(Paths.get("/path/to/api-client-key.pem")));
        String clientId   = "0193abc8-74c5-785f-ae34-11ab890b2681";

        HttpSigner signer = new HttpSigner(privateKey, clientId);

        String method  = "POST";
        String url     = "https://api.core.paybeem.com/api/transaction";
        String payload = "{\"amount\":100,\"currency\":\"USDC\"}";

        Map<String, String> headers = signer.buildHeaders(url, payload, method);

        HttpRequest.Builder requestBuilder = HttpRequest.newBuilder(URI.create(url))
            .POST(HttpRequest.BodyPublishers.ofString(payload))
            .header("Content-Type", "application/json");
        headers.forEach(requestBuilder::header);

        HttpResponse<String> response = HttpClient.newHttpClient()
            .send(requestBuilder.build(), HttpResponse.BodyHandlers.ofString());

        System.out.println(response.statusCode());
        System.out.println(response.body());
    }
}
<?php

$privateKey = file_get_contents('/path/to/api-client-key.pem');
$clientId   = '0193abc8-74c5-785f-ae34-11ab890b2681';

$signer = new HttpSigner($privateKey, $clientId);

$method  = 'POST';
$url     = 'https://api.core.paybeem.com/api/transaction';
$payload = json_encode(['amount' => 100, 'currency' => 'USDC']);

$headers = $signer->buildHeaders($url, $payload, $method);

$curlHeaders = ['Content-Type: application/json'];
foreach ($headers as $name => $value) {
    $curlHeaders[] = "$name: $value";
}

$ch = curl_init($url);
curl_setopt_array($ch, [
    CURLOPT_CUSTOMREQUEST  => $method,
    CURLOPT_POSTFIELDS     => $payload,
    CURLOPT_HTTPHEADER     => $curlHeaders,
    CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
$status   = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);

echo $status . "\n";
echo $response;
using System;
using System.IO;
using System.Net.Http;
using System.Text;
using System.Threading.Tasks;

class Program
{
    static async Task Main()
    {
        string privateKey = File.ReadAllText("/path/to/api-client-key.pem");
        string clientId   = "0193abc8-74c5-785f-ae34-11ab890b2681";

        var signer = new HttpSigner(privateKey, clientId);

        string method  = "POST";
        string url     = "https://api.core.paybeem.com/api/transaction";
        string payload = "{\"amount\":100,\"currency\":\"USDC\"}";

        var headers = signer.BuildHeaders(url, payload, method);

        using var http = new HttpClient();
        var request = new HttpRequestMessage(new HttpMethod(method), url)
        {
            Content = new StringContent(payload, Encoding.UTF8, "application/json")
        };
        foreach (var header in headers)
        {
            request.Headers.TryAddWithoutValidation(header.Key, header.Value);
        }

        var response = await http.SendAsync(request);
        Console.WriteLine((int)response.StatusCode);
        Console.WriteLine(await response.Content.ReadAsStringAsync());
    }
}
import json
import requests

with open("/path/to/api-client-key.pem") as f:
    private_key = f.read()
client_id = "0193abc8-74c5-785f-ae34-11ab890b2681"

signer = HttpSigner(private_key, client_id)

method  = "POST"
url     = "https://api.core.paybeem.com/api/transaction"
payload = json.dumps({"amount": 100, "currency": "USDC"})

headers = signer.build_headers(url, payload, method)
headers["Content-Type"] = "application/json"

response = requests.request(method, url, data=payload, headers=headers)
print(response.status_code)
print(response.text)
const fs = require('fs');
const HttpSigner = require('./HttpSigner');

const privateKey = fs.readFileSync('/path/to/api-client-key.pem', 'utf8');
const clientId   = '0193abc8-74c5-785f-ae34-11ab890b2681';

const signer = new HttpSigner(privateKey, clientId);

const method  = 'POST';
const url     = 'https://api.core.paybeem.com/api/transaction';
const payload = JSON.stringify({ amount: 100, currency: 'USDC' });

const headers = signer.buildHeaders(url, payload, method);
headers['Content-Type'] = 'application/json';

(async () => {
  const response = await fetch(url, { method, headers, body: payload });
  console.log(response.status);
  console.log(await response.text());
})();
require 'json'
require 'net/http'
require 'uri'

private_key = File.read('/path/to/api-client-key.pem')
client_id   = '0193abc8-74c5-785f-ae34-11ab890b2681'

signer = HttpSigner.new(private_key, client_id)

method  = 'POST'
url     = 'https://api.core.paybeem.com/api/transaction'
payload = { amount: 100, currency: 'USDC' }.to_json

headers = signer.build_headers(url, payload, method)
headers['Content-Type'] = 'application/json'

uri = URI(url)
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = true
request = Net::HTTP::Post.new(uri.request_uri, headers)
request.body = payload

response = http.request(request)
puts response.code
puts response.body

Arguments

ArgumentDescription
privateKeyYour RSA private key, loaded from disk or a secrets manager. The signer accepts a full PEM block (including the BEGIN/END lines) or just the Base64 body.
clientIdThe Client Id shown in the Portal under Settings → Clients.
urlThe full request URL, including scheme, host and path. Must exactly match the URL the request is sent to — otherwise the signature will not verify.
payloadThe raw request body as a string. Pass null (or the language's equivalent: None, nil, omit the argument) for GET requests or any request with no body.
methodThe HTTP method (GET, POST, PUT, DELETE, etc.). Case is normalised inside the signer.
🚧

The URL must match exactly

The signature covers the full URL including query string. If your HTTP client appends or re-orders query parameters (some clients do), the server will reject the request with a signature mismatch. Build the final URL once, pass it to buildHeaders, and pass the same URL to your HTTP client without further modification.


Troubleshooting

If BEEM rejects a signed request, check the following in order:

  1. clientId matches the one shown in the Portal (no leading or trailing whitespace).
  2. The public key registered in the Portal corresponds to the private key in the signer — not the other way around.
  3. The IP the request originates from is in the client's network whitelist.
  4. The Date header is within a few minutes of the current UTC time. Skewed clocks cause replay-protection failures.
  5. The Content-Digest is computed over the exact bytes sent in the body — do not pretty-print or re-serialise the JSON between digest and send.
  6. The signature base matches the format BEEM expects (see the buildHeaders method above).
  7. The role assigned to the client allows the operation being attempted.

What's next