GoCardlessDeveloper Docs
Create a sandbox account

API request signing#

View as Markdown

We require API Request Signing to secure your communication to the GoCardless API and to comply with the Strong Customer Authentication requirement. This section will walk you through how to:

  • Generate public and private keys and upload the public key to GoCardless
  • Build the signature base that will be used to generate the Gc-Signature header
  • Build and attach the Gc-Signature and Gc-Signature-Input headers and send them with your API request

Sandbox developer bypass#

For ease of development (e.g. testing our endpoints with Postman or curl), it is possible to bypass request signing in the sandbox environment using the following header:

Gc-Signature: sig-1=:DEVELOPER:

Generate your key pair#

  1. We use ECDSA keys for API request signing. You must generate an ES512 key pair using the P-521 elliptic curve without a passphrase. Use the following commands in a terminal to generate your key pair:
openssl ecparam -name secp521r1 -genkey -noout -out ec-secp521r1-priv-key.pem
openssl ec -in ec-secp521r1-priv-key.pem -pubout > ec-secp521r1-pub-key.pem
  1. Securely save your private key.
  2. Upload your public key to GoCardless via the dashboard.
cat ec-secp521r1-pub-key.pem
# Copy this output and paste into the form. It should look something like:
# -----BEGIN PUBLIC KEY-----
# MIGbMBAGByqG.....................
# .................................
# ................
# -----END PUBLIC KEY-----

When you've added a new public key, a corresponding request_signing_key ID (e.g. RSK00123456789300123456789300) is generated. You will need this for building the request signature.

Signing the API request#

We follow the RFC-9421 specification for HTTP Message Signatures.

  1. Create the signature base. This is a raw string containing all the components covered by the signature. It must be in the format shown in the example below.
"@method": POST
"@authority": api.gocardless.com
"@request-target": /path?param=value
"content-digest": sha256=:9AmU2hRsZnqEo2HiNTLacLN0fOs8YmDiuX4WYeYWvh0=:
"content-type": application/json
"content-length": 18
"@signature-params": ("@method" "@authority" "@request-target" "content-digest" "content-type" "content-length");keyid="your-public-key-identifier";created=1675688690;nonce="8IBTHwOdqNKAWeKl7plt8g=="
signature_base =
"\"@method\": POST\n" \
"\"@authority\": api.gocardless.com\n" \
"\"@request-target\": /path?param=value\n" \
"\"content-digest\": sha256=:9AmU2hRsZnqEo2HiNTLacLN0fOs8YmDiuX4WYeYWvh0=:\n" \
"\"content-type\": application/json\n" \
"\"content-length\": 18\n" \
"\"@signature-params\": (\"@method\" \"@authority\" \"@request-target\" \"content-digest\" \"content-type\" \"content-length\");keyid=\"your-public-key-identifier\";created=1675688690;nonce=\"8IBTHwOdqNKAWeKl7plt8g==\""
const signatureBase = SignatureBase(
  httpMethod: "POST",
  host: "https://api.gocardless.com",
  requestPath: "/outbound_payments",
  contentDigest: "2c26b46b68ffc68ff99b453c1d30413413422d706483bfa0f98a5e886266e7ae",
  contentType: "application/json",
  contentLength: 123,
  keyId: "RSK00123456789300123456789300",
  created: DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString(),
  nonce: Guid.NewGuid().ToString());

string SignatureParams(string keyId, string created, string nonce, bool includeContentParams)
=> @$"(""@method"" ""@authority"" ""@request-target""" +
(includeContentParams ? @" ""content-digest"" ""content-type"" ""content-length""" : "") +
@$");keyid=""{keyId}"";created={created};nonce=""{nonce}""";

