---
title: "Service accounts"
---

> Documentation Index
> Fetch the complete documentation index at: https://truememory-docs.mvstg.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Service accounts

Service account authentication is for backend services that call TIE programmatically — webhooks, cron jobs, internal microservices, or any server-side code. Instead of going through the user sign-in flow, your service authenticates with a shared secret called `AUTH_SECRET`.

:::tip[Building a browser or mobile app?]
Use [User tokens](/authentication/email-password/) instead. `AUTH_SECRET` is a backend-only credential and must never be exposed to clients.
:::

## Before You Start

| What | Where to get it |
|------|----------------|
| **AUTH_SECRET** | Issued by the TIE team. Contact them to request one for your service. |
| **TIE AI Gateway URL** | The base URL for TIE's API (e.g. `https://your-tie-host`). |
| **User UUID** (if acting on behalf of users) | The `user.id` from [TIE Auth responses](/authentication/service-accounts/). Store this in your database when users sign up. |

## Authentication Modes

There are three ways to use service auth, depending on whether your service needs a user context and where your user IDs come from.

| Mode | Headers | Memory & Persona | Use case |
|------|---------|------------------|----------|
| **Service + TIE user** | `Authorization: Bearer <AUTH_SECRET>` + `X-On-Behalf-Of: <user-uuid>` | Yes | Backend acting as a TIE-registered user |
| **Service + external user** | `Authorization: Bearer <AUTH_SECRET>` + `X-External-User-Id: <your-user-id>` | Yes | 3rd-party app with its own user IDs — no TIE sign-up needed |
| **Service only** | `Authorization: Bearer <AUTH_SECRET>` | No | Stateless one-shot calls |

## Service acting as a user

Your service calls TIE on behalf of a specific user. TIE loads that user's persona and memory, and records the conversation — the same as if the user called directly. The service does not need to go through Firebase auth; it just needs to know the user's TIE UUID.

Pass `AUTH_SECRET` as the bearer token and the user's UUID in the `X-On-Behalf-Of` header:

```bash
curl -X POST https://your-tie-host/v1/chat/completions \
  -H "Authorization: Bearer $AUTH_SECRET" \
  -H "X-On-Behalf-Of: 550e8400-e29b-41d4-a716-446655440000" \
  -H "X-App-Id: whatsapp-webhook" \
  -H "Content-Type: application/json" \
  -d '{"model": "claude-haiku-4-5", "messages": [{"role": "user", "content": "What did we discuss yesterday?"}]}'
```

