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:

NameRequiredDescriptionExample
dateAlwaysRFC 2822 formatted dateWed, 26 Feb 2020 17:29:51 GMT
digestPOST
PUT
PATCH
Hashed payloadSHA-256=cjuagrzhZ8joOWLlQCCe5co30bRISL1VIWNq99da+hM=
x-request-idAlwaysUUID v4 identifier123e4567-e89b-42d3-a456-426614174000
SignatureAlwaysHTTP calculated signaturekeyId="0354d723-d8d3-469a-8926-4f3f18b2c416",
algorithm="rsa-sha256",
headers="(request-target) date x-request-id",
signature="eyvAyh5kuqifP8vkUy5KBWPgtQAurB7xMeC6T/KGJQm2JA=="
🚧

Important

Signature, 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.

💡

Note

An 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 ) )
❗️

Critical

The 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. Use JSON.stringify(body) instead.

💡

Note

Make 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

NameDescriptionExample
(request-target)Method and pathname of an URLget /ais/v1/customer/123/accounts
dateAn RFC 2822 formatted dateWed, 26 Feb 2020 17:29:51 GMT
digestThe SHA-256 digest of the body as described in point 1SHA-256=cjuagrzhZ8joOWLlQCCe5co30bRISL1VIWNq99da+hM=
x-request-idAn UUID v4 formatted unique value123e4567-e89b-42d3-a456-426614174000

3. Build the signing string

For GET & DELETE requests, use:

  • (request-target)
  • date
  • x-request-id

For POST, PUT & PATCH requests, use:

  • (request-target)
  • date
  • digest
  • x-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
💡

Note

Make 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 (",").

For GET & DELETE requests:

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 signed

If 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

ErrorCauseFix
invalid_digestBody 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_digestDigest header absent on POST/PUT/PATCHAdd the digest header for requests with a body.
missing_header_dateNeither date nor x-date providedAdd date (or x-date in browsers) with RFC 2822 format.
missing_header_xrequestidx-request-id header missingAdd x-request-id with a UUID v4 value.
verification_failedSigning string mismatch or wrong private keyCheck header order matches the headers parameter, and verify the correct private key is used

Did this page help you?