JWS Integration Guide
This guide walks you through setting up JWS authentication for EBANX API requests. JWS authentication applies to all EBANX API products, including Payments, Payouts, and Transfers. By the end, you will have generated a key pair, registered your public key with EBANX, and be ready to sign your API requests.
Prerequisites
- An active EBANX merchant account
- OpenSSL or equivalent cryptographic tool installed on your system
- Ability to make HTTP requests with custom headers
Setup
- Generate an RSA key pair
Generate a 4096-bit RSA key pair using OpenSSL:
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:4096 -out private_key.pem
openssl rsa -pubout -in private_key.pem -out public_key.pemThis produces two files:
private_key.pem— Your private key. Keep this secure and never share it.public_key.pem— Your public key. You will send this to EBANX.SecurityStore your private key in a secure location (e.g., a secrets manager or HSM). Never commit it to version control, log it, or expose it in client-side code.
- Register your public key with EBANX
Send your
public_key.pemfile to your EBANX Integration Specialist at integration@ebanx.com.After EBANX registers your key, you will receive a unique Key ID (
kid). This identifier is required in the JWS header of every API request. - Prepare the JWS header
Create a JWS header with the following structure:
{
"alg": "RS256",
"kid": "<YOUR_KEY_ID>",
"b64": false,
"crit": ["b64"]
}The
algfield must beRS256. Thekidfield must contain the unique identifier provided by EBANX after registering your public key. Theb64field set tofalseindicates the payload is not base64url-encoded in the token (RFC 7797). Whenb64is present, thecritheader must include"b64". - Sign your request
For every API request, you need to:
- Build the JSON request body.
- Base64URL-encode the JWS header.
- Construct the signing input:
BASE64URL(header).<payload>(raw payload, not base64url-encoded, perb64: false). - Sign the input with your private key using RS256.
- Assemble the detached JWS token:
BASE64URL(header)..BASE64URL(signature). - Send the token in the
X-JWS-SignatureHTTP header.
See the step-by-step shell walkthrough and code examples below.
- Send your API request
Add the JWS token to the
X-JWS-SignatureHTTP header. The same signing process applies to all EBANX API endpoints.Payment example (
/ws/direct):curl -X POST \
--location 'https://sandbox.ebanx.com/ws/direct' \
--header 'Content-Type: application/json' \
--header 'X-JWS-Signature: <YOUR_JWS_SIGNATURE>' \
--data '{
"payment": {
"name": "Jose Silva",
"email": "jose@example.com",
"country": "br",
"payment_type_code": "pix",
"merchant_payment_code": "order-12345",
"currency_code": "BRL",
"amount_total": 100.00
}
}'Payout example (
/ws/payout/create):curl -X POST \
--location 'https://sandbox.ebanx.com/ws/payout/create' \
--header 'Content-Type: application/json' \
--header 'X-JWS-Signature: <YOUR_JWS_SIGNATURE>' \
--data '{
"external_reference": "PAYOUT_001",
"country": "br",
"amount": 100,
"currency_code": "BRL",
"target": "pix_key",
"target_account": "payee@example.com",
"payee": {
"name": "Carlos Machado",
"email": "payee@example.com",
"document": "85351346893",
"document_type": "CPF"
}
}'The request body remains unchanged and is sent as normal JSON. EBANX uses the body as the detached payload when verifying the signature.
Signing step by step (shell)
This section breaks down the JWS signing process using raw shell commands so you can understand each step before using a library.
Step 1: Base64URL-encode the JWS header
HEADER='{"alg":"RS256","kid":"<YOUR_KEY_ID>","b64":false,"crit":["b64"]}'
HEADER_B64=$(echo -n "$HEADER" | openssl base64 -e | tr -d '=' | tr '/+' '_-' | tr -d '\n')
echo "Encoded header: $HEADER_B64"
Step 2: Construct the signing input
Since b64 is false in the JWS header, the signing input uses the raw payload (not base64url-encoded) per RFC 7797:
PAYLOAD='{"payment":{"name":"Jose Silva","email":"jose@example.com","country":"br","payment_type_code":"pix","merchant_payment_code":"order-12345","currency_code":"BRL","amount_total":100}}'
SIGNING_INPUT="${HEADER_B64}.${PAYLOAD}"
Step 3: Compute the RS256 signature
SIGNATURE=$(echo -n "$SIGNING_INPUT" | openssl dgst -sha256 -sign private_key.pem | openssl base64 -e | tr -d '=' | tr '/+' '_-' | tr -d '\n')
Step 4: Assemble the detached JWS token
The final token uses compact serialization with an empty payload section (two consecutive periods):
JWS_TOKEN="${HEADER_B64}..${SIGNATURE}"
echo "JWS Token: $JWS_TOKEN"
Step 5: Send the request
curl -X POST \
--location 'https://sandbox.ebanx.com/ws/direct' \
--header 'Content-Type: application/json' \
--header "X-JWS-Signature: ${JWS_TOKEN}" \
--data "$PAYLOAD"
Integration examples
The following examples demonstrate how to sign a request to the EBANX API using JWS in different programming languages.
Python
Requires the cryptography and requests packages:
pip install cryptography requests
import json
import requests
from base64 import urlsafe_b64encode
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
# Load your private key
with open("private_key.pem", "r") as f:
private_key = f.read()
# Your Key ID provided by EBANX
kid = "<YOUR_KEY_ID>"
# Build the request payload
payload = {
"payment": {
"name": "Jose Silva",
"email": "jose@example.com",
"country": "br",
"payment_type_code": "pix",
"merchant_payment_code": "order-12345",
"currency_code": "BRL",
"amount_total": 100.00
}
}
payload_json = json.dumps(payload, separators=(",", ":"))
# Create the JWS header
jws_header = {"alg": "RS256", "kid": kid, "b64": False, "crit": ["b64"]}
# Encode header as Base64URL
header_b64 = urlsafe_b64encode(
json.dumps(jws_header, separators=(",", ":")).encode()
).rstrip(b"=").decode()
# Sign: header.raw_payload (b64=false means payload is NOT base64url-encoded)
signing_input = f"{header_b64}.{payload_json}"
key = serialization.load_pem_private_key(private_key.encode(), password=None)
signature = key.sign(
signing_input.encode(),
padding.PKCS1v15(),
hashes.SHA256()
)
signature_b64 = urlsafe_b64encode(signature).rstrip(b"=").decode()
# Detached compact serialization: header..signature (payload section is empty)
jws_token = f"{header_b64}..{signature_b64}"
# Send the request
response = requests.post(
"https://sandbox.ebanx.com/ws/direct",
headers={
"Content-Type": "application/json",
"X-JWS-Signature": jws_token,
},
data=payload_json,
)
print(response.json())
Node.js
Uses the built-in crypto module (no external dependencies):
import crypto from "crypto";
import fs from "fs";
const privateKeyPem = fs.readFileSync("private_key.pem", "utf8");
const kid = "<YOUR_KEY_ID>";
const payload = {
payment: {
name: "Jose Silva",
email: "jose@example.com",
country: "br",
payment_type_code: "pix",
merchant_payment_code: "order-12345",
currency_code: "BRL",
amount_total: 100.0,
},
};
const payloadJson = JSON.stringify(payload);
function base64url(buffer) {
return Buffer.from(buffer)
.toString("base64")
.replace(/=/g, "")
.replace(/\+/g, "-")
.replace(/\//g, "_");
}
// Build JWS header
const header = { alg: "RS256", kid: kid, b64: false, crit: ["b64"] };
const headerB64 = base64url(JSON.stringify(header));
// Construct signing input: header.raw_payload (b64=false)
const signingInput = `${headerB64}.${payloadJson}`;
// Sign with RS256
const sign = crypto.createSign("SHA256");
sign.update(signingInput);
const signature = sign.sign(privateKeyPem);
const signatureB64 = base64url(signature);
// Detached compact serialization: header..signature
const jwsToken = `${headerB64}..${signatureB64}`;
// Send the request
const response = await fetch("https://sandbox.ebanx.com/ws/direct", {
method: "POST",
headers: {
"Content-Type": "application/json",
"X-JWS-Signature": jwsToken,
},
body: payloadJson,
});
console.log(await response.json());
PHP
Requires the web-token/jwt-library package:
composer require web-token/jwt-library
<?php
use Jose\Component\Core\AlgorithmManager;
use Jose\Component\Core\JWK;
use Jose\Component\Signature\Algorithm\RS256;
use Jose\Component\Signature\JWSBuilder;
use Jose\Component\Signature\Serializer\CompactSerializer;
use Jose\Component\KeyManagement\JWKFactory;
$privateKeyPem = file_get_contents('private_key.pem');
$kid = '<YOUR_KEY_ID>';
$payload = json_encode([
'payment' => [
'name' => 'Jose Silva',
'email' => 'jose@example.com',
'country' => 'br',
'payment_type_code' => 'pix',
'merchant_payment_code' => 'order-12345',
'currency_code' => 'BRL',
'amount_total' => 100.00,
],
]);
// Create the signing key
$jwk = JWKFactory::createFromKey($privateKeyPem, null, ['kid' => $kid]);
// Build the JWS
$algorithmManager = new AlgorithmManager([new RS256()]);
$jwsBuilder = new JWSBuilder($algorithmManager);
$jws = $jwsBuilder
->create()
->withPayload($payload, true) // true = detached payload
->addSignature($jwk, ['alg' => 'RS256', 'kid' => $kid, 'b64' => false, 'crit' => ['b64']])
->build();
$serializer = new CompactSerializer();
$token = $serializer->serialize($jws, 0);
// Send the request
$ch = curl_init('https://sandbox.ebanx.com/ws/direct');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-JWS-Signature: ' . $token,
],
CURLOPT_POSTFIELDS => $payload,
CURLOPT_RETURNTRANSFER => true,
]);
$response = curl_exec($ch);
curl_close($ch);
echo $response;
Error handling
If JWS authentication fails, the API returns HTTP 401 (Unauthorized). The response body does not contain detailed error codes; the specific failure reason is recorded in EBANX internal logs for debugging purposes.
Common issues and solutions:
| Issue | Possible cause | Solution |
|---|---|---|
| Missing signature | The X-JWS-Signature header is not present in the request | Add the X-JWS-Signature header with your signed JWS token |
| Invalid token format | The JWS token cannot be parsed | Verify the token follows BASE64URL(header)..BASE64URL(signature) format |
| Payload not detached | The JWS token contains an embedded payload | Ensure the payload section in the token is empty (two consecutive periods) |
| Invalid header | Missing or incorrect JWS header parameters | Set alg to RS256 and include your kid in the JWS header |
| Signature mismatch | The signature verification failed | Ensure the exact request body bytes used for signing match what is sent |
If you receive a 401 response and cannot identify the issue, contact your EBANX Integration Specialist. Detailed failure reasons are available in EBANX internal logs and can help diagnose the problem.
Still need help?
We hope this article was helpful. If you still have questions, you can explore the following options:
- Merchant support: Contact our support team at sales.engineering@ebanx.com for assistance.
- Not a partner yet? Please complete the Merchant Signup Form, and our commercial team will reach out to you.