How to Sign Requests
By default, all requests to the Confident API have to be signed using a valid API key and API secret. This is an extra step when integrating (and can be a little intimidating) but it is important so that we can be sure your requests are really coming from you, which helps us protect your account and your information.
This page explains every step needed to correctly sign API requests and
ships complete reference implementations you can copy into your project:
Python and
JavaScript (Node.js). The
code examples on every endpoint page call these implementations as
sign_request() / signRequest().
Quick Start
- Copy the
sign_requestimplementation for your language (below) into your project — or port it, following the algorithm in the next section. - Put the current unix time in seconds in an
X-ConfidentLims-Timestampheader. - Call
sign_request(method, path, headers, data, api_key, api_secret)wherepathis the URL path without the hostname or querystring (for example/v0/labs/orders),headersholds the timestamp header, anddatais the querystring parameters (GET) or the form fields (POST/PUT/DELETE) you are about to send. File uploads are never signed. - Send the request with three headers:
X-ConfidentLims-APIKey(your API key),X-ConfidentLims-Timestamp, andX-ConfidentLims-Signature(the value returned bysign_request). - Check your implementation against the test vector and
then end-to-end against
POST /v0/signingtest.
Signed requests must reach the server within
30 seconds of the timestamp or they are rejected
with request_too_old (the response includes current_server_time for
clock calibration).
Disabling Signing
To speed development you can disable signing on a per-credential
basis. This makes all requests require only the
X-ConfidentLims-APIKey header.
You can enable/disable signing from the organization settings page — you may want to do this while in development to get started more quickly.
How Does Request Signing Work?
Request signing uses a pair of known credentials (your API key and API secret) to first identify your account (the API key part) and second to create a cryptographic "signature" of the important data being sent. The server also knows the API secret and can calculate the same signature using the data it actually received.
Different data or an incorrect API secret (or a long delay) results in a different signature, effectively proving to the server that the request really did come from you (or at least someone with both the API key and the API secret). This helps ensure that only you or people you intentionally share credentials with can use your account.
Never send your API secret in a request — it is only ever used to compute signatures.
The Algorithm — Step by Step
Signing is broken into three phases — creating one canonical string from all the important parts of the request, HMAC-SHA256 hashing that string with your API secret, and prefixing the result with the algorithm name and the list of signed headers so the server can validate it.
The steps below sign this example request (the same inputs as the test vector):
method = 'POST'
route = '/v0/signingtest'
headers = {'X-ConfidentLims-Timestamp': '1474507118'}
data = {'example_field': 'hello world!'}
api_key = '88b750a8-d414-4aee-b26c-2cc7e85434dd'
api_secret = '043bca27-c4d1-4d39-86d6-e5f0c3b4bb4f'
- Base string. Concatenate the uppercase HTTP method and the route
(the URL path without the hostname or querystring).
→
POST/v0/signingtest - Sorted headers. Take every header you are signing — you must include
X-ConfidentLims-Timestampand must not includeX-ConfidentLims-APIKeyorX-ConfidentLims-Signature— lowercase the names, and sort the (name, value) pairs ascending by name. →[('x-confidentlims-timestamp', '1474507118')] - Header string. URL-encode the sorted headers as
name=value&.... →x-confidentlims-timestamp=1474507118 - Header list. Join the lowercase header names with
;. The server uses this to know which headers you signed. →x-confidentlims-timestamp - Sorted params. Take the data you are sending — the querystring
parameters for GET, the form fields for POST/PUT/DELETE, never file
uploads — and sort the (name, value) pairs ascending by name.
→
[('example_field', 'hello world!')] - Append the API key. Add
('api_key', <your API key>)to the end of that list (after sorting — it is not sorted in). →[('example_field', 'hello world!'), ('api_key', '88b750a8-…')] - Param string. URL-encode the list as
name=value&...(see percent-encoding — note the+and%21). →example_field=hello+world%21&api_key=88b750a8-d414-4aee-b26c-2cc7e85434dd - Encoded base string. Percent-encode the base string from step 1.
→
POST%2Fv0%2Fsigningtest - Signing string. Join the results of steps 8, 3 and 7 with
&. →POST%2Fv0%2Fsigningtest&x-confidentlims-timestamp=1474507118&example_field=hello+world%21&api_key=88b750a8-d414-4aee-b26c-2cc7e85434dd - HMAC. Compute HMAC-SHA256 over the signing string (UTF-8) with your
API secret (UTF-8) as the key, hex-encoded lowercase.
→
208b3c11b8f341e46ca7f5055ee6c9297a18217c2a54dc1955230d074f47816b - Final signature. Prefix with the algorithm name and the header list
from step 4, separated by colons:
CC0-HMAC-SHA256:<header list>:<hex digest>. →CC0-HMAC-SHA256:x-confidentlims-timestamp:208b3c11b8f341e46ca7f5055ee6c9297a18217c2a54dc1955230d074f47816b
Send that value in the X-ConfidentLims-Signature header, alongside
X-ConfidentLims-APIKey and the exact X-ConfidentLims-Timestamp you
signed.
Percent-encoding Rules
Every language and library percent-encodes slightly differently, and a
single mismatched character produces an invalid_signature. The API uses
Python's urllib.parse.urlencode / urllib.parse.quote_plus behaviour:
- Unreserved characters are left as-is:
A-Z,a-z,0-9,-,_,.,~. - A space becomes
+(not%20). - Everything else — including
/,:,!,',(,),*,,,&,=and all non-ASCII characters (as UTF-8 bytes) — becomes%XXwith uppercase hex digits. - Values are converted to strings before encoding: numbers as their
decimal form, booleans as
True/Falsein Python ortrue/falsein JavaScript — send them the same way you sign them.
The JavaScript reference implementation below shows how to correct
encodeURIComponent (which leaves !'()* alone and encodes spaces as
%20) to match these rules.
If your HMAC library needs a sanity check, these hold for HMAC-SHA256 (message, key → hex digest):
| Message | Key | HMAC-SHA256 |
|---|---|---|
foo |
bar |
147933218aaabc0b8b10a2b3a5c34684c8d94341bcf10a4736dc7270f7741851 |
test |
secret |
0329a06b62cd16b33eb6792be8c60b158d89a2ee3a876fce9a881ebb488c0914 |
longer_content |
longer_key |
3b3e8443e5156d9422998442c014a0ee2a8c398f29e13442dbf3301401bb192a |
Test Vector
Feed these inputs to your implementation; it must produce exactly this signature before you try a live request.
| Input | Value |
|---|---|
| method | POST |
| route | /v0/signingtest |
| headers | {'X-ConfidentLims-Timestamp': '1474507118'} |
| data | {'example_field': 'hello world!'} |
| api_key | 88b750a8-d414-4aee-b26c-2cc7e85434dd |
| api_secret | 043bca27-c4d1-4d39-86d6-e5f0c3b4bb4f |
| signing string | POST%2Fv0%2Fsigningtest&x-confidentlims-timestamp=1474507118&example_field=hello+world%21&api_key=88b750a8-d414-4aee-b26c-2cc7e85434dd |
| signature | CC0-HMAC-SHA256:x-confidentlims-timestamp:208b3c11b8f341e46ca7f5055ee6c9297a18217c2a54dc1955230d074f47816b |
Reference Implementation — Python
Standard library only (Python 3). Save as sign_request.py; the endpoint
examples import it with from sign_request import sign_request.
"""Sign a Confident LIMS API request. Python 3, standard library only.
signature = sign_request('GET', '/v0/labs/orders', headers, params, API_KEY, API_SECRET)
- method: HTTP method, e.g. 'GET' or 'POST'
- route: URL path after the hostname, without the querystring,
e.g. '/v0/labs/orders'
- headers: headers to sign; must include 'X-ConfidentLims-Timestamp' (unix
seconds) and must NOT include X-ConfidentLims-APIKey or
X-ConfidentLims-Signature
- data: the querystring params (GET) or form fields (POST/PUT/DELETE)
being sent; file uploads are never signed
- api_key / api_secret: your API credentials, as strings
"""
import hashlib
import hmac
import urllib.parse
def sign_request(method, route, headers, data, api_key, api_secret):
# 1. base string: METHOD + route, e.g. 'GET/v0/labs/orders'
base_string = method.upper() + route
# 2. lowercased header names, sorted ascending
sorted_headers = sorted(
(name.lower(), str(value)) for name, value in headers.items()
)
# 3. 'name=value&...' for the sorted headers, url-encoded
header_string = urllib.parse.urlencode(sorted_headers)
# 4. 'name;name;...' listing the signed header names
header_list = ';'.join(name for name, _ in sorted_headers)
# 5. data fields sorted ascending by name (skip file uploads)
params = sorted(
(name, str(value)) for name, value in data.items()
if not hasattr(value, 'read')
)
# 6. append api_key=<your api key> to the END of the list
params.append(('api_key', api_key))
# 7. 'name=value&...' for the sorted params, url-encoded
param_string = urllib.parse.urlencode(params)
# 8. percent-encode the base string ('/' becomes '%2F')
encoded_base_string = urllib.parse.quote_plus(base_string)
# 9. join the three pieces with '&'
signing_string = '&'.join(
[encoded_base_string, header_string, param_string]
)
# 10. HMAC-SHA256 of the signing string, keyed with the api secret
raw_signature = hmac.new(
api_secret.encode('utf-8'),
signing_string.encode('utf-8'),
hashlib.sha256,
).hexdigest()
# 11. prefix with the algorithm and the signed header list
return 'CC0-HMAC-SHA256:{}:{}'.format(header_list, raw_signature)
Reference Implementation — JavaScript (Node.js)
No dependencies (Node.js 18+, ES modules). Save as sign_request.js; the
endpoint examples import it with
import { signRequest } from './sign_request.js'.
// Sign a Confident LIMS API request. Node.js 18+, no dependencies.
//
// const signature = signRequest('GET', '/v0/labs/orders', headers, params, API_KEY, API_SECRET);
//
// - method: HTTP method, e.g. 'GET' or 'POST'
// - route: URL path after the hostname, without the querystring,
// e.g. '/v0/labs/orders'
// - headers: headers to sign; must include 'X-ConfidentLims-Timestamp' (unix
// seconds) and must NOT include X-ConfidentLims-APIKey or
// X-ConfidentLims-Signature
// - data: the querystring params (GET) or form fields (POST/PUT/DELETE)
// being sent; file uploads are never signed
// - apiKey / apiSecret: your API credentials, as strings
import { createHmac } from 'node:crypto';
// Match Python's urllib.parse.quote_plus: everything except A-Z a-z 0-9 - _ . ~
// is percent-encoded (uppercase hex) and a space becomes '+'.
function encode(value) {
return encodeURIComponent(String(value))
.replace(/[!'()*]/g, (c) => '%' + c.charCodeAt(0).toString(16).toUpperCase())
.replace(/%20/g, '+');
}
function urlencode(pairs) {
return pairs.map(([name, value]) => `${encode(name)}=${encode(value)}`).join('&');
}
function sortPairs(pairs) {
return pairs.sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
}
export function signRequest(method, route, headers, data, apiKey, apiSecret) {
// 1. base string: METHOD + route, e.g. 'GET/v0/labs/orders'
const baseString = method.toUpperCase() + route;
// 2. lowercased header names, sorted ascending
const sortedHeaders = sortPairs(
Object.entries(headers).map(([name, value]) => [name.toLowerCase(), String(value)])
);
// 3. 'name=value&...' for the sorted headers, url-encoded
const headerString = urlencode(sortedHeaders);
// 4. 'name;name;...' listing the signed header names
const headerList = sortedHeaders.map(([name]) => name).join(';');
// 5. data fields sorted ascending by name (skip file uploads)
const params = sortPairs(
Object.entries(data)
.filter(([, value]) => !(value instanceof Blob))
.map(([name, value]) => [name, String(value)])
);
// 6. append api_key=<your api key> to the END of the list
params.push(['api_key', apiKey]);
// 7. 'name=value&...' for the sorted params, url-encoded
const paramString = urlencode(params);
// 8. percent-encode the base string ('/' becomes '%2F')
const encodedBaseString = encode(baseString);
// 9. join the three pieces with '&'
const signingString = [encodedBaseString, headerString, paramString].join('&');
// 10. HMAC-SHA256 of the signing string, keyed with the api secret
const rawSignature = createHmac('sha256', apiSecret).update(signingString).digest('hex');
// 11. prefix with the algorithm and the signed header list
return `CC0-HMAC-SHA256:${headerList}:${rawSignature}`;
}
Verifying Your Implementation
- Confirm your code reproduces the test vector exactly.
- Send a signed request to
POST /v0/signingtestwith your real credentials. Asuccess: trueresponse means signing works; aninvalid_signatureerror means the server computed a different signature — re-check the route (it must include the/v0/...prefix and no querystring), the header values, the percent-encoding, and thatapi_keyis the last param. - A
request_too_olderror means your clock is off; compare against thecurrent_server_timein the response.