HTTP Signature
Description
Some APIs require an HTTP signature for reinforced security. Your app_private_key, as defined in the console, will be required for this. The HTTP header follows the signing HTTP Messages IETF standard, with the following particularities:
- the keyId value is your
app_id - the only algorithm currently supported is
rsa-sha256
All endpoints requesting HTTP signature will require following additional header parameters:
| Name | Required | Description | Example |
|---|---|---|---|
| date | Always | RFC 2822 formatted date | Wed, 26 Feb 2020 17:29:51 GMT |
| digest | POST PUT PATCH | Hashed payload | SHA-256=cjuagrzhZ8joOWLlQCCe5co30bRISL1VIWNq99da+hM= |
| x-request-id | Always | UUID v4 identifier | 123e4567-e89b-42d3-a456-426614174000 |
| Signature | Always | HTTP calculated signature | keyId="0354d723-d8d3-469a-8926-4f3f18b2c416", algorithm="rsa-sha256", headers="(request-target) date x-request-id", signature="eyvAyh5kuqifP8vkUy5KBWPgtQAurB7xMeC6T/KGJQm2JA==" |
ImportantSignature, digest (if a POST, PUT or PATCH), date and x-request-id headers are optional in the SANDBOX environment but mandatory when calling the PRODUCTION one.
NoteAn interactive HTTP Signature guide has been created to help you through the process of creating the digest and Signature headers. You can also check out request signature examples in different programming languages (JS, Java, Python...) on our GitHub project.
Process
1. Build the message digest
The digest is a SHA-256 hash of the payload encoded into base64, and concatenated with a "SHA-256=" prefix:
digest = "SHA-256=" + base64( SHA256( body ) )
CriticalThe body must be the exact raw JSON string sent in the request — minified, with no extra whitespace. Using pretty-printed JSON (e.g.
JSON.stringify(body, null, 2)) will produce a different digest. UseJSON.stringify(body)instead.
NoteMake sure your body is encoded into UTF-8 with unescaped unicode to avoid bad surprises in case accents or other special characters are included in the body.
2. Create the signing parameters
| Name | Description | Example |
|---|---|---|
| (request-target) | Method and pathname of an URL | get /ais/v1/customer/123/accounts |
| date | An RFC 2822 formatted date | Wed, 26 Feb 2020 17:29:51 GMT |
| digest | The SHA-256 digest of the body as described in point 1 | SHA-256=cjuagrzhZ8joOWLlQCCe5co30bRISL1VIWNq99da+hM= |
| x-request-id | An UUID v4 formatted unique value | 123e4567-e89b-42d3-a456-426614174000 |
3. Build the signing string
For GET & DELETE requests, use:
(request-target)datex-request-id
For POST, PUT & PATCH requests, use:
(request-target)datedigestx-request-id
(request-target): get /ais/v1/customer/123/accounts?querystring=true\n
date: Wed, 26 Feb 2020 17:29:51 GMT\n
x-request-id: 123e4567-e89b-42d3-a456-426614174000
NoteMake sure the name of each parameter is lower-cased (not the value), there is a ": " between the name and the value, and a return character "\n" at the end of each line except the last one. For the (request-target), include query params in the pathname.
4. Encrypt the signing string
Sign the string with your private key and encode the result into base64:
signature = base64( RSA-SHA256( signing string ) )5. Create the signature string
Concatenate all fields separating them by a comma (",").
keyId=`app_id`,
algorithm=rsa-sha256,
headers=(request-target) date x-request-id,
signature=`signature`For POST, PUT & PATCH requests:
keyId=`app_id`,
algorithm=rsa-sha256,
headers=(request-target) date digest x-request-id,
signature=`signature`This results in an HTTP signature with the following structure:
keyId="0354d723-d8d3-469a-8926-4f3f18b2c416",algorithm="rsa-sha256",headers="(request-target) date digest x-request-id",signature="eyvAyh5kuqifP8vkUy5KBWPgtQAurB7xMeC6T/KGJQm2JA=="Complete example
The helper below implements the whole process: it computes the digest (for requests with a body), builds the signing string in the right order, signs it with your private key, and returns all four headers ready to merge into your request.
const crypto = require('crypto');
function signedHeaders({ appId, privateKeyPem, method, path, body }) {
const date = new Date().toUTCString();
const xRequestId = crypto.randomUUID();
const headers = { date, 'x-request-id': xRequestId };
const lines = [`(request-target): ${method.toLowerCase()} ${path}`, `date: ${date}`];
let signedList = '(request-target) date x-request-id';
if (body !== undefined) {
// Hash the exact minified string you will send as the request body
const raw = JSON.stringify(body);
const digest = 'SHA-256=' + crypto.createHash('sha256').update(raw, 'utf8').digest('base64');
headers.digest = digest;
lines.splice(2, 0, `digest: ${digest}`);
signedList = '(request-target) date digest x-request-id';
}
lines.push(`x-request-id: ${xRequestId}`);
const signingString = lines.join('\n');
const signature = crypto
.sign('sha256', Buffer.from(signingString, 'utf8'), privateKeyPem)
.toString('base64');
headers.signature = `keyId="${appId}",algorithm="rsa-sha256",headers="${signedList}",signature="${signature}"`;
return headers;
}
// Usage — merge with your other headers (Authorization, Content-Type...)
const headers = signedHeaders({
appId: process.env.FINTECTURE_APP_ID,
privateKeyPem: process.env.FINTECTURE_PRIVATE_KEY,
method: 'GET',
path: '/ais/v1/customer/123/accounts?querystring=true',
});import base64
import hashlib
import json
import os
import uuid
from email.utils import formatdate
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
def signed_headers(app_id, private_key_pem, method, path, body=None):
date = formatdate(usegmt=True) # RFC 2822, e.g. Wed, 26 Feb 2020 17:29:51 GMT
x_request_id = str(uuid.uuid4())
headers = {"date": date, "x-request-id": x_request_id}
lines = [f"(request-target): {method.lower()} {path}", f"date: {date}"]
signed_list = "(request-target) date x-request-id"
if body is not None:
# Hash the exact minified string you will send as the request body
raw = json.dumps(body, separators=(",", ":"), ensure_ascii=False)
digest = "SHA-256=" + base64.b64encode(hashlib.sha256(raw.encode()).digest()).decode()
headers["digest"] = digest
lines.insert(2, f"digest: {digest}")
signed_list = "(request-target) date digest x-request-id"
lines.append(f"x-request-id: {x_request_id}")
signing_string = "\n".join(lines)
key = serialization.load_pem_private_key(private_key_pem.encode(), password=None)
signature = base64.b64encode(
key.sign(signing_string.encode(), padding.PKCS1v15(), hashes.SHA256())
).decode()
headers["signature"] = (
f'keyId="{app_id}",algorithm="rsa-sha256",'
f'headers="{signed_list}",signature="{signature}"'
)
return headers
# Usage — merge with your other headers (Authorization, Content-Type...)
headers = signed_headers(
app_id=os.environ["FINTECTURE_APP_ID"],
private_key_pem=os.environ["FINTECTURE_PRIVATE_KEY"],
method="GET",
path="/ais/v1/customer/123/accounts?querystring=true",
)
Send exactly what you signedIf you compute a digest from a body, the bytes you send on the wire must be that exact string. Serialize once, sign it, and send the same variable — don't let your HTTP library re-serialize the object.
Troubleshooting
| Error | Cause | Fix |
|---|---|---|
invalid_digest | Body differs from what was hashed (e.g. pretty-printed vs minified JSON, or body modified after digest computation) | Hash the exact raw body string as sent. Use JSON.stringify(body), not JSON.stringify(body, null, 2). Recompute both digest and signature after any body change. |
missing_header_digest | Digest header absent on POST/PUT/PATCH | Add the digest header for requests with a body. |
missing_header_date | Neither date nor x-date provided | Add date (or x-date in browsers) with RFC 2822 format. |
missing_header_xrequestid | x-request-id header missing | Add x-request-id with a UUID v4 value. |
verification_failed | Signing string mismatch or wrong private key | Check header order matches the headers parameter, and verify the correct private key is used |
Updated 13 days ago