---
title: "Partner API — HMAC authentication — name.ai"
description: "Secure every Partner API request with HMAC-SHA256 signing — required headers, canonical string, and code samples."
url: https://name.ai/partners/docs/authentication
---

[Name.ai Partners](https://name.ai/partners)[Apply for access](https://name.ai/partners/apply)

# HMAC authentication

Secure every Partner API request by signing it **from your backend** with your API secret. Never expose the secret in frontend code, mobile apps, or browsers—store it server-side only (environment variables or a secrets vault).

The examples on this page use placeholders and env var _names_ only. Your real `sk_*` secret must never be committed to source control or pasted into support tickets.

### Overview

Each request sends headers proving you hold the secret without sending the secret over the wire. The server rebuilds the same string and compares HMAC-SHA256 signatures using timing-safe verification.

## Required headers

HeaderRequiredDescription

`X-NameAI-Key-Id`YesPublic credential key id (pk\_sandbox\_… or pk\_live\_…).

`X-NameAI-Timestamp`YesUnix timestamp in seconds UTC (not milliseconds).

`X-NameAI-Nonce`YesUnique value per request (UUID recommended). Reusing a nonce too soon is rejected.

`X-NameAI-Signature`YesHMAC-SHA256 over the canonical string, hex-encoded, prefixed with v1=.

`Idempotency-Key`POST /ordersRequired on POST /api/partner/v1/orders so retries are safe. Same key + same body fingerprint replays the stored response.

## Signature header format

Send the hex digest with the version prefix:

X-NameAI-Signature: v1=<hex\_lowercase>

Example:

X-NameAI-Signature: v1=9a4f1a2d7c8e0f1a2b3c4d5e6f708192a3b4c5d6e7f8091a2b3c4d5e6f708192

## Canonical string

Concatenate exactly six lines separated by newline (`\n`). Line order and spelling matter.

```
METHOD
PATH
QUERY_STRING
TIMESTAMP
NONCE
BODY_SHA256_HEX
```

-   **METHOD** — Uppercase HTTP verb (`GET`, `POST`, …).
-   **PATH** — Path only, starting with `/`, must match the request URL path (e.g. `/api/partner/v1/domains/feed`).
-   **QUERY\_STRING** — Canonical query string (see below). Use empty string if there is no query.
-   **TIMESTAMP** — Same integer string as in `X-NameAI-Timestamp`.
-   **NONCE** — Same string as `X-NameAI-Nonce`.
-   **BODY\_SHA256\_HEX** — Lowercase hex SHA-256 of the **raw** request body bytes. For no body or GET/HEAD, hash the empty string → `e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855`.

## Query string (canonical)

Split parameters, sort by key ascending; if keys tie, sort by value. URL-encode keys and values per Name.ai's encoder (aligned with `lib/server/partner-api/hmac.js`), then join with `&`.

Example: `?expand=items&limit=10` becomes canonical pairs sorted — `expand=items&limit=10` (order follows sort rules).

## Worked example (canonical)

Request: `POST /api/partner/v1/orders` with JSON body `{"domain":"…","expected_price":9900,…}` — hash the **exact** bytes sent on the wire (same spacing, same UTF-8).

Canonical string shape (illustrative; replace hashes with your computed values)

```
POST
/api/partner/v1/orders

1714309200
550e8400-e29b-41d4-a716-446655440000
<sha256_hex_of_exact_body>
```

## Generate signature

`signature_hex = HMAC_SHA256(api_secret, canonical_string)` → hex lowercase → header `v1=`\+ hex.

## Node.js (recommended)

Mirrors production helpers `buildCanonicalString` / `signCanonicalString`. Install: Node 18+ with native `fetch` / `crypto`.

sign-request.mjs

```
import crypto from "crypto";

const EMPTY_SHA256 = crypto.createHash("sha256").update("").digest("hex");

function canonicalizeQuery(searchParams) {
  const params =
    searchParams instanceof URLSearchParams
      ? Array.from(searchParams.entries())
      : Array.from(new URLSearchParams(searchParams || "").entries());
  return params
    .sort(([aKey, aValue], [bKey, bValue]) => {
      if (aKey === bKey) return aValue.localeCompare(bValue);
      return aKey.localeCompare(bKey);
    })
    .map(([key, value]) => {
      const enc = (s) =>
        encodeURIComponent(s).replace(/[!'()*]/g, (ch) =>
          "%" + ch.charCodeAt(0).toString(16).toUpperCase()
        );
      return enc(key) + "=" + enc(value);
    })
    .join("&");
}

function bodySha256(bodyString) {
  if (bodyString == null || bodyString === "") return EMPTY_SHA256;
  return crypto.createHash("sha256").update(bodyString, "utf8").digest("hex");
}

function buildCanonicalString({ method, pathname, searchParams, timestamp, nonce, bodyHash }) {
  return [
    String(method || "").toUpperCase(),
    pathname || "/",
    canonicalizeQuery(searchParams),
    String(timestamp || ""),
    String(nonce || ""),
    bodyHash || EMPTY_SHA256,
  ].join('\n');
}

export function signPartnerRequest({ secret, method, url, bodyString, timestamp, nonce }) {
  const u = new URL(url);
  const bodyHash = bodySha256(bodyString);
  const canonical = buildCanonicalString({
    method,
    pathname: u.pathname,
    searchParams: u.searchParams,
    timestamp,
    nonce,
    bodyHash,
  });
  const sig = crypto.createHmac("sha256", secret).update(canonical, "utf8").digest("hex");
  return { canonical, signatureHeader: "v1=" + sig };
}

// Example GET
const secret = process.env.NAMEAI_API_SECRET; // sk_sandbox_… or sk_live_…
const keyId = process.env.NAMEAI_KEY_ID; // pk_sandbox_…
const base = "https://your-domain.com/api/partner/v1/domains/feed?limit=10";
const ts = String(Math.floor(Date.now() / 1000));
const nonce = crypto.randomUUID();

const { signatureHeader } = signPartnerRequest({
  secret,
  method: "GET",
  url: base,
  bodyString: "",
  timestamp: ts,
  nonce,
});

const res = await fetch(base, {
  headers: {
    "X-NameAI-Key-Id": keyId,
    "X-NameAI-Timestamp": ts,
    "X-NameAI-Nonce": nonce,
    "X-NameAI-Signature": signatureHeader,
  },
});
console.log(await res.json());

// Example POST — create order (hash = exact body bytes you send; reuse same idempotency key only when retrying the same op)
const orderUrl = "https://your-domain.com/api/partner/v1/orders";
const idempotencyKey = "ord_" + crypto.randomUUID();
const bodyString = JSON.stringify({
  domain: "example.com",
  expected_price: 9900,
  currency: "USD",
  buyer: { email: "buyer@example.com", name: "Ada", country: "US" },
  return_url: "https://your-store.com/done",
  cancel_url: "https://your-store.com/cancel",
});
const tsPost = String(Math.floor(Date.now() / 1000));
const noncePost = crypto.randomUUID();

const postSigned = signPartnerRequest({
  secret,
  method: "POST",
  url: orderUrl,
  bodyString,
  timestamp: tsPost,
  nonce: noncePost,
});

const orderRes = await fetch(orderUrl, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-NameAI-Key-Id": keyId,
    "X-NameAI-Timestamp": tsPost,
    "X-NameAI-Nonce": noncePost,
    "X-NameAI-Signature": postSigned.signatureHeader,
    "Idempotency-Key": idempotencyKey,
  },
  body: bodyString,
});
console.log(await orderRes.json());
```

## Python 3

Same algorithm: build the canonical string exactly, then HMAC-SHA256 with the raw secret string as key (UTF-8).

sign\_request.py

```
import hashlib
import hmac
import json
import time
import uuid
from urllib.parse import urlparse, parse_qsl

EMPTY_SHA256 = hashlib.sha256(b"").hexdigest()

def encode_component(s: str) -> str:
    """Match JS encodeURIComponent + /[!'()*]/g — see partner-api/hmac.js encodeQueryComponent."""
    from urllib.parse import quote

    out = quote(s, safe="-_.!~*'()")
    for ch in "!'*()":
        out = out.replace(ch, f"%{ord(ch):02X}")
    return out

def canonicalize_query(query: str) -> str:
    if not query:
        return ""
    qs = query.lstrip("?")
    pairs = parse_qsl(qs, keep_blank_values=True)
    pairs.sort(key=lambda kv: (kv[0], kv[1]))
    return "&".join(f"{encode_component(k)}={encode_component(v)}" for k, v in pairs)

def body_sha256(body: bytes | str | None) -> str:
    if body is None or body == b"" or body == "":
        return EMPTY_SHA256
    if isinstance(body, str):
        body = body.encode("utf8")
    return hashlib.sha256(body).hexdigest()

def build_canonical(method: str, url: str, body_for_hash: bytes | str | None, timestamp: str, nonce: str) -> str:
    u = urlparse(url)
    path = u.path or "/"
    q = canonicalize_query(u.query)
    bh = body_sha256(body_for_hash)
    lines = [method.upper(), path, q, timestamp, nonce, bh]
    return "\n".join(lines)

def sign(secret: str, canonical: str) -> str:
    return hmac.new(secret.encode("utf8"), canonical.encode("utf8"), hashlib.sha256).hexdigest()

# Example GET
import os
import urllib.request

secret = os.environ["NAMEAI_API_SECRET"]
key_id = os.environ["NAMEAI_KEY_ID"]
full_url = "https://your-domain.com/api/partner/v1/domains/feed?limit=10"
ts = str(int(time.time()))
nonce = str(uuid.uuid4())
canon = build_canonical("GET", full_url, "", ts, nonce)
sig = f"v1={sign(secret, canon)}"

req = urllib.request.Request(
    full_url,
    headers={
        "X-NameAI-Key-Id": key_id,
        "X-NameAI-Timestamp": ts,
        "X-NameAI-Nonce": nonce,
        "X-NameAI-Signature": sig,
    },
    method="GET",
)
with urllib.request.urlopen(req) as resp:
    print(resp.read().decode())

# Example POST — create order (body bytes for signing must match request body exactly)
order_url = "https://your-domain.com/api/partner/v1/orders"
body_str = json.dumps(
  {
      "domain": "example.com",
      "expected_price": 9900,
      "currency": "USD",
      "buyer": {"email": "buyer@example.com", "name": "Ada", "country": "US"},
      "return_url": "https://your-store.com/done",
      "cancel_url": "https://your-store.com/cancel",
  },
  separators=(",", ":"),
)
idempotency_key = "ord_" + str(uuid.uuid4())
ts_post = str(int(time.time()))
nonce_post = str(uuid.uuid4())
canon_post = build_canonical("POST", order_url, body_str, ts_post, nonce_post)
sig_post = f"v1={sign(secret, canon_post)}"

post_req = urllib.request.Request(
    order_url,
    data=body_str.encode("utf8"),
    headers={
        "Content-Type": "application/json",
        "X-NameAI-Key-Id": key_id,
        "X-NameAI-Timestamp": ts_post,
        "X-NameAI-Nonce": nonce_post,
        "X-NameAI-Signature": sig_post,
        "Idempotency-Key": idempotency_key,
    },
    method="POST",
)
with urllib.request.urlopen(post_req) as resp:
    print(resp.read().decode())
```

## cURL (POST with JSON)

Compute `Signature` with your script first; substitute placeholders. Include `Idempotency-Key` for order creation.

```
curl -X POST "https://your-domain.com/api/partner/v1/orders" \
  -H "Content-Type: application/json" \
  -H "X-NameAI-Key-Id: pk_live_xxx" \
  -H "X-NameAI-Timestamp: 1714309200" \
  -H "X-NameAI-Nonce: 550e8400-e29b-41d4-a716-446655440000" \
  -H "X-NameAI-Signature: v1=<hex_from_canonical>" \
  -H "Idempotency-Key: ord_001" \
  -d '{"domain":"example.com","expected_price":9900,"currency":"USD","buyer":{"email":"buyer@example.com"},"return_url":"https://your-store.com/done","cancel_url":"https://your-store.com/cancel"}'
```

## Replay protection

-   **Clock skew:** timestamp must be within **±300 seconds** (5 minutes) of server time.
-   **Nonce reuse:** the same nonce cannot be replayed while it is still tracked; generate a fresh nonce every request.

## Common errors

`invalid_signature`Wrong secret, wrong canonical string, or body changed after signing.

`invalid_timestamp`Not Unix seconds, clock drift beyond 5 minutes, or malformed.

`replay_detected`Nonce reused too soon.

`missing_signature_headers`Missing one of the four X-NameAI-\* auth headers.

`missing_idempotency_key`POST /orders without Idempotency-Key.

## Common mistakes

-   Using **milliseconds** instead of Unix **seconds**.
-   Signing with the **key id** (`pk_…`) instead of the **secret** (`sk_…`).
-   Omitting the **v1=** prefix on the signature header.
-   Path mismatch: signing `/orders` but requesting `/api/partner/v1/orders`.
-   Body mismatch: pretty-printing JSON _after_ computing SHA-256, or changing whitespace vs what you send.
-   Wrong query canonicalization (parameter order must match Name.ai sorting rules).

## Security checklist

-   HTTPS only in production.
-   Store `sk_*` in vault / env; rotate if leaked.
-   Never log full secrets or embed them in client-side code.
-   Implement retries with a **new nonce** each attempt (keep the same idempotency key only when retrying the same write).

Related: [API surface](https://name.ai/partners/docs/api), [Sandbox & live](https://name.ai/partners/docs/environments).

Partner API — HMAC authentication — name.ai

---

Markdown representation of https://name.ai/partners/docs/authentication. The same URL returns HTML to any client that asks for `text/html`. Site map for agents: https://name.ai/llms.txt
