User Management#

Every person your application talks to is represented by a User, an abstract identity that can hold multiple platform-specific Identities (a WhatsApp number, a Slack ID, an email address). This guide covers creating, querying, reorganising, and merging users through the GraphQL API.

All operations below are available on the developer API at https://api.outeract.com/ with an API key (Authorization: Bearer <api-key>). User queries require the users:read scope; mutations require users:write.

Users vs Identities#

flowchart TB
    subgraph U["User: John Doe"]
        I1["Identity (WHATSAPP)<br/>externalId: +14155551234"]
        I2["Identity (SLACK)<br/>externalId: U12345678"]
        I3["Identity (PHONE)<br/>externalId: +14155551234"]
    end
  • A User belongs to an application and carries profile config (name, systemUser) plus a materialized lastActiveAt timestamp.
  • An Identity (UserIdentity) is a concrete handle on a platform: an externalId plus either a platformConnection (for platform-bound identities) or an explicit identityType (for standalone identities such as PHONE).

An identity’s identityType is the explicit type if set, otherwise it is derived from the platform connection’s platform name.

See the Users concept page for the full data model.

Auto-Creation#

You usually don’t create users yourself. Outeract auto-creates a User + Identity when:

  1. An inbound message arrives from an unknown sender
  2. You send an outbound message to a new recipient
  3. A link code activation introduces a new identity
Let auto-creation do the work where possible, because it guarantees identities are correctly bound to the platform connection that observed them.

Creating Users Manually#

Manual creation is useful for pre-registering users (e.g. importing a CRM) or creating bot/system users.

mutation {
  createUser(name: "John Doe") {
    id
    name
    systemUser
  }
}

Pass isSystemUser: true to mark the user as a bot/agent. To change a user later, use updateUser; only the fields you pass are changed:

mutation {
  updateUser(
    userId: "user-uuid"
    name: "John D."
    config: { crm_id: "cust_8823" }
  ) {
    id
    name
    config
  }
}

Querying Users#

The users query is cursor-paginated and supports search, config filters, and activity ordering:

query {
  users(first: 20, search: "john", orderBy: "last_active_at") {
    edges {
      node {
        id
        name
        systemUser
        lastActiveAt
        identities {
          id
          externalId
          identityType
        }
      }
    }
    pageInfo {
      hasNextPage
      endCursor
    }
  }
}
  • search matches name, user ID, or identity external ID.
  • orderBy accepts "created_at" (default, newest first) or "last_active_at" (most recently active first, see below).
  • userIds fetches a specific batch of users by ID (pagination is skipped).
  • configFilters matches config fields exactly, e.g. configFilters: [{ field: "system_user", value: "true" }].

Filtering by Identity#

To resolve a platform handle back to a user, query identities directly:

```graphql query { identities(externalIds: ["+14155551234"]) { id externalId identityType user { id name } } } ```
```graphql query { identities(userId: "user-uuid") { id externalId identityType } } ```
```graphql query { identities(identityType: "WHATSAPP", limit: 50) { id externalId user { id name } } } ```

Adding Identities#

createIdentity attaches a new handle to an existing user. If identityType matches a platform name (e.g. "WHATSAPP", "TWILIO") and your app has a connection for that platform, the identity is bound to it; otherwise a standalone identity is created with that type:

mutation {
  createIdentity(
    userId: "user-uuid"
    externalId: "+14155551234"
    identityType: "WHATSAPP"
  ) {
    id
    externalId
    identityType
  }
}

Reassigning an Identity#

Move a single identity to a different user (e.g. a shared phone number was attributed to the wrong person):

mutation {
  transferIdentity(
    identityId: "identity-uuid"
    targetUserId: "user-uuid"
  ) {
    id
    user { id name }
  }
}

To remove a handle entirely, use deleteIdentity(identityId: ...). Deleting a whole user (deleteUser(id: ...)) cascades to all of its identities.

Merging Duplicate Users#

When the same person appears as two users (common before they’ve linked platforms), merge them with transferIdentities. This is the mutation behind the console’s Merge Users dialog:

mutation {
  transferIdentities(
    sourceUserId: "user-to-merge"
    targetUserId: "user-to-keep"
    deleteSourceUser: true
    deleteSourceConversations: true
  ) {
    id
    identities {
      id
      externalId
    }
  }
}

A merge:

  • Moves all identities from the source user to the target
  • Transfers participant edges, so conversations follow the user
  • Deduplicates conversations (messages merge into the oldest conversation)
  • Creates a user.merged event
  • Deletes the source user (unless deleteSourceUser: false)
Merging is not reversible. If linking should be user-initiated, prefer link codes: the user proves ownership of both identities and Outeract performs the merge for you.

Link codes are the self-service alternative to manual merging: generate a code for a user’s identity on platform A, and when they send it from platform B, Outeract links both identities to a single user.

mutation {
  generateLinkCode(identityId: "identity-uuid", expiryMinutes: 15) {
    code
    expiresAt
  }
}

See the Link Codes guide for the full flow, auto-detection, and error handling.

Activity Tracking with lastActiveAt#

lastActiveAt is a materialized, indexed column on the User, updated as events flow through the ledger, so sorting an entire user base by recency is a cheap query rather than an event-table aggregation:

query {
  users(first: 10, orderBy: "last_active_at") {
    edges {
      node {
        id
        name
        lastActiveAt
      }
    }
  }
}

Use this to build “recently active” views or to find dormant users worth re-engaging.

See Also#