IDs and identityMenu
On this page

IDs and identity

Updated

Learn how Kirora ids, your own externalId, email and phone identify a contact, how identify resolves and refuses them, and how to get the id of every resource.

A contact can be known by four values: Kirora’s id for it, your own id (externalId), its email address and its phone number. This page sets out what each one is, where each is accepted, how identify decides which contact you mean, and how to get the id of every other resource.

The four identifiers

Identifier Assigned by Unique in the workspace Notes
id Kirora, at creation Yes A UUID. Never changes
externalId You Yes Your user or customer id, up to 255 characters, matched exactly
email You Yes Matched trimmed and without case
phone You Yes Stored in E.164 form (+447700900123)

Use externalId as the key between your system and Kirora. It is the identifier you control, it does not change when a contact changes their address, and the web SDK’s user token names the contact by it.

A contact needs at least one of externalId, email or phone. Each is unique, so two contacts can never share one: a write that would give a contact a value another contact holds is refused with 409 ALREADY_EXISTS, with details.field naming it.

Where your own id and the email address are accepted

Your externalId works in these places, and only these. Everywhere else, use Kirora’s id. The email address works on the contact routes and wherever a body takes a ContactIdentifier: exactly one of contactId, externalId or email.

Where How
/v1/contacts/{id} and every route under it Add ?idType=externalId and put your id in the path, or ?idType=email and the address, URL-encoded (ada%40example.com). One that names no contact is 404
trackEvent and each event in trackEventsBatch externalId in place of contactId or email
sendTransactionalMessage to: { "externalId": "…" }
triggerCampaign externalId on a recipient
addListContacts, deleteListContacts externalIds in place of contactIds or emails
deleteContacts A ContactIdentifier per entry
mergeContacts keep, a ContactIdentifier

For example, reading a contact by your id, then by its address:

Code
GET /v1/contacts/customer-881?idType=externalId
GET /v1/contacts/ada%40example.com?idType=email

listContacts has no exact-match filter for either: read the one contact by its identifier instead.

How identify finds the contact

POST /v1/contacts/identify (identifyContact) creates or updates one contact. It looks for an existing contact by externalId, then email, then phone, and stops at the first that matches. If none matches, it creates the contact and returns 201 with meta.created: true; otherwise it updates the match and returns 200.

JSON
{ "externalId": "customer-881", "email": "[email protected]", "firstName": "Ada" }

Once a contact is matched, the identifiers ranked below the one that matched may change. Those ranked above it can only be filled in where the contact has none:

  • Matched by externalId. email and phone in the request replace the contact’s. This is how you change an address.
  • Matched by email or phone, and the contact has no externalId. Your externalId is added to it.
  • Matched by email or phone, and the contact has a different externalId. The request names two contacts. It is refused with 409 CONFLICT and details.field naming the identifier that differs (externalId here). Nothing is written.
Response Cause What to do
409 CONFLICT A lower-ranked identifier matched a contact that holds a different value for a higher-ranked one Check your mapping: either the email belongs to someone else in your system, or the contact needs merging
409 ALREADY_EXISTS The email, phone or externalId you are setting belongs to another contact (details.field) Merge the two contacts, or correct the value
400 VALIDATION_ERROR None of the three identifiers was sent Send at least one
400 SIGNUP_BLOCKED The address matches the workspace’s signup blocklist, so no contact is created Nothing to retry; the address is blocked on purpose

identifyContactsBatch resolves each contact exactly this way, and returns these errors per contact (see Batches).

When an email address changes

Identify by externalId with the new address, or update the contact:

Code
PATCH /v1/contacts/customer-881?idType=externalId
{ "email": "[email protected]" }

The contact keeps its id, consent, history and list memberships. Messages already sent keep the address they went to in recipientEmail; the contact’s current address is contact.email. The change is recorded as a contact.field_updated event, which can start a journey.

Identifying by the new address alone, without externalId, finds no contact and creates a second one. That is the usual cause of duplicates.

