Authentication
The finperks Giftcard API requires requests to be signed for authentication. finperks provides a client id and client secret as credentials. These credentials are scoped to the specific API environment and may be scoped further to only supporting specific actions to allow safe usage in accordance to the associated risk for the use case. The secret will never have to be included in plain text in a request, but is only used to create the signature.
The API uses this signature algorithm in addition to transport encryption via HTTPS. More information about HTTPS certificate verification can be found under Accessing the API.
Request Signature
Authorizationheader format for API requests
Authorization: FP1-HMAC-SHA256 KeyId=ID, Signature=SIGNATURE
For requests, the API expects a standard HTTP Authorization header with a custom authentication scheme, FP1-HMAC-SHA256.
Replace ID with your client id and SIGNATURE with a signature based on certain parts of the request.
The specific calculation method to build the signature is described below.
Example
Dateheader
Date: Wed, 09 Jul 2025 16:17:31 GMT
The signature includes the current time, which must be included in the request's Date header.
When the Authorization header is missing or invalid, the API returns HTTP status 401 with a WWW-Authenticate: FP1-HMAC-SHA256 header and an error.
Webhook Signature
For webhooks, we provide the signature in a custom header, Fp-Signature.
Fp-Signatureheader format for webhook requests
Fp-Signature: FP1-HMAC-SHA256 KeyId=ID, Signature=SIGNATURE
ID will be replaced by the key that was used for the Signature to optimize validation during key rotation.
SIGNATURE will be replaced with a signature based on certain parts of the request.
The algorithm for calculating the signature is described below.
Webhook requests contain a Date header as used in Request Signature.
When receiving webhooks, always verify the signature.
Calculating the Signature
Calculating the signature means generating a string with certain parameters and then using your client secret to sign the string with HMAC-SHA256.
This section describes how the signature can be calculated. It also contains a signature generator which you can use to confirm the correctness of your implementation.
The string to sign contains the parameters in the following order, separated by newlines (line feed characters \n only).
Example String to sign
api.finperks.com:443
POST
/v1/orders
Wed, 09 Jul 2025 16:17:31 GMT
5433c546-82d1-4105-baf7-7243a562273d
9123e49bd48ab640096f65ed7b21fc2a1006a799dad0553a20ec9aff745be887
- Host and port, separated by a colon, e.g.
api.finperks.com:443. The host must match the host in the request'sHost-header. - HTTP Method, e.g.
POST - Path, e.g.
/v1/orders - Query string without question mark
- Date as specified in the
Dateheader, e.g.Wed, 09 Jul 2025 16:17:31 GMT(make sure the date and your system's clock is correct, it must be within a small window around the current time) - Idempotency key as specified in the Idempotency-Key header (see below, may be empty if the header is not used for a request)
- SHA-256 digest of the request body (SHA-256 of an empty string if the body is empty), e.g.
aa4edf67aba250d4cb4fb483835d0e40bad5ef7f17de8f4efa8c2f3eb12688c2
The newlines are always necessary, regardless of whether a value is empty or not, so that number of lines in the string to sign is always the same. Do not add a newline after the body digest. Make sure to only use line feed characters, and no carriage returns. This is important, as we use the same mechanism to calculate the signature to verify your credentials.
The last step is signing the string with your client secret. Use the client secret in the form finperks provides it (do not convert the data to binary).
Example Key: 924f85f0-3581-4678-967a-5eb5615a0a41
Bash Example showing HMAC-SHA256 signing
signature=$(printf "%s" "$string_to_sign" | openssl dgst -sha256 -hmac "$api_key" | sed 's/^.* //')
echo "$signature"
Use the client secret to sign the string using HMAC with SHA-256 as hash function and encode the resulting bytes in hexidecimal form.
With the example string to sign and example key above, you can calculate the following signature:
Example Signature: 0ce8cbdab32f25d12fb0535740ab6993fdc3fd67cf4b5ac14c33588337b36066
Code Examples
Below are complete examples for generating signatures in various languages. Each example includes:
- A helper function for formatting the
Dateheader in RFC 7231 format - The signature generation function
- A usage example verified against the test data
A frequent cause of signature validation failures is an incorrectly formatted day in the Date header. The day must always be two digits with a leading zero (e.g., 09 not 9).
Many standard library date formatters produce Wed, 9 Jul 2025 instead of Wed, 09 Jul 2025, causing signature mismatches.
- Python
- JavaScript (Node.js)
- Ruby
- Go
- Java
import hashlib
import hmac
from datetime import datetime, timezone
def get_http_date() -> str:
"""
Returns the current UTC time in RFC 7231 format.
Python's %d format specifier already includes the leading zero.
"""
now = datetime.now(timezone.utc)
return now.strftime('%a, %d %b %Y %H:%M:%S GMT')
def generate_signature(
client_secret: str,
host_with_port: str,
method: str,
path: str,
query_string: str,
date_header: str,
idempotency_key: str,
body: str
) -> str:
body_digest = hashlib.sha256(body.encode('utf-8')).hexdigest()
string_to_sign = '\n'.join([
host_with_port,
method,
path,
query_string,
date_header,
idempotency_key,
body_digest
])
return hmac.new(
client_secret.encode('utf-8'),
string_to_sign.encode('utf-8'),
hashlib.sha256
).hexdigest()
# Usage example with test data
if __name__ == '__main__':
signature = generate_signature(
client_secret='30ce906050147eab919e8258871c45e7e3a3cb07',
host_with_port='api.finperks.com:443',
method='POST',
path='/v1/orders',
query_string='',
date_header='Sun, 06 Nov 2005 08:49:37 GMT',
idempotency_key='123e4567-e89b-12d3-a456-426614174000',
body='{"amount":1000,"currency":"USD"}'
)
expected = '786bd09c754ad301bb267a158c7b79a5a5a262dc50656c6d24c2c49bb49a5270'
assert signature == expected, f'Signature mismatch: {signature}'
print(f'Signature: {signature}')
# For live requests, use get_http_date() instead of a fixed date:
# date_header = get_http_date()
const crypto = require('crypto');
/**
* Returns the current UTC time in RFC 7231 format.
* WARNING: Do NOT use toUTCString() - it may omit the leading zero on some platforms.
*/
function getHttpDate() {
const now = new Date();
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
const months = [
'Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun',
'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'
];
const day = days[now.getUTCDay()];
const date = String(now.getUTCDate()).padStart(2, '0');
const month = months[now.getUTCMonth()];
const year = now.getUTCFullYear();
const hours = String(now.getUTCHours()).padStart(2, '0');
const minutes = String(now.getUTCMinutes()).padStart(2, '0');
const seconds = String(now.getUTCSeconds()).padStart(2, '0');
return `${day}, ${date} ${month} ${year} ${hours}:${minutes}:${seconds} GMT`;
}
function generateSignature(
clientSecret,
hostWithPort,
method,
path,
queryString,
dateHeader,
idempotencyKey,
body
) {
const bodyDigest = crypto
.createHash('sha256')
.update(body, 'utf8')
.digest('hex');
const stringToSign = [
hostWithPort,
method,
path,
queryString,
dateHeader,
idempotencyKey,
bodyDigest,
].join('\n');
return crypto
.createHmac('sha256', clientSecret)
.update(stringToSign, 'utf8')
.digest('hex');
}
// Usage example with test data
const signature = generateSignature(
'30ce906050147eab919e8258871c45e7e3a3cb07',
'api.finperks.com:443',
'POST',
'/v1/orders',
'',
'Sun, 06 Nov 2005 08:49:37 GMT',
'123e4567-e89b-12d3-a456-426614174000',
'{"amount":1000,"currency":"USD"}'
);
const expected = '786bd09c754ad301bb267a158c7b79a5a5a262dc50656c6d24c2c49bb49a5270';
console.assert(signature === expected, `Signature mismatch: ${signature}`);
console.log(`Signature: ${signature}`);
// For live requests, use getHttpDate() instead of a fixed date:
// const dateHeader = getHttpDate();
require 'openssl'
require 'time'
# Returns the current UTC time in RFC 7231 format.
# Ruby's httpdate method correctly formats dates per RFC 7231,
# including the leading zero on single-digit days.
def get_http_date
Time.now.utc.httpdate
end
def generate_signature(
client_secret:,
host_with_port:,
method:,
path:,
query_string:,
date_header:,
idempotency_key:,
body:
)
body_digest = OpenSSL::Digest::SHA256.hexdigest(body)
string_to_sign = [
host_with_port,
method,
path,
query_string,
date_header,
idempotency_key,
body_digest
].join("\n")
OpenSSL::HMAC.hexdigest('SHA256', client_secret, string_to_sign)
end
# Usage example with test data
signature = generate_signature(
client_secret: '30ce906050147eab919e8258871c45e7e3a3cb07',
host_with_port: 'api.finperks.com:443',
method: 'POST',
path: '/v1/orders',
query_string: '',
date_header: 'Sun, 06 Nov 2005 08:49:37 GMT',
idempotency_key: '123e4567-e89b-12d3-a456-426614174000',
body: '{"amount":1000,"currency":"USD"}'
)
expected = '786bd09c754ad301bb267a158c7b79a5a5a262dc50656c6d24c2c49bb49a5270'
raise "Signature mismatch: #{signature}" unless signature == expected
puts "Signature: #{signature}"
# For live requests, use get_http_date instead of a fixed date:
# date_header = get_http_date
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"fmt"
"strings"
"time"
)
func getHTTPDate() string {
// http.TimeFormat is the correct format for RFC 7231 dates.
// WARNING: Do NOT use time.RFC1123 - it uses "2" instead of "02" for days,
// which omits the leading zero on single-digit days.
return time.Now().UTC().Format(http.TimeFormat)
}
func generateSignature(
clientSecret string,
hostWithPort string,
method string,
path string,
queryString string,
dateHeader string,
idempotencyKey string,
body string,
) string {
bodyHash := sha256.Sum256([]byte(body))
bodyDigest := hex.EncodeToString(bodyHash[:])
stringToSign := strings.Join([]string{
hostWithPort,
method,
path,
queryString,
dateHeader,
idempotencyKey,
bodyDigest,
}, "\n")
mac := hmac.New(sha256.New, []byte(clientSecret))
mac.Write([]byte(stringToSign))
return hex.EncodeToString(mac.Sum(nil))
}
// Usage example with test data
func main() {
signature := generateSignature(
"30ce906050147eab919e8258871c45e7e3a3cb07",
"api.finperks.com:443",
"POST",
"/v1/orders",
"",
"Sun, 06 Nov 2005 08:49:37 GMT",
"123e4567-e89b-12d3-a456-426614174000",
`{"amount":1000,"currency":"USD"}`,
)
expected := "786bd09c754ad301bb267a158c7b79a5a5a262dc50656c6d24c2c49bb49a5270"
if signature != expected {
panic(fmt.Sprintf("Signature mismatch: %s", signature))
}
fmt.Printf("Signature: %s\n", signature)
// For live requests, use getHTTPDate() instead of a fixed date:
// dateHeader := getHTTPDate()
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.time.ZonedDateTime;
import java.time.ZoneOffset;
import java.time.format.DateTimeFormatter;
import java.util.Locale;
public class SignatureGenerator {
// Use "dd" (not "d") to ensure leading zero on single-digit days.
// Locale.ENGLISH ensures English day/month names regardless of system locale.
private static final DateTimeFormatter HTTP_DATE_FORMAT = DateTimeFormatter
.ofPattern("EEE, dd MMM yyyy HH:mm:ss 'GMT'", Locale.ENGLISH)
.withZone(ZoneOffset.UTC);
public static String getHttpDate() {
return HTTP_DATE_FORMAT.format(ZonedDateTime.now(ZoneOffset.UTC));
}
public static String generateSignature(
String clientSecret,
String hostWithPort,
String method,
String path,
String queryString,
String dateHeader,
String idempotencyKey,
String body
) throws Exception {
MessageDigest sha256 = MessageDigest.getInstance("SHA-256");
byte[] bodyHashBytes = sha256.digest(body.getBytes(StandardCharsets.UTF_8));
String bodyDigest = bytesToHex(bodyHashBytes);
String stringToSign = String.join("\n",
hostWithPort,
method,
path,
queryString,
dateHeader,
idempotencyKey,
bodyDigest
);
Mac hmac = Mac.getInstance("HmacSHA256");
SecretKeySpec secretKey = new SecretKeySpec(
clientSecret.getBytes(StandardCharsets.UTF_8),
"HmacSHA256"
);
hmac.init(secretKey);
byte[] signatureBytes = hmac.doFinal(
stringToSign.getBytes(StandardCharsets.UTF_8)
);
return bytesToHex(signatureBytes);
}
private static String bytesToHex(byte[] bytes) {
StringBuilder sb = new StringBuilder();
for (byte b : bytes) {
sb.append(String.format("%02x", b));
}
return sb.toString();
}
// Usage example with test data
public static void main(String[] args) throws Exception {
String signature = generateSignature(
"30ce906050147eab919e8258871c45e7e3a3cb07",
"api.finperks.com:443",
"POST",
"/v1/orders",
"",
"Sun, 06 Nov 2005 08:49:37 GMT",
"123e4567-e89b-12d3-a456-426614174000",
"{\"amount\":1000,\"currency\":\"USD\"}"
);
String expected = "786bd09c754ad301bb267a158c7b79a5a5a262dc50656c6d24c2c49bb49a5270";
if (!signature.equals(expected)) {
throw new AssertionError("Signature mismatch: " + signature);
}
System.out.println("Signature: " + signature);
// For live requests, use getHttpDate() instead of a fixed date:
// String dateHeader = getHttpDate();
}
}
Test Data for Verification
Use these test cases to verify your implementation produces the expected signatures.
POST Request:
| Parameter | Value |
|---|---|
| Secret | 30ce906050147eab919e8258871c45e7e3a3cb07 |
| Host | api.finperks.com:443 |
| Method | POST |
| Path | /v1/orders |
| Query String | (empty) |
| Date | Sun, 06 Nov 2005 08:49:37 GMT |
| Idempotency Key | 123e4567-e89b-12d3-a456-426614174000 |
| Body | {"amount":1000,"currency":"USD"} |
| Expected Signature | 786bd09c754ad301bb267a158c7b79a5a5a262dc50656c6d24c2c49bb49a5270 |
GET Request:
| Parameter | Value |
|---|---|
| Secret | 30ce906050147eab919e8258871c45e7e3a3cb07 |
| Host | api.finperks.com:443 |
| Method | GET |
| Path | /v1/products |
| Query String | ?countrycode=DE |
| Date | Sun, 06 Nov 2005 08:49:37 GMT |
| Idempotency Key | (empty) |
| Body | (empty) |
| Expected Signature | 3c8e65ab28539ace0817369d6943584d78be271dbe93bcb5408ee98a0141e30e |
Example
Example Request Headers with signature in the
Authorizationheader
POST /v1/orders HTTP/1.1
Authorization: FP1-HMAC-SHA256 KeyId=6b0dff1a-f729-42d1-9eed-d2f17ef5aedb, Signature=0ce8cbdab32f25d12fb0535740ab6993fdc3fd67cf4b5ac14c33588337b36066
Date: Wed, 09 Jul 2025 16:17:31 GMT
Host: api.finperks.com
Signature Generator
This signature generator allows you to verify and debug your implementation of the signature algorithm.
Note:
- Make sure to not have extra whitespace in any of the fields.
- Make sure you do not accidentally change the newline characters when copying code between the browser and your text editor. E.g. Windows often adds carriage return characters in addition to line feed characters.
The bytes put into the signature must be the same sent with the actual request, so different kinds of newlines and whitespace can invalidate the signature.