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

# Authentication

> Authenticate PrimeVault API requests with an API-user key and a short-lived ES256 bearer token bound to the exact request path and JSON body.

PrimeVault authenticates every external API request with an API-user key and a short-lived ES256 bearer token. Authentication is organization-scoped: credentials may access only the organization and permissions assigned to that API user.

## Base URL

```text theme={null}
https://api.primevault.com
```

All endpoint paths are relative to this URL.

## Recommended: use the JavaScript SDK

The SDK signs the exact request URL and JSON body, adds the required headers, and maps HTTP failures to typed errors.

```typescript theme={null}
import { APIClient } from "@primevault/js-api-sdk";

const apiClient = new APIClient(
  process.env.PRIMEVAULT_API_KEY!,
  "https://api.primevault.com",
  process.env.PRIMEVAULT_ACCESS_PRIVATE_KEY!,
);
```

<Warning>
  Create the client only in a trusted server environment. Never embed the API key or access private key in browser, mobile, or other distributed client code.
</Warning>

## Generate a bearer token manually

Generate tokens manually only when you can't use the official SDK to send the request. The token is request-bound: generate it from the relative URL path and the same body object that will be sent, then create a new token for the next request.

The examples below accept either a PEM private key or a hex-encoded PKCS#8 DER private key. They intentionally use the DER-encoded ECDSA signature produced by the PrimeVault SDKs. Do not replace the signing step with a generic JWT helper unless it can emit that signature format.

<CodeGroup>
  ```javascript JavaScript (Node.js) expandable theme={null}
  import {
    createHash,
    createPrivateKey,
    createSign,
    randomUUID,
  } from "node:crypto";

  const sortKeys = (value) => {
    if (Array.isArray(value)) return value.map(sortKeys);
    if (value && typeof value === "object") {
      return Object.fromEntries(
        Object.keys(value)
          .sort()
          .map((key) => [key, sortKeys(value[key])]),
      );
    }
    return value;
  };

  const base64url = (value) => Buffer.from(value).toString("base64url");

  function generatePrimeVaultToken(apiKey, privateKey, urlPath, body = {}) {
    const now = Math.floor(Date.now() / 1000);
    const bodyHash = createHash("sha256")
      .update(JSON.stringify(sortKeys(body)))
      .digest("hex");

    const header = { alg: "ES256", typ: "JWT" };
    const payload = {
      iat: now,
      exp: now + 120,
      urlPath,
      userId: apiKey,
      body: bodyHash,
      jti: randomUUID(),
    };

    const signingInput = [header, payload]
      .map((part) => base64url(JSON.stringify(sortKeys(part))))
      .join(".");

    const trimmedKey = privateKey.trim();
    const key = trimmedKey.startsWith("-----BEGIN")
      ? createPrivateKey(trimmedKey)
      : createPrivateKey({
          key: Buffer.from(trimmedKey, "hex"),
          format: "der",
          type: "pkcs8",
        });

    const signer = createSign("SHA256");
    signer.update(signingInput);
    signer.end();

    return `${signingInput}.${base64url(signer.sign(key))}`;
  }

  const apiKey = process.env.PRIMEVAULT_API_KEY;
  const privateKey = process.env.PRIMEVAULT_ACCESS_PRIVATE_KEY;
  const urlPath = "/api/external/vaults/";
  const token = generatePrimeVaultToken(apiKey, privateKey, urlPath);

  const headers = {
    Authorization: `Bearer ${token}`,
    "Api-Key": apiKey,
    Accept: "application/json",
  };
  ```

  ```python Python expandable theme={null}
  import base64
  import hashlib
  import json
  import os
  import time
  import uuid
  from typing import Optional

  from cryptography.hazmat.primitives import hashes, serialization
  from cryptography.hazmat.primitives.asymmetric import ec


  def base64url(value: bytes) -> str:
      return base64.urlsafe_b64encode(value).decode("ascii")


  def generate_primevault_token(
      api_key: str,
      private_key: str,
      url_path: str,
      body: Optional[dict] = None,
  ) -> str:
      now = int(time.time())
      canonical_body = json.dumps(
          body or {},
          sort_keys=True,
          separators=(",", ":"),
          ensure_ascii=False,
      )

      header = {"alg": "ES256", "typ": "JWT"}
      payload = {
          "iat": now,
          "exp": now + 120,
          "urlPath": url_path,
          "userId": api_key,
          "body": hashlib.sha256(canonical_body.encode()).hexdigest(),
          "jti": str(uuid.uuid4()),
      }

      signing_input = ".".join(
          base64url(
              json.dumps(
                  part,
                  sort_keys=True,
                  separators=(",", ":"),
                  ensure_ascii=False,
              ).encode()
          )
          for part in (header, payload)
      )

      trimmed_key = private_key.strip()
      if trimmed_key.startswith("-----BEGIN"):
          key = serialization.load_pem_private_key(
              trimmed_key.encode(),
              password=None,
          )
      else:
          key = serialization.load_der_private_key(
              bytes.fromhex(trimmed_key),
              password=None,
          )

      signature = key.sign(
          signing_input.encode(),
          ec.ECDSA(hashes.SHA256()),
      )
      return f"{signing_input}.{base64url(signature)}"


  api_key = os.environ["PRIMEVAULT_API_KEY"]
  private_key = os.environ["PRIMEVAULT_ACCESS_PRIVATE_KEY"]
  url_path = "/api/external/vaults/"
  token = generate_primevault_token(api_key, private_key, url_path)

  headers = {
      "Authorization": f"Bearer {token}",
      "Api-Key": api_key,
      "Accept": "application/json",
  }
  ```
