Skip to content

Identify API

The Identify API creates a customer profile the first time it sees a userId, and updates that profile on every later call. A customer profile holds the traits you know about one person, such as their email address and plan. JustAI saves the update before it responds.

Send customer profiles walks through planning and sending profiles. This page is the field-by-field reference.

POST https://ingest.justwords.ai/api/identify/{org_slug}
X-Api-Key: YOUR_API_KEY
Content-Type: application/json
Header Required Value
X-Api-Key Yes, or send the key in Authorization Your JustAI API key. Authorization: Bearer and HTTP Basic also work. See Authentication.
Content-Type Recommended application/json
X-JustAI-Environment Non-production environments only The environment key, for example staging. Leave it out for Production. See Environments.

Send one JSON object with these fields:

Field Type Required Rules
userId string Yes Your stable ID for the person. Must not be empty after trimming. JustAI removes leading and trailing spaces before it saves the ID. A number or null returns 400.
traits object Yes, unless consent has an entry The traits to save. Keys are strings. Values can be any JSON value: string, number, boolean, null, array, or object. Needs at least one key unless consent has at least one entry.
messageId string No Your unique ID for this update. Must not be empty after trimming. If you leave it out, JustAI generates a UUID and returns it.
timestamp string No When the change happened, in ISO 8601 with Z or a numeric offset. If you leave it out, JustAI uses the time it receives the call. See Timestamps.
consent array No Channel consent decisions. See Consent decisions.
context object No Accepted for compatibility. JustAI does not save or use it.
integrations object No Accepted for compatibility. JustAI does not save or use it.

JustAI ignores every other top-level field, including type, anonymousId, sentAt, originalTimestamp, receivedAt, channel, version, writeKey, and projectId.

These rules matter most when a CDP builds the payload:

  • JustAI reads traits only from top-level traits and ignores context.traits. A call with traits only in context.traits returns 400.
  • JustAI ignores anonymousId and has no alias endpoint. An Identify call does not link earlier anonymous events to the profile.
  • The endpoint does not check the call type. Any body with a userId and traits updates the profile, including a Segment or RudderStack group call. Send only identify calls to this endpoint.
  • An anonymous Identify call, with userId set to null or left out, returns 400. Filter these calls out before they reach JustAI.
  • traits, context, and integrations must be objects when present. null returns 400.

timestamp must be an ISO 8601 date-time string with a time zone. JustAI accepts these forms:

  • 2026-10-01T12:00:00Z
  • 2026-10-01T12:00:00.123Z, with fractional seconds of any length
  • 2026-10-01T12:00:00+05:30 or 2026-10-01T12:00:00+0530
  • 2026-10-01T12:00Z, with no seconds

JustAI rejects these forms with 400:

  • 2026-10-01T12:00:00, with no time zone
  • 2026-10-01 12:00:00Z, with a space in place of T
  • 2026-10-01T12:00:00+05, with an hour-only offset
  • 2026-10-01, a date alone
  • 2026-10-01t12:00:00z, with a lowercase t or z
  • 2026-02-30T00:00:00Z, a date that does not exist
  • 1790856000000, an epoch number

The timestamp also has two bounds:

  • It cannot be earlier than 1970-01-01T00:00:00Z.
  • It cannot be more than 5 minutes later than JustAI’s clock. A later time returns 400 with {"error":"timestamp cannot be more than 5 minutes in the future"}.

consent is an optional array of decisions. Each decision records one person’s choice for one channel and purpose.

Field Values Required Notes
channel "email", "sms" Yes
status "granted", "withdrawn", "unknown" Yes
purpose "marketing", "tracking" No Defaults to "marketing". "tracking" covers email open and click tracking, and applies to email only.
basis "consent", "legitimate_interest", "contract", "implied_transaction", "implied_contract", "implied_inquiry" No The legal basis for the decision.
source "import", "signup_form", "api", "manual" No Defaults to "api".
consentedAt integer No When the person made the decision, in epoch milliseconds.
proofRef string No What the person agreed to, for example a form ID. Must not be empty.
expiresAt integer No When the decision lapses, in epoch milliseconds.

JustAI ignores unknown fields inside a decision.

Rule Issue message when the rule is broken
A granted implied_transaction, implied_contract, or implied_inquiry decision needs consentedAt. consentedAt is required for a granted implied-consent basis
A granted implied_contract decision also needs expiresAt, set to the contract’s end date plus the period that Canada’s Anti-Spam Legislation (CASL) allows for implied consent. expiresAt is required for a granted implied_contract decision (termination + CASL window)
A tracking decision applies to email only. tracking consent applies to the email channel only
A granted tracking decision uses basis: "consent" or leaves basis out. a tracking grant needs basis 'consent'

A decision applies to the contact address on the profile, not to the userId:

  • An email decision applies to traits.email, trimmed and lowercased.
  • An sms decision applies to traits.phone, trimmed.

JustAI takes the address from the same call, or from the stored profile as of the decision time. If no address is known, JustAI skips the decision and still returns 200. Include traits.email or traits.phone in the same call unless the profile already has it.

A consent-only call is valid. Send "traits": {} with at least one decision, for example to record a withdrawal from your preference center. A consent-only call for a user ID that JustAI has not seen records no decision, because the new profile has no address.

  • The newest decision wins. A decision’s time is its consentedAt. Without consentedAt, it is the call’s timestamp, or the time JustAI received the call.
  • If consentedAt is more than 5 minutes in the future, JustAI uses the call’s time instead.
  • When two decisions have the same time, withdrawn wins over granted.
  • A decision with status unknown never replaces an existing decision.
