Skip to main content

Make your first Public API call

By the end you will have an access token and a list of the tenant's departments. It takes about 15 minutes once HCM Cafe has registered your client.

Before you start​

  • A server-side environment. The API is for server-to-server calls, so never put the private key in a browser or mobile app.
  • OpenSSL, or one of the languages below, to create the key pair.
  • Your client ID, which HCM Cafe sends you after registering your public key.

1. Create a key pair​

Create a P-256 (ES256) key pair:

openssl ecparam -name prime256v1 -genkey -noout -out partner.key
openssl ec -in partner.key -pubout -out partner.pub

partner.key is your private key. Store it as you would a password, for example in a secrets manager, and never send it to anyone, including HCM Cafe.

partner.pub is your public key. It looks like this:

-----BEGIN PUBLIC KEY-----
MFkwEwYHKoZIzj0CAQYIKoZIzj0DAQcDQgAE...
-----END PUBLIC KEY-----

RSA keys of 2048 bits or more, signing with RS256, also work.

2. Send the public key​

Send partner.pub to your HCM Cafe contact, together with:

  • the tenant you are integrating for,
  • the scopes you need (see scopes),
  • optionally, the IP ranges your servers call from, if you want token requests from anywhere else refused.

HCM Cafe replies with your client ID and the scopes the tenant approved.

3. Get an access token​

To get a token, you sign a short-lived JWT, called a client assertion, with your private key, and post it to the token URL.

ClaimValue
iss and subYour client ID
audhttps://api.hcmcafe.com/oauth/token, exactly
iat and nbfNow, in seconds since 1970
expAt most 5 minutes from now. 2 minutes is plenty
jtiA new unique value for every request, up to 128 characters. A UUID works

Post it as a form:

FieldValue
grant_typeclient_credentials
client_assertion_typeurn:ietf:params:oauth:client-assertion-type:jwt-bearer
client_assertionThe signed assertion
scopeOptional. Space-separated scopes. Leave it out to get every scope your client holds

The response holds the token:

{
"access_token": "eyJhbGciOiJFUzI1NiIs...",
"token_type": "Bearer",
"expires_in": 600,
"scope": "organisation:read"
}

Use the samples below. Each one gets a token and lists departments, which needs the organisation:read scope. Replace your-client-id with your client ID.

curl (bash)​

Needs openssl, xxd and curl.

CLIENT_ID=your-client-id
TOKEN_URL=https://api.hcmcafe.com/oauth/token
KEY=partner.key

b64url() { openssl base64 -A | tr '+/' '-_' | tr -d '='; }
now=$(date +%s)
header=$(printf '{"alg":"ES256","typ":"JWT"}' | b64url)
payload=$(printf '{"iss":"%s","sub":"%s","aud":"%s","iat":%d,"nbf":%d,"exp":%d,"jti":"%s"}' \
"$CLIENT_ID" "$CLIENT_ID" "$TOKEN_URL" "$now" "$now" "$((now + 120))" "$(openssl rand -hex 16)" | b64url)
der=$(mktemp)
printf '%s.%s' "$header" "$payload" | openssl dgst -sha256 -sign "$KEY" -out "$der"
signature=$(openssl asn1parse -inform DER -in "$der" | awk -F: '/INTEGER/ { printf "%064s", $NF }' | tr ' ' '0' | xxd -r -p | b64url)
rm -f "$der"

TOKEN=$(curl -sS "$TOKEN_URL" \
--data-urlencode "grant_type=client_credentials" \
--data-urlencode "client_assertion_type=urn:ietf:params:oauth:client-assertion-type:jwt-bearer" \
--data-urlencode "client_assertion=$header.$payload.$signature" \
--data-urlencode "scope=organisation:read" | sed -n 's/.*"access_token":"\([^"]*\)".*/\1/p')

curl -sS https://api.hcmcafe.com/v1/departments -H "Authorization: Bearer $TOKEN"

OpenSSL writes ECDSA signatures in DER format, and a JWT needs the two raw 32-byte values joined, which is what the asn1parse line does.

Node.js (18 or later)​

No packages needed.

import { createPrivateKey, randomUUID, sign } from 'node:crypto';
import { readFileSync } from 'node:fs';

const CLIENT_ID = 'your-client-id';
const TOKEN_URL = 'https://api.hcmcafe.com/oauth/token';
const ASSERTION_SECONDS = 120;

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

function createAssertion(privateKey) {
const now = Math.floor(Date.now() / 1000);
const header = base64url(JSON.stringify({ alg: 'ES256', typ: 'JWT' }));
const payload = base64url(JSON.stringify({
iss: CLIENT_ID, sub: CLIENT_ID, aud: TOKEN_URL,
iat: now, nbf: now, exp: now + ASSERTION_SECONDS, jti: randomUUID()
}));
const signature = sign('sha256', Buffer.from(`${header}.${payload}`), { key: privateKey, dsaEncoding: 'ieee-p1363' });
return `${header}.${payload}.${signature.toString('base64url')}`;
}

