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

# Authentication and connecting a customer

> The two server-side secrets, how a customer connects their Vort account to your product, and how recruiters are identified.

Every call to the Vort Partner API is made **from your server**, with two secrets that never leave it. RFC 2119 keywords apply.

## Two secrets

| Secret | Format | Identifies | Header | Sent on |
| - | - | - | - | - |
| Partner API key | `vpk_live_` + 48 hex | You, the vendor | `x-partner-key` | every request |
| Org token | `vot_` + 48 hex | One customer organization | `x-vort-org` | every request except `/activations/redeem` |

<Warning>
  Every call is **server-to-server**. A Vort key or org token MUST NOT reach a browser, a mobile app, a log line visible to customers, a URL, or a frontend bundle. Certification checks for the strings `vpk_` and `vot_` in your frontend bundle and browser network traffic (item [C-02](/certification#checklist)).
</Warning>

* Vort stores only SHA-256 hashes of both secrets. A lost org token cannot be recovered: ask Vort for a new activation code and redeem it.
* The acting recruiter is identified by `acting_user_external_id` in the request body. It is a field, not a credential. Your server is responsible for making sure the logged-in user in your product is who you say they are.
* **Key rotation:** Vort can rotate your partner key on request. The old key stops working the moment the new one is issued (there is no overlap window), so plan the rotation with Vort and deploy the new key immediately.
* **Webhook secret:** Vort sends you a webhook signing secret out of band. It is distinct from your API key (see [Webhooks](/webhooks)).

## Request headers

<ParamField header="x-partner-key" type="string" required>
  Your partner API key, `vpk_live_` + 48 lowercase hex. Sent on every request.
</ParamField>

<ParamField header="x-vort-org" type="string">
  The customer's org token, `vot_` + 48 lowercase hex, obtained by redeeming an activation code. Required on every request except `POST /activations/redeem`.
</ParamField>

<ParamField header="x-vort-lang" type="string">
  `he` or `en`. Language of notices and messages for this request. Default: your partner default (`he` unless agreed otherwise).
</ParamField>

<ParamField header="Content-Type" type="string">
  `application/json` on every `POST`.
</ParamField>

A request with a missing or unknown key gets `401 invalid_partner_key`; a missing or unknown org token gets `401 invalid_org_token`. See [Errors](/errors).

## Connecting a customer

A customer organization is connected once, by one of its admins, with a one-time activation code that Vort issues to that customer.

```text theme={null}
 Customer admin                 Your frontend              Your server                      Vort
 receives VORT-XXXX-XXXX  ───▶  "Connect Vort" form  ───▶  POST /activations/redeem  ───▶   200 {org_token, organization_name}
                                shows organization_name ◀─ stores org_token (encrypted, server side)
```

<Steps>
  <Step title="The admin pastes the code">
    The admin pastes the code into your **Connect Vort** screen (see [Screen 1 in the UI specification](/ui-specification#screen-1-connect-vort)). The form posts to **your** server; the browser never talks to Vort.
  </Step>

  <Step title="Your server redeems it">
    Your server calls `POST /activations/redeem` with the code and **your** id for this customer (`external_id`, unique per partner). Only `x-partner-key` is sent.
  </Step>

  <Step title="Store the org token">
    Vort returns the org token **once**. Store it server-side, encrypted at rest, keyed by your `external_id`. Vort cannot show it again.
  </Step>

  <Step title="Show the connected state">
    Your screen shows the connected state with `organization_name`, never the token.
  </Step>
</Steps>

The code format is `VORT-XXXX-XXXX` using the alphabet `ABCDEFGHJKMNPQRSTUVWXYZ23456789` (no `0`, `O`, `1`, `I`, `L`). Codes are valid 14 days by default. Vort normalizes case, spaces and dashes, so `vort xxxx xxxx` is accepted. Send what the user typed; you MAY trim it.

<CodeGroup>
  ```bash curl theme={null}
  curl -sS -X POST "$VORT_BASE/activations/redeem" \
    -H "x-partner-key: $VORT_PARTNER_KEY" \
    -H "Content-Type: application/json" \
    -d '{"code":"VORT-7KQM-3XHP","external_id":"hrmony-tenant-5521"}'
  ```

  ```javascript Node.js theme={null}
  const res = await fetch(`${process.env.VORT_BASE}/activations/redeem`, {
    method: "POST",
    headers: {
      "x-partner-key": process.env.VORT_PARTNER_KEY,
      "Content-Type": "application/json",
    },
    body: JSON.stringify({ code: codeTypedByAdmin.trim(), external_id: "hrmony-tenant-5521" }),
  });
  const data = await res.json();
  // 200: store data.org_token encrypted, server side. Return only data.organization_name to the browser.
  ```

  ```python Python theme={null}
  import os
  import requests

  res = requests.post(
      f"{os.environ['VORT_BASE']}/activations/redeem",
      headers={"x-partner-key": os.environ["VORT_PARTNER_KEY"]},
      json={"code": code_typed_by_admin.strip(), "external_id": "hrmony-tenant-5521"},
      timeout=20,
  )
  data = res.json()
  # 200: store data["org_token"] encrypted, server side. Return only data["organization_name"] to the browser.
  ```

  ```php PHP (Laravel) theme={null}
  use Illuminate\Support\Facades\Http;

  $res = Http::withHeaders(['x-partner-key' => config('services.vort.partner_key')])
      ->acceptJson()
      ->timeout(20)
      ->post(config('services.vort.base') . '/activations/redeem', [
          'code' => trim($request->input('code')),
          'external_id' => 'hrmony-tenant-5521',
      ]);
  // 200: store $res->json('org_token') encrypted, server side. Return only organization_name to the browser.
  ```
</CodeGroup>

```json Response theme={null}
{
  "organization_id": "0b8f4a52-2d1e-4c7b-9a61-5f0e3c2d9b17",
  "org_token": "vot_9c1e...<48 hex total>",
  "organization_name": "Acme Staffing Ltd"
}
```

### Redeem errors

| HTTP | `error` | Show the admin |
| - | - | - |
| 404 | `code_not_found` | The API `message`, under the input. |
| 409 | `code_already_redeemed` (+ `redeemed_at`) | The API `message`, under the input. |
| 410 | `code_expired` | The API `message`, under the input. |
| 409 | `external_id_taken` | That this workspace is already connected, and to contact Vort. |

<Warning>
  `POST /activations/redeem` is **not safe to retry blindly**: the code is single-use and the token is not returned a second time. If the first response was lost (for example, a timeout), ask Vort for a new activation code.
</Warning>

### Reconnecting

On `401 invalid_org_token` from any later call, show the Connect Vort screen again in the disconnected state, with a note that the connection must be renewed. The admin redeems a new code from Vort. On `403 organization_disabled`, tell the admin their Vort connection is paused and to contact Vort.

## Registering recruiters

Before a recruiter starts a run, register them with `POST /users`, using your own id for them:

```bash curl theme={null}
curl -sS -X POST "$VORT_BASE/users" \
  -H "x-partner-key: $VORT_PARTNER_KEY" \
  -H "x-vort-org: $VORT_ORG" \
  -H "Content-Type: application/json" \
  -d '{"external_id":"u-88121","email":"dana@acme-staffing.example","full_name":"Dana Levi","role":"recruiter"}'
```

* The recruiter is registered as an **API-only identity** of this organization. The `email` is stored as a contact and display e-mail only: it is **not a login**, no Vort account is created or linked under it, and nothing is e-mailed to the recruiter. Recruiters registered this way cannot sign in to Vort directly.
* Later calls name the recruiter with `acting_user_external_id`. An unregistered id is rejected with `404 user_not_found`.
* `POST /users` is an upsert on `external_id` and is safe to retry.

Full request and response schema: [Register a recruiter](/api-reference/users/register).

## Server checklist

<Check>
  * Partner key in your secrets manager or environment, never in source control.
  * Org tokens encrypted at rest, one per connected customer, readable only by the service that calls Vort.
  * No key or token in browser responses, browser storage, URLs, logs visible to customers, or error messages.
  * Base URL in configuration, one value per environment (sandbox and production).
  * When you contact Vort, identify the key by its first 12 characters only (for example `vpk_live_3f9`).
</Check>
