Skip to content

Direct API

Call the JustAI data APIs from your own servers to keep customer profiles and customer events up to date without a CDP. Your backend calls the Identify API when a customer’s details change and the Track API when a customer does something. This guide covers authentication, request rules, safe retries, and monitoring, with an example client in Node.js and Python.

  • API access for your organization. Your JustAI team turns it on. After that, any member of your organization can create API keys.
  • An API key for the environment you send to. Select that environment, open the account menu at the bottom of the left sidebar, select Settings > API keys, and select New key. Copy the key from the API key created dialog, because JustAI shows it only once. See Credentials.
  • Your organization slug. Your JustAI team gives it to you at kickoff.
  • For an environment other than Production, the environment’s path value and environment key. Your JustAI team gives you both.
  • A server to send from. Keep the key on your servers. See Keep API keys on your servers.

All three APIs use the base URL https://ingest.justwords.ai.

API Request Success response
Identify API POST /api/identify/{org_slug} 200
Track API POST /api/track/{org_slug} 202
Profiles API GET /api/profiles/{org_slug}/{user_id} 200

Paths match exactly. A trailing slash or a different method returns 404. URL-encode the user ID in a Profiles API path.

Send your API key in the X-Api-Key header. If your HTTP client can set only an Authorization header, send Authorization: Bearer YOUR_API_KEY instead. Send one of the two headers, not both.

POST /api/track/{org_slug} HTTP/1.1
Host: ingest.justwords.ai
X-Api-Key: YOUR_API_KEY
Content-Type: application/json

Send Content-Type: application/json with every request body.

An API key has no per-API scopes. One key works with every supported JustAI API for its environment, so treat each key as a full-access secret.

Each API key belongs to the environment that was selected when the key was created.

  • Production. Put your organization slug in the path. Leave out the X-JustAI-Environment header, or send X-JustAI-Environment: prod.
  • Another environment. Put the environment’s path value in place of the organization slug, and send the environment key in X-JustAI-Environment. Use a key created in that environment.

A key used with a different environment or organization returns 403. A non-production key can never write to Production. An environment header with an empty value returns 400, so leave the header out for Production.

Each request carries one profile update or one event. The APIs have no batch endpoint. A JSON array or a {"batch": [...]} body returns 400. To send many updates, send many requests, as described in Control concurrency and retries.

An Identify request sets traits on one profile:

Identify request body
{
"userId": "user-1234",
"messageId": "6f3b8c1e-2a4d-4f7e-9c51-0d8e2b7a9f13",
"timestamp": "2026-10-12T09:30:00Z",
"traits": {
"email": "ada@example.com",
"first_name": "Ada",
"plan": "pro"
}
}
Response (200)
{ "success": true, "messageId": "6f3b8c1e-2a4d-4f7e-9c51-0d8e2b7a9f13", "duplicate": false }

A Track request records one event:

Track request body
{
"type": "track",
"messageId": "order-5678-completed",
"userId": "user-1234",
"event": "Order Completed",
"timestamp": "2026-10-12T09:31:00Z",
"properties": {
"order_id": "order-5678",
"value": 42.5,
"currency": "USD"
}
}
Response (202)
{ "message_id": "order-5678-completed", "status": "accepted" }

Use the same userId in Identify and Track. A Track event with a userId can start, advance, or end a published journey whose event name matches exactly, including case. Only events that arrive after the journey is published count. After you publish a journey, wait about a minute before you send a test event. Events that arrive in the first moments after publishing can miss the journey. To check that an event reached a journey, see Confirm that an event reaches a journey. For field rules, see Send customer profiles and Send customer events.

A messageId identifies one update or one event. Reuse it only to retry the same request with the same body. Then a retry never counts twice. A repeated Identify call returns 200 with "duplicate": true and applies nothing again. A repeated Track event returns 202, counts once, and starts a journey at most once. Track requires messageId, and Identify generates one when you leave it out. Duplicates and idempotency covers every case, including a reused ID with a different body.

Create message IDs this way:

  • Track. Derive the ID from the record that caused the event, such as order-5678-completed. Or generate a UUID when the action happens and store it with the record before you send. Never generate a new ID inside your retry loop.
  • Identify. Generate the ID once per update, such as a UUID, and keep it with the request, so JustAI recognizes a retry as a duplicate. Send timestamp too.
  • Corrections. Don’t reuse a messageId to correct an event. JustAI can’t edit or delete an accepted event. If you send a corrected event with a new messageId, JustAI records it in addition to the original, and it can start a matching journey again. To remove events, contact your JustAI team.

