> ## Documentation Index
> Fetch the complete documentation index at: https://docs.userecord.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Authentication

> You need two things: your key id and your secret. Everything else is generated for you.

## The short version

You hold **two values**. Your code (or the **Try it** button) makes the rest on every call.

| You give | Looks like | What it is |
| - | - | - |
| **Key id** | `VFN_TEST_…` or `VFN_LIVE_…` | Who you are. Sent as the `x-api-key` header. |
| **Secret** | `VFN_SK_…` | Proves it is you. **Never sent.** It only signs. |

| Generated for each call | How |
| - | - |
| `x-timestamp` | The current time in milliseconds. |
| `x-signature` | A signature of the request, made with your secret. |
| `Idempotency-Key` | A new UUID on each write (`POST`, `PUT`, `PATCH`, `DELETE`). |

You do not calculate these by hand. Copy the [signing helper](#signing-helper) once, then call `call("GET", "/api/v1/partner/…")`.

<Info>
  Optional: `x-on-behalf-of` is your own id for the recruiter making the call (1 to 128 of `A-Z a-z 0-9 . _ : @ -`). It sets per-recruiter rate limits, is kept in the audit log, and fills `owner` on a mandate or `requested_by` on a skill request when you leave them out.
</Info>

## Why a signature and not just the secret?

Sending a secret on every request risks leaking it (logs, proxies, screenshots). Instead, your code **signs** each request with the secret, and Record checks the signature with its own copy. The secret stays on your side.

Three small rules follow from that:

* **Fresh every time.** The signature covers the exact method, path, time and body, so sign each request again, retries included.
* **Within 5 minutes.** The timestamp must be close to Record's clock.
* **A write is accepted once.** A signed `POST`, `PUT`, `PATCH` or `DELETE` that is replayed is refused. To retry safely, send the same `Idempotency-Key` with a new signature. See [Idempotency](/blue-collar-api/reference/requests-and-responses#idempotency).

## Why does Try it say `x-secret-key`?

`x-secret-key` is **only a form field** in these docs so you can type your secret once. It is not a real API header.

When you press **Send**, the docs page, in your browser:

1. reads the secret you typed,
2. makes `x-timestamp` and `x-signature` (and an `Idempotency-Key` if you left it empty),
3. **removes `x-secret-key`**, then sends the request.

So Record only ever receives `x-api-key`, `x-timestamp` and `x-signature`, exactly like your own code. The secret goes nowhere except your own browser: not to Record, and not to anyone else. These docs keep what you type in your browser's local storage so you do not retype it. Clear your browser's site data to remove it, and never type a LIVE secret on a shared computer.

## Try it from these docs

<Steps>
  <Step title="Open any endpoint">
    For example [Search skills](/api-reference/skills/search-skills), then click **Try it**.
  </Step>

  <Step title="Enter your two values">
    Put your key id in `x-api-key` and your secret in `x-secret-key`. Use a **TEST** key.
  </Step>

  <Step title="Fill the request and send">
    Fill the fields, press **Send**. Leave `Idempotency-Key` empty: it is made for you.
  </Step>
</Steps>

<Note>
  Try it needs a secure page (HTTPS or localhost) to sign. A `401 UNAUTHORIZED` means the key id or secret is wrong, or the key has been regenerated.
</Note>

## API keys

Create a key in the Record playground under **Settings → API keys**. The secret is shown **once**, when the key is created, so store it in your secret manager straight away. There is one active key per project and mode.

<Warning>
  Regenerating a key issues a new pair and turns the old one off immediately. The new key starts without Blue Collar API scopes, so ask Record to enable it again before you switch.
</Warning>

### Test and live

| | TEST key | LIVE key |
| - | - | - |
| Data | Test data only. TEST and LIVE never see each other's objects. | Live data only. |
| Phone numbers | Only numbers Record has listed for the key. Others answer `403 FORBIDDEN`. | Any valid number. |
| Messages | **Real.** Test campaigns still send WhatsApp, SMS and calls. | Real. |
| Limits | One tenth of the live limits. | See [Limits](/blue-collar-api/reference/limits). |

## Scopes

Record switches the Blue Collar API on per key by granting scopes. A key without any scopes answers `403 SCOPE_MISSING`. Each endpoint needs one of the scopes listed for it.

| Scope | Opens |
| - | - |
| `skills:read` | Skill search, browse, lookup, match and extract. |
| `skills:request` | Ask Record to add a missing skill, and track your requests. |
| `workflows:read` | Read workflows. |
| `workflows:write` | Create, change and archive workflows. Also reads them. |
| `mandates:read` | Read job mandates and their questions, and the place, country, employment-type and shift lookups. |
| `mandates:write` | Create, change and close job mandates. Also everything in `mandates:read`. |
| `campaigns:write` | Create, start, pause and resume campaigns, add candidates and resend links. Also reads campaigns and candidate rows (without verdicts). |
| `results:read` | Campaigns, candidate rows and result records. |
| `results:answers` | Adds what the candidate said to `GET /candidates/{candidate_id}/result?include=answers`. |
| `webhooks:manage` | Webhook endpoints, delivery logs, tests and backfills. |
| `events:read` | The event feed (`POST /events/pull`) and `GET /events/{event_id}`. |
| `analytics:read` | Workflow, mandate and campaign analytics. |

## How a request is signed

The helper below does this for you. This is what it does, in case you write your own.

<Steps>
  <Step title="Take the time">
    `timestamp` is the current time in **milliseconds** since the epoch.
  </Step>

  <Step title="Build the string to sign">
    ```text theme={null}
    METHOD:/path?query:timestamp:body
    ```

    * `METHOD` is upper case: `GET`, `POST`, `PUT`, `PATCH` or `DELETE`.
    * `/path?query` is the full path exactly as you send it, starting with `/api/v1/partner`, query string included.
    * `body` is the exact bytes you send. Empty for a request without a body.
  </Step>

  <Step title="Sign it">
    HMAC-SHA256 of that string, with your secret as the key, as lowercase hex.
  </Step>

  <Step title="Send the headers">
    | Header | Value |
    | - | - |
    | `x-api-key` | Your key id. |
    | `x-timestamp` | The timestamp from step 1. |
    | `x-signature` | The hex signature from step 3. |
    | `Content-Type` | `application/json`, on requests with a body. |
    | `Idempotency-Key` | A new UUID on each write. |
    | `x-on-behalf-of` | Optional, see above. |
  </Step>
</Steps>

## Signing helper

Copy one helper into your code. Every example in these docs calls it. It needs only your key id and secret.

<CodeGroup>
  ```javascript JavaScript theme={null}
  // Node 18+. No packages needed.
  const crypto = require("crypto");

  const BASE = "https://api-bc.userecord.io";
  const KEY = process.env.RECORD_API_KEY;       // VFN_TEST_… or VFN_LIVE_…
  const SECRET = process.env.RECORD_API_SECRET; // VFN_SK_…

  async function call(method, pathAndQuery, body, idempotencyKey = crypto.randomUUID()) {
    const raw = body === undefined ? "" : JSON.stringify(body);
    const ts = String(Date.now());
    const sig = crypto
      .createHmac("sha256", SECRET)
      .update(`${method}:${pathAndQuery}:${ts}:${raw}`)
      .digest("hex");

    const headers = { "x-api-key": KEY, "x-timestamp": ts, "x-signature": sig };
    if (raw) headers["Content-Type"] = "application/json";
    if (method !== "GET") headers["Idempotency-Key"] = idempotencyKey;

    return fetch(BASE + pathAndQuery, { method, headers, body: raw || undefined });
  }
  ```

  ```python Python theme={null}
  import hashlib, hmac, json, os, time, uuid
  import requests

  BASE = "https://api-bc.userecord.io"
  KEY = os.environ["RECORD_API_KEY"]        # VFN_TEST_… or VFN_LIVE_…
  SECRET = os.environ["RECORD_API_SECRET"]  # VFN_SK_…

  def call(method, path_and_query, body=None, idempotency_key=None):
      raw = json.dumps(body, separators=(",", ":")) if body is not None else ""
      ts = str(int(time.time() * 1000))
      sig = hmac.new(SECRET.encode(), f"{method}:{path_and_query}:{ts}:{raw}".encode(),
                     hashlib.sha256).hexdigest()

      headers = {"x-api-key": KEY, "x-timestamp": ts, "x-signature": sig}
      if raw:
          headers["Content-Type"] = "application/json"
      if method != "GET":
          headers["Idempotency-Key"] = idempotency_key or str(uuid.uuid4())

      return requests.request(method, BASE + path_and_query, data=raw.encode() or None,
                              headers=headers, timeout=30)
  ```

  ```bash cURL theme={null}
  BASE="https://api-bc.userecord.io"
  KEY="$RECORD_API_KEY"       # VFN_TEST_… or VFN_LIVE_…
  SECRET="$RECORD_API_SECRET" # VFN_SK_…

  # usage: pcall METHOD "/api/v1/partner/path?query" ['{"json":"body"}']
  pcall() {
    local method="$1" path="$2" body="${3:-}"
    local ts=$(( $(date +%s) * 1000 ))
    local sig=$(printf '%s' "$method:$path:$ts:$body" | openssl dgst -sha256 -hmac "$SECRET" -hex | sed 's/^.* //')
    local args=(-s -X "$method" "$BASE$path" -H "x-api-key: $KEY" -H "x-timestamp: $ts" -H "x-signature: $sig")
    [ -n "$body" ] && args+=(-H "Content-Type: application/json" --data-binary "$body")
    [ "$method" != "GET" ] && args+=(-H "Idempotency-Key: $(uuidgen)")
    curl "${args[@]}"
  }
  ```
</CodeGroup>

Test your key with a read:

<CodeGroup>
  ```javascript JavaScript theme={null}
  const res = await call("GET", "/api/v1/partner/workflows?limit=1");
  console.log(res.status, await res.json());
  ```

  ```python Python theme={null}
  res = call("GET", "/api/v1/partner/workflows?limit=1")
  print(res.status_code, res.json())
  ```

  ```bash cURL theme={null}
  pcall GET "/api/v1/partner/workflows?limit=1"
  ```
</CodeGroup>

A `200` with `"success": true` means the key, secret and clock are right.

## If you get `401 UNAUTHORIZED`

| Check | Fix |
| - | - |
| Key id and secret match | They must be the same pair. Regenerating a key turns the old pair off. |
| You sign what you send | The path, query and body in the signature must be exactly what you send. |
| Your clock | Sync your server clock. It must be within 5 minutes of Record's. |
| Signing again on a retry | A signature works once for writes. Sign afresh and reuse only the `Idempotency-Key`. |

## Answers

<CodeGroup>
  ```json Success theme={null}
  { "success": true, "data": { } }
  ```

  ```json Failure (4xx / 5xx) theme={null}
  {
    "success": false,
    "error": {
      "code": "NOT_FOUND",
      "message": "...",
      "details": [{ "path": "body.name", "message": "..." }],
      "request_id": "req_..."
    }
  }
  ```

  ```json Paged list theme={null}
  { "items": [ ], "count": 50, "total": 240, "next_cursor": "eyJpZCI6...", "has_more": true }
  ```
</CodeGroup>

Quote `request_id` (also the `x-request-id` header) when you contact Record support. Pass `next_cursor` back as `?cursor=` until `has_more` is `false`; `total` can be `null` when a filter is used. See [Requests and responses](/blue-collar-api/reference/requests-and-responses).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.