Events#

Events are the core data model in Outeract. Every message, status update, and action is represented as an immutable event, providing a complete audit trail of all activity.

Overview#

Outeract uses an event-sourced architecture where:

  • All state changes are captured as events
  • Events are immutable (never modified after creation)
  • Current state is derived from the event stream
  • Full history is always available
flowchart LR
    subgraph Stream["Event Stream"]
        E1["message.inbound<br/>t=0"] --> E2["message.outbound<br/>t=1"]
        E2 --> E3["message.delivered<br/>t=2"]
        E3 --> E4["message.read<br/>t=3"]
        E4 --> More["..."]
    end

Event Model#

FieldTypeDescription
idUUIDUnique identifier
app_idUUIDApplication this event belongs to
event_type_idUUIDReference to EventSchema
statusstringpending, processing, completed, failed
payloadJSONEvent-specific data
origin_event_idUUIDParent event (for status updates)
processed_atdatetimeWhen the event was processed
created_atdatetimeWhen the event was created

Event Types#

Events are categorized by type. Built-in types follow a hierarchical naming convention:

Message Events#

message.inbound    - Incoming message from a user
message.outbound   - Outgoing message to a user

Payload Structure:

{
  "type": "message",
  "message": {
    "text": "Hello, world!",
    "role": "user"
  },
  "platform": "whatsapp",
  "external_message_id": "wamid.xxx",
  "created_at": "2024-01-15T10:30:00Z",
  "sent_at": "2024-01-15T10:30:01Z",
  "delivered_at": "2024-01-15T10:30:02Z",
  "read_at": null,
  "failed_at": null
}
link_code.generated   - A link code was created
link_code.activation  - A link code was used

Generation Payload:

{
  "code": "1234-5678-9012-3456",
  "expiry_minutes": 15,
  "max_uses": 1,
  "uses": 0,
  "generated_by_identity_id": "pu_abc123"
}

User Events#

user.merged  - Two user records were merged

Custom Events#

You can define custom event types using any valid name. Event type names must:

  • Use lowercase letters, numbers, dots, and underscores only
  • Match the pattern: ^[a-z0-9_.]+$
  • Not use reserved prefixes: message.*, user.*, system.*, link_code.*
order.created
payment.completed
support.ticket.opened
purchase
subscription.renewed

Event Relationships#

Events are connected to other entities via edges:

flowchart TB
    Event["Event<br/>(message.inbound)"]
    Event --> SB["sent_by edge"]
    Event --> ST["sent_to edge"]
    Event --> AT["attachment edge"]
    SB --> PU["Platform User"]
    ST --> SU["System User"]
    AT --> F["File"]

Querying Events#

Get Recent Events#

query {
  events(limit: 20) {
    id
    eventType
    payload
    createdAt
  }
}

Filter by Event Type#

query {
  events(eventTypes: ["message.inbound", "message.outbound"], limit: 50) {
    id
    eventType
    payload
  }
}

Filter by Date Range#

query {
  events(
    since: "2024-01-01T00:00:00Z"
    until: "2024-01-31T23:59:59Z"
  ) {
    id
    eventType
    createdAt
  }
}

Filter by Conversation#

Use conversationId to get all events in a specific conversation:

query {
  events(conversationId: "conv_abc123") {
    id
    eventType
    payload
  }
}

Filter by Relationships (RelationFilter)#

Use relatedTo for advanced filtering based on event-to-event relationships. Since conversations are events, this is the primary way to filter by conversation. Supports nested AND/OR conditions:

Simple filter - events in a conversation:

query {
  events(relatedTo: {eventIds: ["conv_123"], direction: "outgoing"}) {
    id
    eventType
  }
}

OR filter - events in ANY of the conversations:

query {
  events(
    relatedTo: {
      or_: [
        {eventIds: ["conv_1"], direction: "outgoing"},
        {eventIds: ["conv_2"], direction: "outgoing"}
      ]
    }
  ) {
    id
    eventType
  }
}

AND filter - events in ALL conversations (intersection):

query {
  events(
    relatedTo: {
      and_: [
        {eventIds: ["conv_1"], direction: "outgoing"},
        {eventIds: ["conv_2"], direction: "outgoing"}
      ]
    }
  ) {
    id
    eventType
  }
}

Nested conditions - (A OR B) AND C:

query {
  events(
    relatedTo: {
      and_: [
        {
          or_: [
            {eventIds: ["conv_1"], direction: "outgoing"},
            {eventIds: ["conv_2"], direction: "outgoing"}
          ]
        },
        {eventIds: ["conv_3"], direction: "outgoing"}
      ]
    }
  ) {
    id
    eventType
  }
}

RelationFilter Input:

FieldTypeDescription
eventIds[UUID]Event IDs to filter by (e.g., conversation IDs)
directionStringEdge direction: "outgoing", "incoming", or "any" (default)
and_[RelationFilter]Nested filters combined with AND
or_[RelationFilter]Nested filters combined with OR

