---
title: "Authentication & Signing · COUNT Partner API"
description: "Two layers of auth: an HMAC signature proving the request came from your app, and a workspace access token scoping it to one customer's data."
canonical: "https://developers.getcount.com/getting-started/authentication"
source: "https://developers.getcount.com/getting-started/authentication"
---
Getting Started

# Authentication & signing

The Partner API uses two layers of authentication: an HMAC signature that proves the request came from your app, and a workspace access token that scopes the request to one customer's data.

Both layers are required on data endpoints

Data endpoints need **both** a valid HMAC signature (your app) and a Bearer access token (the workspace). Only the token-exchange routes are signed without a Bearer token.

## Layer 1 — HMAC request signing

Every request carries three headers derived from your `clientSecret`. The signature is an HMAC-SHA256 of a base string built from the request:

Base string format

```bash
METHOD:path:timestamp:bodyHash

# Example for: POST /partners/customers
POST:/customers:1717689600:9b74c9897bac770ffc029102a200c5de
```

The four parts, joined by colons, are:

- `METHOD` — the uppercase HTTP method.
- `path` — the path **relative to the `/partners` mount**. For `/partners/customers` you sign `/customers`.
- `timestamp` — Unix time in seconds; send the same value in `x-timestamp`. Must be within ±300 seconds of server time (five-minute clock skew window).
- `bodyHash` — SHA-256 hex of the exact JSON body for POST/PUT/PATCH, or an empty string otherwise.

Sign the path without /partners

The most common signing mistake is including the `/partners` prefix in the base string. The server signs `req.path`, which is relative to the mount.

signRequest (Node.js)

```javascript
import crypto from 'node:crypto';

function signRequest({ method, path, timestamp, body, clientSecret }) {
  // path is relative to the /partners mount, e.g. "/customers"
  const bodyHash = ['POST', 'PUT', 'PATCH'].includes(method)
    ? crypto.createHash('sha256').update(body ?? '').digest('hex')
    : '';

  const baseString = `${method}:${path}:${timestamp}:${bodyHash}`;

  return crypto.createHmac('sha256', clientSecret).update(baseString).digest('hex');
}
```

Attach the result as headers:

Request headers

```bash
x-client-id: <your clientId>
x-timestamp: <unix seconds, same value used in the signature>
x-signature: <hex HMAC-SHA256 of the base string>
Authorization: Bearer <workspace access token>   # data endpoints only
Content-Type: application/json                     # when sending a body
```

## Layer 2 — Workspace access tokens (OAuth)

To act on a workspace you first send the user through the OAuth consent flow, then exchange the returned code for an access token. The token exchange is signed with HMAC but does not need a Bearer token.

Use the arrows or dots below to step through each OAuth phase. Details and code samples update as you browse.

Your system Step 1 of 7

### Create OAuth credentials

Register your integration in COUNT Partners and store clientId and clientSecret on your server.

your-app.com/oauth/callback?code=…

Browser returns to your app with a one-time authorization code

Server to server

POST /partners/grant-access-token

{ accessToken, refreshToken }

GET /partners/customers

x-client-id · x-timestamp

x-signature · Bearer token

Signed Partner API request

- Create an OAuth app in the COUNT Partners dashboard.
- Keep clientSecret server-side only; never ship it to browsers or mobile apps.

[Authentication overview](https://developers.getcount.com/authentication)

See [OAuth consent experience](https://developers.getcount.com/getting-started/oauth-consent) for what your users see on the consent screen and how to handle the redirect.

Exchange the authorization code

```json
POST https://api.getcount.com/partners/grant-access-token

{
  "code": "<authorization code from the redirect>",
  "grantType": "authorization_code"
}
```

The response contains the tokens and the workspace they are scoped to:

Token response

```json
{
  "accessToken": "eyJhbGciOi...",
  "refreshToken": "eyJhbGciOi...",
  "accessTokenExpiresAt": "2026-06-06T12:00:00.000Z",
  "refreshTokenExpiresAt": "2026-07-06T12:00:00.000Z",
  "workspaceId": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
  "workspaceName": "Acme Books"
}
```

Access tokens expire. When they do, call `POST /partners/refresh-user-access-token` with your refresh token to get a new pair.

## Putting it together

Sign the base string, set the three HMAC headers plus the Bearer token, and send the request. Every code sample in this reference does exactly this — the [starter templates](https://developers.getcount.com/sdks) wrap it into a single helper so you never compute a signature by hand.