string SignatureBase(
string httpMethod, string host, string requestPath,
string? contentDigest, string? contentType, int? contentLength,
string keyId, string created, string nonce
)
=> ($"\"@method\": {httpMethod}\n" +
$"\"@authority\": {host}\n" +
$"\"@request-target\": {requestPath}\n" +
(string.IsNullOrWhiteSpace(contentDigest) ? "" :
$"\"content-digest\": sha256=:#{contentDigest}:\n" +
$"\"content-type\": {contentType}\n" +
$"\"content-length\": {contentLength}\n") +
$"\"@signature-params\": {SignatureParams(keyId, created, nonce, includeContentParams: contentDigest != null)}");
// Example for POST request
const signatureBase = `"@method": POST\n` +
`"@authority": api.gocardless.com\n` +
`"@request-target": /path?param=value\n` +
`"content-digest": sha256=:9AmU2hRsZnqEo2HiNTLacLN0fOs8YmDiuX4WYeYWvh0=:\n` +
`"content-type": application/json\n` +
`"content-length": 18\n` +
`"@signature-params": ` +
`("@method" "@authority" "@request-target"` +
` "content-digest" "content-type" "content-length"` +
`);keyid="RSK00123456789300123456789300";created=1675688690;nonce="8IBTHwOdqNKAWeKl7plt8g=="`;

// Example for GET request
const signatureBaseGet = `"@method": GET\n` +
`"@authority": api.gocardless.com\n` +
`"@request-target": /path?param=value\n` +
`"@signature-params": ` +
`("@method" "@authority" "@request-target"` +
`);keyid="RSK00123456789300123456789300";created=1675688690;nonce="8IBTHwOdqNKAWeKl7plt8g=="`;