Note: Specify only one of eventIds, and_, or or_ per filter object.

Note: RelationFilter only matches event-to-event relationships. For filtering by user, use the userId parameter instead.

Filter by Edge Target (Arbitrary Nodes)#

Use relatedNodeId to filter events by an edge (event as source) pointing at an arbitrary node, not only other events. For example, all events with any edge targeting a specific user:

query {
  events(relatedNodeId: "user_xyz789") {
    id
    eventType
  }
}

Two optional parameters narrow the match (both are ignored unless relatedNodeId is given):

ParameterTypeDescription
relatedNodeIdUUIDOnly events having an edge whose target node ID equals this
relatedNodeTypeStringRequire the edge’s target node type (e.g. "user"). Omit for any type
relatedEdgeTypeStringRequire the edge’s edge type (e.g. "sent_to", "participant"). Omit for any edge type

This combines with all other filters (AND) and is applied at the database level, so cursor pagination stays exact: last: 50 returns 50 matching events. A typical use is scoping a user’s history to one counterpart:

query {
  # The 50 newest events for a user that also have a sent_to edge to another node
  events(
    userId: "user_abc123"
    relatedNodeId: "system_user_xyz789"
    relatedEdgeType: "sent_to"
    last: 50
  ) {
    id
    eventType
    payload
  }
}

Get Events for a User#

query {
  events(userId: "user_abc123", limit: 50) {
    id
    eventType
    payload
  }
}

Combining Filters#

All filters can be combined and are applied with AND logic:

query {
  # Events for a user in a specific conversation
  events(userId: "user_abc123", conversationId: "conv_xyz789") {
    id
    eventType
    payload
  }
}

Creating Events#

Via Message API#

When you send a message, an event is automatically created:

mutation {
  sendMessage(
    platformConnectionId: "pc_abc123"
    recipientUserId: "user-uuid"
    message: "Hello!"
  ) {
    id
    eventTypeName
  }
}

This creates a message.outbound event with appropriate edges.

Custom Events#

Create custom events for your application:

mutation {
  createEvent(
    eventType: "order.created"
    payload: {
      order_id: "order_12345"
      total: 99.99
      currency: "USD"
    }
  ) {
    id
    eventType
    payload
  }
}

Event Status#

Events have a lifecycle status:

StatusDescription
pendingEvent created, not yet processed
processingCurrently being processed
completedSuccessfully processed
failedProcessing failed

Status Updates#

Status updates create child events linked via origin_event_id:

flowchart TB
    Parent["message.outbound<br/>id: evt_001<br/>status: completed"]
    Parent -->|origin_event_id| Delivered["delivered status<br/>evt_002"]
    Parent -->|origin_event_id| Read["read status<br/>evt_003"]
    Parent -->|origin_event_id| Failed["failed status"]

Query status updates:

query {
  event(id: "evt_001") {
    id
    eventType
    childEvents {
      id
      eventType
      payload
    }
  }
}

Event Schemas#

Event types are defined by EventSchemas, managed in the console under Settings → Event Schemas. Each has a name, an optional JSON Schema, an enforce-validation flag, and a global/app-specific scope.

Built-in (Global) Schemas#

Global schemas are available to all applications:

  • message.inbound
  • message.outbound
  • link_code.generated
  • link_code.activation

Custom Schemas#

Define custom event types with JSON Schema validation. Add a schema named order.created with enforced validation and this JSON Schema:

{
  "type": "object",
  "required": ["order_id", "total"],
  "properties": {
    "order_id": { "type": "string" },
    "total": { "type": "number", "minimum": 0 },
    "currency": { "type": "string", "enum": ["USD", "EUR", "GBP"] }
  }
}

Event Subscriptions (Webhooks)#

Subscribe to events via outbound webhooks. Create a subscription in the console under Settings → Webhooks, pointing at your endpoint (e.g. https://myapp.com/webhooks/outeract) and matching the event types you care about (e.g. message.inbound, order.*).

When matching events occur, Outeract POSTs to your URL:

{
  "event_id": "evt_abc123",
  "event_type": "message.inbound",
  "app_id": "app_xyz789",
  "payload": {
    "message": { "text": "Hello!" }
  },
  "edges": {
    "sent_by": {
      "identity_id": "pu_123",
      "external_id": "+14155551234"
    }
  },
  "created_at": "2024-01-15T10:30:00Z"
}

Learn more about Webhooks →

Best Practices#

1. Use Event Types Consistently#

Follow the category.action naming convention for custom events.

2. Include Relevant Data in Payloads#

Store enough context in the payload to understand the event without external lookups.

3. Don’t Store Sensitive Data#

Avoid putting passwords, tokens, or PII in event payloads.

4. Use Edges for Relationships#

Link events to users and files via edges, not payload fields.

5. Handle Idempotency#

Events may be delivered multiple times. Use external_message_id for deduplication.