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

  1. Copy the sign_request implementation for your language (below) into your project — or port it, following the algorithm in the next section.
  2. Put the current unix time in seconds in an X-ConfidentLims-Timestamp header.
  3. Call sign_request(method, path, headers, data, api_key, api_secret) where path is the URL path without the hostname or querystring (for example /v0/labs/orders), headers holds the timestamp header, and data is the querystring parameters (GET) or the form fields (POST/PUT/DELETE) you are about to send. File uploads are never signed.
  4. Send the request with three headers: X-ConfidentLims-APIKey (your API key), X-ConfidentLims-Timestamp, and X-ConfidentLims-Signature (the value returned by sign_request).
  5. 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'
  1. Base string. Concatenate the uppercase HTTP method and the route (the URL path without the hostname or querystring). → POST/v0/signingtest
  2. Sorted headers. Take every header you are signing — you must include X-ConfidentLims-Timestamp and must not include X-ConfidentLims-APIKey or X-ConfidentLims-Signature — lowercase the names, and sort the (name, value) pairs ascending by name. → [('x-confidentlims-timestamp', '1474507118')]
  3. Header string. URL-encode the sorted headers as name=value&.... → x-confidentlims-timestamp=1474507118
  4. Header list. Join the lowercase header names with ;. The server uses this to know which headers you signed. → x-confidentlims-timestamp
  5. 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!')]
  6. 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-…')]
  7. 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
  8. Encoded base string. Percent-encode the base string from step 1. → POST%2Fv0%2Fsigningtest
  9. 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
  10. HMAC. Compute HMAC-SHA256 over the signing string (UTF-8) with your API secret (UTF-8) as the key, hex-encoded lowercase. → 208b3c11b8f341e46ca7f5055ee6c9297a18217c2a54dc1955230d074f47816b
  11. 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 %XX with uppercase hex digits.
  • Values are converted to strings before encoding: numbers as their decimal form, booleans as True/False in Python or true/false in 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

  1. Confirm your code reproduces the test vector exactly.
  2. Send a signed request to POST /v0/signingtest with your real credentials. A success: true response means signing works; an invalid_signature error 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 that api_key is the last param.
  3. A request_too_old error means your clock is off; compare against the current_server_time in the response.