Send your own messageId on Identify when you can give every new update a new ID. Leave messageId out only when your sync tool can’t guarantee a new ID whenever a synced value changes. JustAI then generates a new ID for each call, so it can’t recognize a retry and treats every retry as a new update. When you leave messageId out, always send timestamp, so a delayed retry can’t overwrite a newer value. Your own backend can always generate an ID once per update, so send one on every Identify call.

Send times as ISO 8601 strings with Z or a numeric offset, such as 2026-10-12T09:30:00Z, 2026-10-12T09:30:00.123Z, or 2026-10-12T11:30:00+02:00. JustAI rejects a time without a time zone, a space instead of T, a date alone, and epoch numbers.

  • Identify timestamp is when the profile change happened in your system. If you leave it out, JustAI uses the time it receives the call. JustAI rejects times more than 5 minutes in the future and times before 1970.
  • Track timestamp is required. It is when the customer did the action, not when you sent the request.

Requests can arrive out of order. On a profile, each trait keeps the value with the newest timestamp, so an older Identify call never overwrites a newer value. Keep your server clocks synchronized so that Identify times are never in the future.

JustAI doesn’t publish a fixed rate limit. Send live updates as they happen, one request per update or event. For a profile backfill, 10 or fewer concurrent requests is a conservative suggested starting point, not an enforced limit. Confirm the concurrency with your JustAI team. Before the first full run, send your JustAI team the number of profiles and your planned concurrency. Your JustAI team confirms the rate or discusses whether a customer-specific file import can meet your needs. See Load your existing customers.

Retry only temporary failures: 429, any 5xx, a timeout, or a dropped connection. Retry them with exponential backoff and jitter, and send the same body and messageId. Every other error means that the request, the key, or the URL needs a fix, and the same request fails the same way again. Status codes lists each error with its cause and fix.

If many requests fail with 5xx at the same time, lower your concurrency before you retry. Cap the number of attempts. When a request still fails, save its body and messageId so you can send it again after you fix the cause. Sending it again with the same messageId is safe, except after a 409.

This client sends an Identify request, reads the profile back, and sends a Track request. Its send function retries 429 responses, 5xx responses, timeouts, and dropped connections with backoff, and stops on every other error. The profile read makes one attempt. It reads JUSTAI_ORG_SLUG and JUSTAI_API_KEY from environment variables. For a JustAI environment other than Production, set JUSTAI_ORG_SLUG to the environment’s path value and JUSTAI_ENVIRONMENT to its environment key.

The sample sends a test event named Connection Tested, so it doesn’t start journeys built on your real events. That name stays in your event list and in journey event pickers for 30 days. In your own code, send your real event names.

Requires Node.js 18 or later.