const privateKey = createPrivateKey(readFileSync('partner.key'));
const tokenResponse = await fetch(TOKEN_URL, {
method: 'POST',
body: new URLSearchParams({
grant_type: 'client_credentials',
client_assertion_type: 'urn:ietf:params:oauth:client-assertion-type:jwt-bearer',
client_assertion: createAssertion(privateKey),
scope: 'organisation:read'
})
});
if (!tokenResponse.ok) throw new Error(`Token request failed: ${tokenResponse.status} ${await tokenResponse.text()}`);
const { access_token: accessToken } = await tokenResponse.json();

const departments = await fetch('https://api.hcmcafe.com/v1/departments', {
headers: { Authorization: `Bearer ${accessToken}` }
});
console.log(departments.status, await departments.json());

Save it as first-call.mjs and run node first-call.mjs. dsaEncoding: 'ieee-p1363' gives the signature format JWTs use.

Python​

Needs pip install "pyjwt[crypto]" requests.

import time
import uuid

import jwt
import requests

CLIENT_ID = "your-client-id"
TOKEN_URL = "https://api.hcmcafe.com/oauth/token"
ASSERTION_SECONDS = 120

with open("partner.key", "rb") as key_file:
private_key = key_file.read()

now = int(time.time())
assertion = jwt.encode(
{"iss": CLIENT_ID, "sub": CLIENT_ID, "aud": TOKEN_URL,
"iat": now, "nbf": now, "exp": now + ASSERTION_SECONDS, "jti": str(uuid.uuid4())},
private_key,
algorithm="ES256",
)

token_response = requests.post(TOKEN_URL, data={
"grant_type": "client_credentials",
"client_assertion_type": "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
"client_assertion": assertion,
"scope": "organisation:read",
}, timeout=30)
token_response.raise_for_status()
access_token = token_response.json()["access_token"]

departments = requests.get("https://api.hcmcafe.com/v1/departments",
headers={"Authorization": f"Bearer {access_token}"}, timeout=30)
print(departments.status_code, departments.json())

C# (.NET 8 or later)​

Needs the Microsoft.IdentityModel.JsonWebTokens package.

using System.Net.Http.Headers;
using System.Net.Http.Json;
using System.Security.Cryptography;
using Microsoft.IdentityModel.JsonWebTokens;
using Microsoft.IdentityModel.Tokens;

const string ClientId = "your-client-id";
const string TokenUrl = "https://api.hcmcafe.com/oauth/token";

using var ecdsa = ECDsa.Create();
ecdsa.ImportFromPem(File.ReadAllText("partner.key"));

var now = DateTime.UtcNow;
var assertion = new JsonWebTokenHandler().CreateToken(new SecurityTokenDescriptor
{
Issuer = ClientId,
Audience = TokenUrl,
IssuedAt = now,
NotBefore = now,
Expires = now.AddMinutes(2),
Claims = new Dictionary<string, object> { ["sub"] = ClientId, ["jti"] = Guid.NewGuid().ToString() },
SigningCredentials = new SigningCredentials(new ECDsaSecurityKey(ecdsa), SecurityAlgorithms.EcdsaSha256)
});

using var http = new HttpClient();
var tokenResponse = await http.PostAsync(TokenUrl, new FormUrlEncodedContent(new Dictionary<string, string>
{
["grant_type"] = "client_credentials",
["client_assertion_type"] = "urn:ietf:params:oauth:client-assertion-type:jwt-bearer",
["client_assertion"] = assertion,
["scope"] = "organisation:read"
}));
tokenResponse.EnsureSuccessStatusCode();
var token = await tokenResponse.Content.ReadFromJsonAsync<TokenResponse>();

http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", token!.AccessToken);
Console.WriteLine(await http.GetStringAsync("https://api.hcmcafe.com/v1/departments"));

record TokenResponse(
[property: System.Text.Json.Serialization.JsonPropertyName("access_token")] string AccessToken,
[property: System.Text.Json.Serialization.JsonPropertyName("expires_in")] int ExpiresIn);

4. Check the result​

A working call returns 200 and the tenant's departments:

{
"items": [
{ "id": "b3874593-7ceb-40e3-9f17-25516cf7dd7e", "code": null, "name": "Sales", "active": true }
],
"nextCursor": null
}

If the token request returns 401 with invalid_client, or the call returns an error, see Troubleshooting.

Next steps​

  • Reuse a token until it is close to expiry, rather than getting one per call. See Authentication.
  • Read Conventions before you build a sync: paging, rate limits and safe retries.
  • Find every route and field in the API reference.