</CodeGroup>

For POST and PUT requests, construct the body once, pass that object to the token helper, and send the same object as JSON without mutating its keys or values between signing and transmission.

## Required headers

| Header | Value | Purpose |
| - | - | - |
| `Authorization` | `Bearer <signed JWT>` | Short-lived ES256 token bound to the exact request path and, for writes, the exact JSON body. |
| `Api-Key` | API-user key | Identifies the API user and organization. |
| `version` | Installed SDK version | Identifies the client contract. Added by the SDK. |
| `Content-Type` | `application/json` | Required when a JSON request body is sent. |
| `Accept` | `application/json` | Requests a JSON response. Added by the SDK. |

Header names are case-insensitive at the HTTP layer, but use the spelling above in examples and diagnostics.

## How request signing works

<Steps>
  <Step>
    Build the relative request URL, excluding the scheme and host. The query string may be included; the verifier compares the path component.
  </Step>

  <Step>
    Canonically serialize the request body with recursively sorted keys and compact JSON separators. Use an empty object for a request without a body.
  </Step>

  <Step>
    Hash that canonical body with SHA-256 and create a short-lived payload containing `iat`, `exp`, `urlPath`, `userId`, `body`, and a unique `jti`.
  </Step>

  <Step>
    Base64url-encode the header and payload, then sign `header.payload` with P-256 and SHA-256.
  </Step>

  <Step>
    Send the resulting bearer token with the same API-user key in the `Api-Key` header.
  </Step>
</Steps>

<Note>
  Generate a new token for every request. Changing the path, body keys, or body values after token generation invalidates the request binding.
</Note>

## Credential and key safety

* Store credentials in a secret manager or protected server environment variables.
* Do not log bearer tokens, API keys, private keys, or complete bank-account details.
* Rotate credentials immediately if they may have been exposed.
* Keep server time synchronized. Tokens expire 120 seconds after they are issued, so clock skew causes 401 errors.
* Restrict source IPs where appropriate; see [API user IP whitelisting](/ip-whitelisting).
* Use the least-privileged API user required by the integration.

## Authentication failures

| HTTP status | Meaning | What to check |
| - | - | - |
| `401` | Authentication failed | Missing or expired token, wrong API key, mismatched path/body signature, or clock skew. |
| `403` | Authenticated but not permitted | API-user role, policy, vault permission, organization scope, sub-org scope, or IP allowlist. |

<Warning>
  Do not treat a 403 as a token-refresh problem. Verify authorization and resource scope before retrying.
</Warning>

## Setup references

Use [Setting up API User](/getting-started/setting-up-api-user) to provision credentials and [API user IP whitelisting](/ip-whitelisting) to restrict allowed source addresses.


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