OAuth 2.0 client credentials

Your server requests an access token from the crmSeries authorization server with the client_credentials grant, your client ID and secret, and the api scope.

curl --request POST 'https://auth.crmseries.com/connect/token' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'grant_type=client_credentials' \
  --data-urlencode 'client_id=YOUR_CLIENT_ID' \
  --data-urlencode 'client_secret=YOUR_CLIENT_SECRET' \
  --data-urlencode 'scope=api'

The client ID is a GUID issued by crmSeries for your dealership. A successful response looks like this:

{
  "access_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Send the access token in the Authorization header of each API request:

curl 'https://apiv2.crmseries.com/api/v2/YOUR_OPERATION_PATH' \
  --header 'Authorization: Bearer YOUR_ACCESS_TOKEN'

Cache and renew the token

expires_in is the number of seconds the token stays valid. crmSeries allows no grace period after expiry, so a token used one second late is rejected with 401. Reuse one token across requests and request a new one about a minute before it expires. The client_credentials grant has no refresh token; a new token request is how you renew access.

// Node.js 18+: one cached token for the whole process
let cached = null;

export async function getAccessToken() {
  if (cached && Date.now() < cached.expiresAt - 60_000) return cached.token;
  const response = await fetch('https://auth.crmseries.com/connect/token', {
    method: 'POST',
    headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
    body: new URLSearchParams({
      grant_type: 'client_credentials',
      client_id: process.env.CRMSERIES_CLIENT_ID,
      client_secret: process.env.CRMSERIES_CLIENT_SECRET,
      scope: 'api',
    }),
  });
  if (!response.ok) throw new Error(`Token request failed: ${response.status}`);
  const body = await response.json();
  cached = { token: body.access_token, expiresAt: Date.now() + body.expires_in * 1000 };
  return cached.token;
}

API key

If your integration uses an API key, send it in the Authorization header using the ApiKey scheme. If your integration acts on behalf of individual dealer users, you can add an optional X-User-Email header so requests are attributed to that user.

curl --request POST 'https://apiv2.crmseries.com/api/v2/YOUR_OPERATION_PATH' \
  --header 'Authorization: ApiKey YOUR_API_KEY' \
  --header 'X-User-Email: user@yourdealer.com' \
  --header 'Content-Type: application/json' \
  --data '{ ... }'

The ApiKey prefix isn't case-sensitive. When you send X-User-Email, the address must belong to an active user in your dealership's crmSeries account; otherwise the request is rejected with 401. Leave the header out when a request isn't made on behalf of a specific person.

API key support varies by operation

crmSeries checks either your bearer token or your API key for a given request, never both. Not every operation accepts an API key, and some reporting operations accept only a bearer token obtained with client credentials. Check the Authentication section of each operation's reference page for the methods it supports.

Protect your credentials

Store secrets in a server-side secret manager, rotate them if exposed, and never place them in frontend code. Dealer client credentials and API keys are provisioned individually for your integration, not through self-service — contact your crmSeries integration contact to request new or rotated credentials.