Basis A grant expires
implied_transaction 2 years after consentedAt
implied_inquiry 6 months after consentedAt
implied_contract At the expiresAt you send

For implied_transaction and implied_inquiry, an earlier expiresAt that you send takes precedence.

Agree with your JustAI team how your consent records map to these fields before launch. Identity and consent covers the decisions to make.

  • Identify writes only the top-level traits that you send. Traits that you leave out stay unchanged.
  • Identify never removes a trait. Sending null stores the value null, and the key stays on the profile.
  • A nested object or array replaces the whole stored value for that trait. There is no deep merge. For example, sending "address": {"city": "Paris"} replaces the entire stored address object.
  • Each trait has its own update time. JustAI writes a trait only when the call’s time is equal to or later than the time that trait was last written. The call’s time is its timestamp, or the time JustAI received it.
  • An older call does not overwrite a newer value for a trait, but its other, newer traits still apply. When two calls have the same time, the call that arrives later wins.
  • Every accepted call returns 200, even when JustAI skips some traits as older. Read the profile back to see what JustAI saved.
  • Confirm with your JustAI team whether A profile attribute changes is available for your organization. When available, a call that changes at least one trait value can start that journey. A call that changes nothing, and a duplicate retry, trigger nothing.

Ask your JustAI team whether treating each email address as one person is available and appropriate for your organization. Confirm that setup before relying on it. When it is enabled, an Identify call whose traits.email already belongs to another profile joins the two profiles, and both user IDs read back the same profile. Placeholder values that are not real addresses, such as unknown or N/A, never join profiles.

Terminal window
curl -X POST "https://ingest.justwords.ai/api/identify/$JUSTAI_ORG_SLUG" \
-H "X-Api-Key: $JUSTAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"userId": "user-1234",
"messageId": "user-1234:2026-10-01T12:00:00Z",
"timestamp": "2026-10-01T12:00:00Z",
"traits": {
"email": "ada@example.com",
"first_name": "Ada",
"plan": "pro",
"address": { "city": "Paris", "country": "FR" }
},
"consent": [
{
"channel": "email",
"status": "granted",
"basis": "consent",
"source": "signup_form",
"consentedAt": 1790855880000,
"proofRef": "newsletter-signup-form"
}
]
}'
Response
{"success":true,"messageId":"user-1234:2026-10-01T12:00:00Z","duplicate":false}
Status Body Meaning
200 {"success":true,"messageId":"...","duplicate":false} JustAI accepted the call. It skips any trait whose stored value is newer, and a call can leave every value unchanged. Read the profile back to see what changed.
200 {"success":true,"messageId":"...","duplicate":true} JustAI already accepted this messageId with the same userId, timestamp, and traits. Nothing is applied again.
400 {"error":"Invalid Identify call","issues":[...]} The body failed validation. See Errors.
400 {"error":"timestamp cannot be more than 5 minutes in the future"} timestamp is more than 5 minutes ahead of JustAI’s clock.
409 {"error":"messageId <id> was already used for a different call"} This messageId was used before with a different userId, timestamp, or traits.

Authentication, path, and server errors are the same for all three APIs. See Errors and retries.

JustAI remembers each messageId per environment. The same ID in Production and in another environment does not collide.

  • Use a new messageId for every new update. Reuse a messageId only to retry the same call with the same body.
  • An identical retry returns 200 with "duplicate": true and changes nothing.
  • A reused messageId with a different userId, timestamp, or traits returns 409. A timestamp that is present in one call and absent in the other counts as different.
  • JustAI does not compare consent when it checks for a duplicate. If you reuse a messageId with the same traits and a changed consent decision, JustAI returns "duplicate": true and drops the new decision. Send every consent change with a new messageId.

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.

For warehouse and reverse ETL syncs, build the messageId from values that change whenever any synced value changes, such as a hash of the user ID, the update time, and every synced trait. The Hightouch profile sync template builds it this way. An ID built only from user_id and updated_at returns 409 when a trait changes but updated_at doesn’t.

A 400 from Identify lists every problem that JustAI found. Each issue has a code, a path to the field, and a message. Some issues carry extra fields, such as expected and received.

Response
{"error":"Invalid Identify call","issues":[{"code":"invalid_type","expected":"string","received":"undefined","path":["userId"],"message":"Required"}]}
Cause Issue path Issue message
userId is missing ["userId"] Required
userId is null ["userId"] Expected string, received null
userId is a number ["userId"] Expected string, received number
userId is empty or only spaces ["userId"] String must contain at least 1 character(s)
No traits and no consent, or traits only in context.traits ["traits"] At least one trait is required unless consent is provided
traits is null ["traits"] Expected object, received null
timestamp has the wrong format ["timestamp"] Invalid datetime. If JustAI cannot read the value as a date at all, a second issue follows with the message Timestamp must be on or after 1970-01-01T00:00:00Z.
timestamp is a number ["timestamp"] Expected string, received number
timestamp is before 1970 ["timestamp"] Timestamp must be on or after 1970-01-01T00:00:00Z
The body is an array [] Expected object, received array
The body is not valid JSON [] Expected object, received null
A consent decision breaks a rule For example, ["consent",0,"consentedAt"] See Validation rules

Read the profile back with the Profiles API and compare fields with what you sent. You can also search for the user ID on Users in JustAI and open the profile.

A read right after an update can briefly show the previous values. If your change is not there, wait a minute and read again.