| Header | Required | Description |
|--------|----------|-------------|
| `Authorization` | Yes | `Bearer <AUTH_SECRET>` |
| `X-On-Behalf-Of` | Yes | The user's UUID (from `user.id` in [auth responses](/authentication/service-accounts/)) |
| `X-App-Id` | Recommended | Identifies the calling app and [scopes threads](/ai-gateway/chat-completions/#per-app-thread-isolation) |

### Where to get the user UUID

When a user signs up through [TIE Auth](/authentication/email-password/), the response includes `user.id` — a deterministic UUID derived from their Firebase UID. Store this in your database alongside the user record. When your backend needs to call TIE on that user's behalf, pass it in `X-On-Behalf-Of`.

```mermaid
sequenceDiagram
accTitle: Service-as-user authentication flow
accDescr: Shows how a backend service calls TIE on behalf of a user using AUTH_SECRET

    participant User
    participant App as Your App
    participant Backend as Your Backend
    participant TIE as TIE AI Gateway

    User->>App: Sends message (e.g. WhatsApp)
    App->>Backend: Forward message + user identifier
    Backend->>Backend: Look up user's TIE UUID from database
    Backend->>TIE: POST /v1/chat/completions<br/>Authorization: Bearer AUTH_SECRET<br/>X-On-Behalf-Of: user-uuid
    TIE->>TIE: Load user's memory & persona
    TIE-->>Backend: AI response
    Backend-->>App: Forward response to user
```

## Acting for users with external IDs

If your users never sign in to TIE, you don't need to create TIE accounts for them. Pass any stable string identifier from your own system in the `X-External-User-Id` header — TIE deterministically derives a user UUID from it (`UUID5` of `ext:<your-id>`), so the same ID always maps to the same memory graph. No registration call, no ID mapping table.

```bash
# Write a memory for your user "crm-user-8841"
curl -X POST https://your-tie-host/memories/graph \
  -H "Authorization: Bearer $AUTH_SECRET" \
  -H "X-External-User-Id: crm-user-8841" \
  -H "Content-Type: application/json" \
  -d '{"data": "User prefers morning meetings and is based in Kuala Lumpur.", "type": "text", "agent_id": "chatbot"}'

# Retrieve memory context for the same user
curl -X POST https://your-tie-host/memories/retrieve \
  -H "Authorization: Bearer $AUTH_SECRET" \
  -H "X-External-User-Id: crm-user-8841" \
  -H "Content-Type: application/json" \
  -d '{"query": "when should I schedule the call?", "agent_id": "chatbot"}'
```

**Rules for `X-External-User-Id` values:**

- Non-empty, max 256 characters, no control characters — otherwise `400 AUTH_INVALID_USER_ID`.
- **Case-sensitive**: `Alice` and `alice` derive different users. Pick one canonical form and always send it.
- Cannot be combined with `X-On-Behalf-Of` — sending both returns `400 AUTH_SUBJECT_CONFLICT`.
- Requires service auth — sending it with a user token returns `400 AUTH_SUBJECT_CONFLICT`.

:::caution[One ID, one memory graph — across all apps]
The derived user is global: two apps sending the same external ID read and write the **same** memory graph. That's intentional when the ID refers to the same human (one person, one memory). If your IDs are app-local (e.g. auto-increment integers like `42`), namespace them yourself — `myapp:42` — to avoid colliding with another app's user `42`.
:::

## Service only (stateless)

Your service makes a call with no user context. No memory, no persona, no turn recording. This is useful for utility tasks like title generation, text summarization, or content classification where there is no meaningful user to attach to.

```bash
curl -X POST https://your-tie-host/v1/chat/completions \
  -H "Authorization: Bearer $AUTH_SECRET" \
  -H "X-App-Id: title-generator" \
  -H "Content-Type: application/json" \
  -d '{"model": "gemini-2.5-flash", "messages": [{"role": "user", "content": "Generate a title for: ..."}]}'
```

**Restrictions in service-only mode:**

- `persona_id` is not allowed (returns `400`)
- Memory tools (`memory_search`, `memory_write`) are disabled
- No conversation turns are recorded
- Threads still work for multi-turn within the same request chain, but have no user association

## Security

:::danger[AUTH_SECRET is a backend secret]
Never expose `AUTH_SECRET` to browsers or mobile clients. It grants access to any user's context via `X-On-Behalf-Of` or `X-External-User-Id` — anyone with the secret can impersonate any user.
:::

- Store `AUTH_SECRET` in environment variables or a secret manager (e.g. GCP Secret Manager), not in code.
- Rotate the secret by coordinating with the TIE team. During rotation, both old and new values can be active briefly.
- If you suspect the secret has been compromised, contact the TIE team immediately to revoke and reissue it.

## Error Codes

| Code | Status | When |
|------|--------|------|
| `AUTH_TOKEN_MISSING` | 401 | No `Authorization` header provided |
| `AUTH_TOKEN_INVALID` | 401 | `AUTH_SECRET` value does not match |
| `AUTH_INVALID_USER_ID` | 400 | `X-On-Behalf-Of` is not a valid UUID, or `X-External-User-Id` is empty, longer than 256 characters, or contains control characters |
| `AUTH_SUBJECT_CONFLICT` | 400 | Both `X-On-Behalf-Of` and `X-External-User-Id` sent, or `X-External-User-Id` sent with a user token |
| `VALIDATION_ERROR` | 400 | `persona_id` used in service-only mode (no subject user) |

Source: https://truememory-docs.mvstg.com/authentication/service-accounts/index.mdx