Requirements:

  1. Each line is concatenated with a newline character (\n).
  2. You must respect the format (e.g., respect every character like ", @, or spaces).
  3. @method, @authority, and @request-target are required components in building the signature base.

You can add any other signed headers you want to include into the signature base. They must appear in the same order as listed in @signature-params. For any additional signed headers, you must pass them as HTTP headers in the request itself.

The following table describes how the signature base is built:

KeyValue
"@method"The HTTP method in uppercase, e.g. GET or POST.
"@authority"The target URI host, usually conveyed via the Host header (e.g. api.gocardless.com).
"@request-target"The full path including any query parameters. Query parameters must be ordered deterministically by sorting keys.
"content-digest"Only needed if the request has a body (e.g. POST/PUT). SHA-256 base64-encoded hash of the body, wrapped as sha256=:CONTENT_DIGEST:. The body must be serialised deterministically (sorted keys) to produce a consistent digest.
"content-type"Only needed if the request has a body. The content type, e.g. application/json.
"content-length"Only needed if the request has a body. Size of the message body in bytes.
"@signature-params"The following signature parameters should be passed as follows and separated by semicolon: (<signature components>);<signature param1>;<signature param2>

Signature Components
In brackets — the components of the request that are included in the signature process. This would be "@method" "@authority" "@request-target", followed by any additional request headers, each separated by a space. They should appear in the order as you used to build the rest of the signature base.

Signature Parameters
keyid — the identifier of your public request signing key.
created — a Unix timestamp (e.g. 1675688690) in UTC.
nonce — a random unique value generated for this signature as a String value. This must be a random base64-encoded string of at least 128 bits of data from a cryptographically secure random number generator.

More details on building the signature base can be found in section 2.5 of RFC-9421.

  1. Sign the signature base with your private key.
private_key = OpenSSL::PKey::EC.new(your_private_key)
signature = private_key.sign(OpenSSL::Digest.new("SHA512"), signature_base)
using var privateKeyPem = File.OpenText("ec-secp521r1-priv-key.pem");
var signature = GetSignature(privateKeyPem, signatureBase);

string GetSignature(TextReader privateKeyPem, string message)
{
var keys = (AsymmetricCipherKeyPair)new PemReader(privateKeyPem).ReadObject();
var signer = SignerUtilities.GetSigner("SHA-512withECDSA");
var privateKey = keys.Private;
signer.Init(true, privateKey);

  var messageBytes = GetMessageBytes(message);
  signer.BlockUpdate(messageBytes, 0, messageBytes.Length);

  return signer.GenerateSignature();

}
import * as crypto from "node:crypto";
import { Buffer } from "node:buffer";
import * as fs from "node:fs";

// Read in signature file
const privateKeyPem = fs.readFileSync("ec-secp521r1-priv-key.pem", "utf-8");

// Create a private key object
const privateKey = crypto.createPrivateKey({
key: privateKeyPem,
format: "pem",
});

// Prepare the data for signing
const dataBuffer = Buffer.from(signatureBase, "utf8");

// Generate the signature
const sign = crypto.createSign("SHA512");
sign.update(dataBuffer);
sign.end();
const signature = sign.sign(privateKey, "base64");

console.log("Signature:", signature);
  1. Encode the signature in base64.
encoded_signature = Base64.strict_encode64(signature)
const signatureStr = Convert.ToBase64String(signature);
  1. Set the Gc-Signature and Gc-Signature-Input HTTP headers.

Requirements:

  • Gc-Signature is the base64 encoded signature. It must be in the format Gc-Signature: sig-1=:<SIGNATURE>: where sig-1 identifies the signature and the value is wrapped in colons.
  • Gc-Signature-Input is the same string as @signature-params used in building the signature base. It must be in the format sig-1=<SIGNATURE_INPUT>.
Gc-Signature-Input: sig-1=("@method" "@authority" "@request-target" "content-digest" "content-type" "content-length");keyid="your-public-key-identifier";created=1675688690;nonce="8IBTHwOdqNKAWeKl7plt8g=="
 
Gc-Signature: sig-1=:LjbtqUbfmvjj5C5kr1Ugj4PmLYvx9wVjZvD9GsTT4F7GrcQ\
     EdJzgI9qHxICagShLRiLMlAJjtq6N4CDfKtjvuJyE5qH7KT8UCMkSowOB4+ECxCmT\
     8rtAmj/0PIXxi0A0nxKyB09RNrCQibbUjsLS/2YyFYXEu4TRJQzRw1rLEuEfY17SA\
     RYhpTlaqwZVtR8NV7+4UKkjqpcAoFqWFQh62s7Cl+H2fjBSpqfZUJcsIk4N6wiKYd\
     4je2U/lankenQ99PZfB4jY3I5rSV2DSBVkSFsURIjYErOs0tFTQosMTAoxk//0RoK\
     UqiYY8Bh0aaUEb0rQl3/XaVe4bXTugEjHSw==:
  1. Send your request with the attached headers.

If making a request with a payload, the payload sent must have the same structure as used to generate the content-digest to ensure they match during verification.

Code Example#

Example methods for building the signature headers:

module SignatureHelper

### Build Signature Base

def self.signature_base(http_method, host, request_uri, content_digest, content_type, content_length, key_id, created, nonce)
signature_base = <<~SIGBASE
"@method": #{http_method}
"@authority": #{host}
"@request-target": #{request_uri}
"content-digest": sha256=:#{content_digest}:
"content-type": #{content_type}
"content-length": #{content_length}
"@signature-params": #{signature_params(key_id, created, nonce)}
SIGBASE

  signature_base.strip

end

### Build Signature Params

def self.signature_params(key_id, created, nonce)
'("@method" "@authority" "@request-target" "content-digest" "content-type" "content-length");keyid="%s";created=%d;nonce="%s"' % [key_id, created, nonce]
end

### Sign and return base64 encoded signature for `Gc-Signature` header

def self.signature_header(private_key_pem, signature_base)
private_key = OpenSSL::PKey::EC.new(private_key_pem)
signature = private_key.sign(OpenSSL::Digest.new("SHA512"), signature_base)
signature = Base64.strict_encode64(signature)

  "sig-1=:#{signature}:"

end

### Return value for `Gc-Signature-Input` header

def self.signature_input_header(key_id, created, nonce)
"sig-1=#{signature_params(key_id, created, nonce)}"
end

### Sort query parameters to be used in request URL and @request-target

def self.order_query_params(original_url)
return original_url if original_url.nil? || original_url.empty?

  uri = URI.parse(original_url)
  return original_url unless uri.query

  query_params = URI.decode_www_form(uri.query).sort
  uri.query = URI.encode_www_form(query_params)
  uri.to_s

end

### Sort request body to be used in request and Content-Digest

def self.order_request_body(request_body)
json_string = JSON.generate(sort_hash(request_body))
json_string
end

def self.sort_hash(hash) # Recursively sort the hash and its nested hashes
hash.each_with_object({}) do |(key, value), sorted_hash|
sorted_key = key.to_s # Convert key to string
sorted_value = value.is_a?(Hash) ? sort_hash(value) : value
sorted_hash[sorted_key] = sorted_value
end.sort.to_h # Sort the hash by keys and convert back to a hash
end

### Calculate Content-Digest

def self.content_digest(request_body)
digest = OpenSSL::Digest.new("SHA256").digest(request_body)
Base64.strict_encode64(digest)
end
end
using System.Security.Cryptography;
using System.Text;
using Org.BouncyCastle.Crypto;
using Org.BouncyCastle.OpenSsl;
using Org.BouncyCastle.Security;

class SignatureHelper
{
public SignatureHelper(
TextReader privateKeyPem,
string httpMethod,
string host,
string requestPath,
string keyId,
string? contentDigest = null,
int? contentLength = null,
string? contentType = "application/json",
string? created = null,
string? nonce = null)
{
nonce ??= Guid.NewGuid().ToString();
// Notice we expect seconds since epoch (not milliseconds/nanoseconds) to UTC now.
created ??= DateTimeOffset.UtcNow.ToUnixTimeSeconds().ToString();
var sigBase = SignatureBase(httpMethod, host, requestPath, contentDigest, contentType, contentLength, keyId, created, nonce);
var signature = GetSignature(privateKeyPem, sigBase);
this.GcSignature = SignatureHeader(signature);
this.GcSignatureInput = SignatureInputHeader(keyId, created, nonce, includeContentParams: contentDigest != null);
}

  public string GcSignature { get; private set; }
  public string GcSignatureInput { get; private set; }

  private void verifySignature(TextReader publicKeyPem, string signature, string signatureBase)
  {
      if (publicKeyPem == null) return;

      var signatureBytes = Convert.FromBase64String(signature);

      var publicKey = (AsymmetricKeyParameter)new PemReader(publicKeyPem).ReadObject();
      var verifier = SignerUtilities.GetSigner("SHA-512withECDSA");
      verifier.Init(false, publicKey);

      var messageBytes = GetMessageBytes(signatureBase);
      verifier.BlockUpdate(messageBytes, 0, messageBytes.Length);

      if (!verifier.VerifySignature(signatureBytes))
      {
          throw new Exception("Signature verification failed");
      }
  }

  public static string HashWithSHA256(string value)
  {
      var byteArray = SHA256.HashData(Encoding.UTF8.GetBytes(value));
      return Convert.ToBase64String(byteArray);
  }

  private string SignatureHeader(string signature)
  {
      return $"sig-1=:#{signature}:";
  }

  private string SignatureInputHeader(string keyId, string created, string nonce, bool includeContentParams)
      => $"sig-1={SignatureParams(keyId, created, nonce, includeContentParams)}";

  private string SignatureBase(
      string httpMethod, string host, string requestPath,
      string? contentDigest, string? contentType, int? contentLength,
      string keyId, string created, string nonce)
      => ($"\"@method\": {httpMethod}\n" +
          $"\"@authority\": {host}\n" +
          $"\"@request-target\": {requestPath}\n" +
          (string.IsNullOrWhiteSpace(contentDigest) ? "" :
              $"\"content-digest\": sha256=:#{contentDigest}:\n" +
              $"\"content-type\": {contentType}\n" +
              $"\"content-length\": {contentLength}\n") +
          $"\"@signature-params\": {SignatureParams(keyId, created, nonce, includeContentParams: contentDigest != null)}")
          .Trim();

  private string SignatureParams(string keyId, string created, string nonce, bool includeContentParams)
  {
      return @$"(""@method"" ""@authority"" ""@request-target""" +
          (includeContentParams ? @" ""content-digest"" ""content-type"" ""content-length""" : "") +
          @$");keyid=""{keyId}"";created={created};nonce=""{nonce}""";
  }

  private string GetSignature(TextReader privateKeyPem, string message)
  {
      var keys = (AsymmetricCipherKeyPair)new PemReader(privateKeyPem).ReadObject();
      var signer = SignerUtilities.GetSigner("SHA-512withECDSA");
      var privateKey = keys.Private;
      signer.Init(true, privateKey);

      var messageBytes = GetMessageBytes(message);
      signer.BlockUpdate(messageBytes, 0, messageBytes.Length);

      var signature = signer.GenerateSignature();

      return Convert.ToBase64String(signature);
  }

  private byte[] GetMessageBytes(string message) => Encoding.UTF8.GetBytes(message);

}
import { GoCardlessClient } from "gocardless-nodejs/client"
import { v4 as uuid } from 'uuid';
import * as fs from 'node:fs';
import * as crypto from 'node:crypto';
import { Buffer } from 'node:buffer';

class SignatureHelper {
public constructor(
privateKeyPem: string,
httpMethod: "GET" | "PUT" | "POST" | "PATCH" | "DELETE" | string,
host: string,
requestPath: string,
keyId: string,
contentDigest?: string,
contentLength?: number,
created?: string,
nonce?: string,
publicKeyPem?: string, // debug
contentType: string = "application/json",
) {
nonce = nonce || uuid().toString();
// Notice we expect seconds since epoch (not milliseconds/nanoseconds) to UTC now.
created = created || Math.floor(Date.now() / 1000).toString();

      const sigBase = this.getSignatureBase(httpMethod, host, requestPath, keyId, created, nonce, contentDigest, contentType, contentLength);
      console.log("Signature Base:", sigBase);
      const sig = this.getSignature(privateKeyPem, sigBase, publicKeyPem);
      this._gcSignature = this.getSignatureHeader(sig);
      this._gcSignatureInput = this.getSignatureInputHeader(keyId, created, nonce, contentDigest != null);
  }

  private _gcSignature: string;
  private _gcSignatureInput: string;

  public getGcSignature(): string { return this._gcSignature; }
  public getGcSignatureInput(): string { return this._gcSignatureInput; }

  public static getRequestBody(client: GoCardlessClient, method: 'put' | 'post', requestParameters: any, payloadKey: string): string | undefined {
      // @ts-ignore
      return JSON.stringify(client._api.getRequestBody(method, requestParameters, payloadKey));
  }

  public static getSha256DigestHeader(digest: string): string {
      return `sha256=:${digest}:`;
  }

  public static getSha256Digest(
      content: string,
      algorithm: string = 'sha256',
      encoding: crypto.BinaryToTextEncoding = 'base64'
  ): string {
      try {
          if (typeof content !== 'string') {
              throw new TypeError('Input content must be a string.');
          }

          const hash = crypto.createHash(algorithm);
          hash.update(content, 'utf8');
          const digest = hash.digest(encoding);

          return digest;
      } catch (error) {
          console.error(`Error generating content digest using ${algorithm}:`, error);
          throw error;
      }
  }

  private getSignatureBase(httpMethod: string, host: string, requestPath: string, keyId: string, created: string, nonce: string, contentDigest?: string, contentType?: string, contentLength?: number): string {
      return (`\"@method\": ${httpMethod}\n` +
          `\"@authority\": ${host}\n` +
          `\"@request-target\": ${requestPath}\n` +
          (!contentDigest ? "" :
              `\"content-digest\": sha256=:${contentDigest}:\n` +
              `\"content-type\": ${contentType}\n` +
              `\"content-length\": ${contentLength}\n`) +
          `\"@signature-params\": ${this.getSignatureParams(keyId, created, nonce, contentDigest != null)}`)
          .trim();
  }

  private getSignatureHeader(signature: string): string {
      return `sig-1=:${signature}:`;
  }

  private getSignatureInputHeader(keyId: string, created: string, nonce: string, includeContentParams: boolean): string {
      return `sig-1=${this.getSignatureParams(keyId, created, nonce, includeContentParams)}`;
  }

  private getSignatureParams(keyId: string, created: string, nonce: string, includeContentParams: boolean): string {
      return `("@method" "@authority" "@request-target"` +
          (includeContentParams ? ` "content-digest" "content-type" "content-length"` : "") +
          `);keyid="${keyId}";created=${created};nonce="${nonce}"`;
  }

  private getSignature(privateKeyPem: string, message: string, publicKeyPem?: string): string {
      const sig = this.signStringSecp521r1(privateKeyPem, message);
      if (publicKeyPem) {
          const isValid = this.verifySignatureSecp521r1(publicKeyPem, message, sig);
          if (!isValid) {
              throw new Error("Signature verification failed");
          }
      }
      return sig;
  }

  private signStringSecp521r1(privateKeyPem: string, dataToSign: string): string {
      try {
          const privateKey = crypto.createPrivateKey({
              key: privateKeyPem,
              format: 'pem',
          });

          const dataBuffer = Buffer.from(dataToSign, 'utf8');
          const sign = crypto.createSign('SHA512');
          sign.update(dataBuffer);
          sign.end();
          const signature = sign.sign(privateKey, 'base64');

          return signature;
      } catch (error) {
          console.error('Error signing string:', error);
          throw error;
      }
  }

  private verifySignatureSecp521r1(publicKeyPem: string, dataToVerify: string, signatureBase64: string): boolean {
      try {
          const publicKey = crypto.createPublicKey({
              key: publicKeyPem,
              format: 'pem',
          });

          const dataBuffer = Buffer.from(dataToVerify, 'utf8');
          const verify = crypto.createVerify('SHA512');
          verify.update(dataBuffer);
          verify.end();
          const isValid = verify.verify(publicKey, signatureBase64, 'base64');

          return isValid;
      } catch (error) {
          console.error('Error verifying signature with public key PEM:', error);
          return false;
      }
  }

}

You can then use this code to generate the HTTP Signature headers and attach them to your request:

def make_request(request_body, request_uri, private_key)
sorted_request_body = SignatureHelper.order_request_body(request_body)
content_digest = SignatureHelper.content_digest(sorted_request_body)
content_length = sorted_request_body.bytesize
content_type = "application/json"

sorted_request_uri = SignatureHelper.order_query_params(request_uri)

created = Time.now.to_i
nonce = SecureRandom.alphanumeric(16)
key_id = "YOUR_REQUEST_SIGNING_KEY_ID"

signature_base = SignatureHelper.signature_base("POST", "api-sandbox.gocardless.com", sorted_request_uri, content_digest, content_type, content_length, key_id, created, nonce)

signature_header = SignatureHelper.signature_header(private_key, signature_base)
signature_input_header = SignatureHelper.signature_input_header(key_id, created, nonce)

# Make the request attaching the required headers

end
using GoCardless;
using GoCardless.Services;

// Init the GoCardless .NET client
var accessToken = "live_XXXXXXXXXXXXXX"; // your API access token
var gocardless = GoCardlessClient.Create(accessToken, GoCardlessClient.Environment.SANDBOX);

// Set up request signing
// Provide the actual path to your file, or load the pem text into a TextReader from memory
using var privateKeyPem = File.OpenText("ec-secp521r1-priv-key.pem");

// You can get the public key from the dashboard once you upload the public certificate
string keyId = "RSK0000000000000000000";

// This request action will sign every request made by the client, and attach the right headers.
var signApiRequest = new Action<HttpRequestMessage>(request =>
{
var requestBody = request.Content?.ToString();
var contentDigest = string.IsNullOrWhiteSpace(requestBody)
? null
: SignatureHelper.HashWithSHA256(requestBody);
var contentLength = requestBody?.Length;
var requestUri = request.RequestUri ?? throw new InvalidOperationException("Request URI is null");
var signatureHelper = new SignatureHelper(
privateKeyPem: privateKeyPem,
httpMethod: request.Method.ToString(),
host: requestUri.Host,
requestPath: requestUri.PathAndQuery,
keyId: keyId,
contentDigest: contentDigest,
contentLength: contentLength
);

  request.Headers.Add("Gc-Signature", signatureHelper.GcSignature);
  request.Headers.Add("Gc-Signature-Input", signatureHelper.GcSignatureInput);
  if (contentDigest != null)
  {
      request.Headers.Add("Content-Digest", contentDigest);
  }

});
var requestSettings = new GoCardless.Internals.RequestSettings { CustomiseRequestMessage = signApiRequest };

// Get an outbound payment by ID
var outboundPayment = await gocardless.OutboundPayments.GetAsync("OUTXXXXXXXXXXXXXXX", null, requestSettings);
import { Environments } from "gocardless-nodejs/constants";
import { GoCardlessClient } from "gocardless-nodejs/client"
import { v4 as uuid } from 'uuid';
import * as fs from 'node:fs';
import { SignatureHelper } from ".";

async function main() {
const token = "<YOUR_GC_API_ACCESS_TOKEN>";
const client = new GoCardlessClient(token, Environments.Live, {});
const privateKeyPem = fs.readFileSync("ec-secp521r1-priv-key.pem", 'utf-8')
// @ts-ignore
const baseUrl = client._api._baseUrl;
const host = new URL(baseUrl).host;
const keyId = "<YOUR_UPLOADED_PUBLIC_KEY_ID>";

  // Example POST request
  const postRequestPath = `/outbound_payments`;
  const requestParams = {
      amount: 100,
      scheme: "faster_payments",
      links: {
          recipient_bank_account: "BA0003GXWZ71HA",
      }
  };
  const requestBody = SignatureHelper.getRequestBody(client, "post", requestParams, 'outbound_payments')!;
  const digest = SignatureHelper.getSha256Digest(requestBody);
  const contentLength = Buffer.byteLength(requestBody, 'utf8');
  const sigHelperPost = new SignatureHelper(
      privateKeyPem,
      "POST",
      host,
      postRequestPath,
      keyId,
      digest,
      contentLength,
  );
  const obp = await client.outboundPayments.create(requestParams, uuid(), {
      "Gc-Signature": sigHelperPost.getGcSignature(),
      "Gc-Signature-Input": sigHelperPost.getGcSignatureInput(),
      "Content-Digest": SignatureHelper.getSha256DigestHeader(digest),
  });
  console.log(obp);

  // Example GET request
  const id = obp.id
  const getRequestPath = `/outbound_payments/${id}`;
  const sigHelperGet = new SignatureHelper(
      privateKeyPem,
      "GET",
      host,
      getRequestPath,
      keyId,
  )
  const updated_obp = await client.outboundPayments.find(id, {
      "Gc-Signature": sigHelperGet.getGcSignature(),
      "Gc-Signature-Input": sigHelperGet.getGcSignatureInput(),
  })
  console.log(updated_obp);

}

main().catch(error => {
console.error("Unhandled error in main function:", error);
});

API Response#

API requests that are correctly signed will be processed normally. If the signature is invalid, the API will return 401 Unauthorized.

Test your request signing#

To test whether you are correctly building request signatures, make a POST request to the /test-signature endpoint. It will validate the Gc-Signature and Gc-Signature-Input headers and return 204 No Content if valid. No further validation is performed on any request body passed.

curl --silent \
-H "Content-Type: application/json" \
-H "Content-Digest: sha256=:dg0ak4ae6PgXhyxkn0FYx0th5QxzaDabkM2wBtufB2g=:" \
-H "Content-Length: 16" \
-H "Gocardless-Version: 2015-07-06" \
-H "Authorization: Bearer ${API_ACCESS_TOKEN}" \
-H "Gc-Signature: sig-1=:${SIGNATURE}:" \
-H "Gc-Signature-Input: sig-1=(\"@method\" \"@authority\" \"@request-target\" \"content-digest\" \"content-type\" \"content-length\");keyid=\"${KEY_ID}\";created=${created};nonce=\"${nonce}\"" \
"https://api.gocardless.com/test_signature" \
-d @- << EOF
{
  "foo": "bar"
}
EOF

What's next?#