justai.mjs
import { randomUUID } from "node:crypto";
const BASE_URL = "https://ingest.justwords.ai";
const ORG_SLUG = process.env.JUSTAI_ORG_SLUG;
const API_KEY = process.env.JUSTAI_API_KEY;
const ENVIRONMENT = process.env.JUSTAI_ENVIRONMENT;
const MAX_ATTEMPTS = 6;
const TIMEOUT_MS = 10_000;
const authHeaders = { "X-Api-Key": API_KEY };
if (ENVIRONMENT) {
authHeaders["X-JustAI-Environment"] = ENVIRONMENT;
}
const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
// Exponential backoff with full jitter, capped at 30 seconds.
const backoff = (attempt) => Math.random() * Math.min(30_000, 500 * 2 ** attempt);
async function send(path, body) {
const headers = { ...authHeaders, "Content-Type": "application/json" };
const payload = JSON.stringify(body);
for (let attempt = 1; ; attempt++) {
let response;
try {
response = await fetch(BASE_URL + path, {
method: "POST",
headers,
body: payload,
signal: AbortSignal.timeout(TIMEOUT_MS),
});
} catch (error) {
// Timeout or dropped connection.
if (attempt === MAX_ATTEMPTS) {
throw error;
}
await sleep(backoff(attempt));
continue;
}
if (response.ok) {
return response.json();
}
// Read the error body so the connection is released before a retry.
const errorBody = await response.text();
const retryable = response.status === 429 || response.status >= 500;
if (!retryable || attempt === MAX_ATTEMPTS) {
throw new Error(`JustAI returned ${response.status}: ${errorBody}`);
}
await sleep(backoff(attempt));
}
}
// Returns the profile, or null when no profile has this user ID.
async function getProfile(userId) {
const response = await fetch(
`${BASE_URL}/api/profiles/${ORG_SLUG}/${encodeURIComponent(userId)}`,
{ headers: authHeaders, signal: AbortSignal.timeout(TIMEOUT_MS) },
);
const text = await response.text();
if (response.ok) {
return JSON.parse(text);
}
// An unknown user returns JSON. A wrong path returns plain text "404 Not Found".
if (response.status === 404 && text.includes("Profile not found")) {
return null;
}
throw new Error(`JustAI returned ${response.status}: ${text}`);
}
const identify = await send(`/api/identify/${ORG_SLUG}`, {
userId: "user-1234",
messageId: randomUUID(),
timestamp: new Date().toISOString(),
traits: { email: "ada@example.com", first_name: "Ada", plan: "pro" },
});
console.log(identify);
// A read right after an update can return null or the previous values for
// a short time. Read again later if the change isn't there yet.
const profile = await getProfile("user-1234");
console.log(profile);
const track = await send(`/api/track/${ORG_SLUG}`, {
type: "track",
messageId: randomUUID(),
userId: "user-1234",
event: "Connection Tested",
timestamp: new Date().toISOString(),
properties: { source: "example-client" },
});
console.log(track);

Each sample builds the request body once, before the first attempt, so every retry sends the same messageId and timestamp. In your own code, build the body when the update or action happens and keep it with any request you save for later.

  • Store keys in a secrets manager or a server environment variable. Never put a key in browser, mobile, or other client-side code. Browsers can’t call these APIs, and a key in client code exposes full access to your environment.
  • Never log a key. Log its suffix if you need to tell keys apart. Settings > API keys lists each key by suffix.
  • Create a separate key for each service that sends data, so you can replace one without touching the others.
  • To rotate a key, create a new key and confirm test requests succeed before switching production traffic to it. A new key may not work immediately. Then revoke the old key with the trash icon in its row and select Revoke. Confirm that requests with the old key return 401 after revocation.

Log the endpoint, status code, messageId, and error body of every failed request. Set alerts on these signals:

Signal Likely cause Action
Any 401 or 403 A revoked key, or a key from the wrong environment Check the key in Settings > API keys and the environment header.
A rise in 400 or 413 A change in your data or code Fix the producer, then resend the saved requests with their saved messageId.
Any 409 from Identify Your code reused a messageId for different content Fix how you generate IDs, then resend those updates with new messageId values.
Requests that fail after every retry A network problem or a service disruption Keep the saved requests. Contact your JustAI team if failures continue.

Check the data in JustAI as well:

  • Profiles. Search a recent user ID in Users and compare the attributes.
  • Events. Open Events > Custom events and check Last seen for your event names. New events can take time to appear. If an accepted event still doesn’t appear after you refresh, contact your JustAI team with its messageId.
  • Journeys. After your marketing team publishes a journey, confirm that your events reach it. See Confirm that an event reaches a journey.
  1. Run the example client. To keep test data out of Production, run it against another environment by setting JUSTAI_ORG_SLUG and JUSTAI_ENVIRONMENT.

    The client sends an Identify call and a Connection Tested event for user-1234. If that ID belongs to a real customer, change it in the code first.

    The client prints three results. In the Identify response, success is true and duplicate is false. The profile shows email, first_name, and plan under fields. If the read came too soon, it prints null (None in Python) or the previous values. In the Track response, status is accepted.

  2. If the profile was null or showed the previous values, wait a minute and read it again:

    Terminal window
    curl "https://ingest.justwords.ai/api/profiles/$JUSTAI_ORG_SLUG/user-1234" \
    -H "X-Api-Key: $JUSTAI_API_KEY"

    For an environment other than Production, add -H "X-JustAI-Environment: $JUSTAI_ENVIRONMENT" to the command.

    The response fields contain email, first_name, and plan.

  3. In JustAI, open Events > Custom events in the same environment, and select Refresh.

    Connection Tested appears in the list after processing.