# 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](#reference-implementation-python) and
[JavaScript (Node.js)](#reference-implementation-javascript-nodejs). 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](#test-vector) and
   then end-to-end against
   [`POST /v0/signingtest`](/v0/docs/signing-test).

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](#test-vector)):

```python
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](#percent-encoding-rules) — 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`.

```python
"""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'`.

```javascript
// 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](#test-vector) exactly.
2. Send a signed request to
   [`POST /v0/signingtest`](/v0/docs/signing-test)
   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.

---

HTML version: https://api.confidentcannabis.com/v0/docs/request-signing