Sends and events that name a contact

  • A transactional send to an email address that no contact has creates a contact for transactional mail only. With externalId or contactId, the contact must already exist. To create a contact or set its profile fields, call POST /v1/contacts/identify first. A transactional send never changes a contact’s profile or consent.
  • A tracked event must name an existing contact. One that names nobody is 404 and creates nothing, so identify first.
  • The web SDK acts for the contact whose externalId is the user token’s sub. If no contact has it, the SDK’s first call creates one. Sign-out in the SDK detaches the device from that contact; it does not delete the contact.

Merging duplicates

POST /v1/contacts/{id}/merge (mergeContacts) folds the contact in the path, the duplicate, into the contact you keep, which keep names by any identifier. Here the email-only Ada, signed up before she had an account, merges into customer-881:

Code
POST /v1/contacts/ada%40example.com/merge?idType=email
{ "keep": { "externalId": "customer-881" } }
  • The kept contact keeps its own values and gains any the duplicate had that it lacks, including identifiers. An address or phone it gains brings that channel’s consent with it.
  • The duplicate’s messages, events, devices and consent changes move across; its lists and suppressions are added.
  • The duplicate is then deleted, ending its journey enrollments. A merge cannot be undone. The response is the kept contact, after the merge.

A contact with a message being handed to a provider returns 409 CONTACT_SEND_IN_PROGRESS; retry in a few seconds. A message waiting to be retried doesn’t block it. A keep that names no contact is 404; merging a contact into itself is 400.

Getting the id of a resource

Every resource has a Kirora id (a UUID). Create calls return it in data.id, and list calls return it on every item. Some resources can also be named another way.

Resource Where its id comes from Also named by
Contact identifyContact, listContacts externalId or email, on the routes above
Template createTemplate, listTemplates Its alias, in a transactional send’s email.template
List createList, listLists —
Segment createSegment, listSegments —
Campaign createCampaign, listCampaigns —
Journey createJourney, listJourneys —
Topic createTopic, listTopics —
Push project and push app listPushProjects: the project’s id, and each app’s id in apps (the pushAppId a device registers with) —
Sending domain createSendingDomain, listSendingDomains —
Webhook createWebhook, listWebhooks —
Message The send’s response (data.messages[].id), listMessages correlationId, as a filter on listMessages
Event trackEvent (data.id) Your idempotency key, which a repeat returns the same event for
Your workspace getMe (data.workspace.id) —
Partner workspace createWorkspace, listWorkspaces Its externalId, which is your own tenant id, in the path and the Kirora-Workspace header

Ids differ between workspaces, so a staging workspace’s ids never work in live (see Staging).

Shared devices

One device can be used by several of your contacts: a family tablet, a shop’s till. Kirora follows whoever is signed in, never the device:

  • From your server, name the contact who acted in each call (externalId, email or contactId), not the device.
  • With the web SDK, call logout() in your sign-out handler, before your own sign-out, then identify the next contact when they sign in. Events tracked before sign-in wait on the device and go as the next contact who signs in. The SDK asks your server for a fresh user token at sign-in and whenever one expires; see Web SDK.
  • Push: registering an installation for a different contact moves the device to them, so the previous contact stops receiving push on it. See Push notifications.

Identify can also record marketing consent, in consent: email and push, each true or false, and statement, how the contact agreed. Granting consent (a channel, or a topic set to true) needs the statement; without one the call is refused with 400 VALIDATION_ERROR at consent.statement:

JSON
{
  "externalId": "user_123",
  "email": "[email protected]",
  "consent": {
    "email": true,
    "statement": { "basis": "opted_in", "evidence": "Sign-up form on example.com" }
  }
}

The same request without statement answers:

JSON
{
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "Say how this contact agreed (consent.statement: { basis, evidence }) when granting consent",
    "errors": [
      {
        "in": "body",
        "field": "consent.statement",
        "code": "custom",
        "message": "Say how this contact agreed (consent.statement: { basis, evidence }) when granting consent"
      }
    ]
  }
}

Withdrawing ("email": false) needs no statement. A contact reads its consent back as consent: { "email": true, "push": false }. Topic choices go in topics, keyed by topic id: {"<topicId>": {"subscribed": true}} for every channel, or {"<topicId>": {"channels": {"email": true, "push": false}}} per channel. Each change is recorded in the contact’s consent changes (listContactConsentChanges). A transactional send needs no marketing consent.

Next steps