---
title: "Create Span Updates"
method: POST
path: "/v1/span-updates"
tags: ["span-updates"]
---

# Create Span Updates

`POST /v1/span-updates`

## Request

- Base URL: `https://app.tokumalabs.com/api`
- URL: `https://app.tokumalabs.com/api/v1/span-updates`
- Auth: OAuth 2
  - Password
  - Token URL: `v1/auth/token`

## Request body

- SpanUpdateBatchRequest
  - `updates` SpanUpdateRequest[], required
    - `device_id` string, required
    - `session_id` string, required
    - `span_id` string, required
    - `started_at` string, date-time, required
    - `ended_at` string, date-time, required
    - `app` AppInfo, required
      - `app_id` string, nullable
      - `app_name` string, required
      - `bundle_id` string, nullable
      - `app_path` string, nullable
    - `attributes` union
      - DocumentAttributes: Attributes for document-based sessions (Word, Excel, PowerPoint).
        - `type` 'document', required
        - `document_name` string, nullable
        - `document_path` string, nullable
      - DefaultAttributes: Attributes for sessions without app-specific identity data.
        - `type` 'default', required
      - TeamsChatAttributes: Attributes for Microsoft Teams chat sessions. Each Teams chat conversation is its own session — chat_title keys the session identity so the server groups per-chat TimeBlocks separately.
        - `type` 'teams_chat', required
        - `chat_title` string, required
        - `has_external_participants` boolean, nullable
      - ChatAttributes: Attributes for a non-Teams chat conversation (WhatsApp web first). Each conversation is its own session — ``chat_title`` keys the session identity so the server groups per-conversation TimeBlocks separately, exactly as :class:`TeamsChatAttributes` does for Teams. Deliberately generic rather than WhatsApp-named: the WhatsApp desktop clients, and later Slack / WeChat, reuse this variant and the ``CHAT_IDENTITY`` event rather than each minting their own. Only the *reader* is per-app. ``has_external_participants`` has no meaning in every chat app (WhatsApp has no org boundary) and is simply ``None`` there. **Deploy ordering — this is the hard one.** ``SessionAttributes`` is a discriminated union with fixed tags, so a client emitting ``type: "chat"`` against a server that predates this class 422s the *entire* span-update batch and the user's span is lost. Server must ship first.
        - `type` 'chat', required
        - `chat_title` string, required
        - `has_external_participants` boolean, nullable
      - EmailMessageAttributes: Attributes for Outlook desktop email/compose/calendar-invite sessions. Each opened email, compose draft, or calendar invite is its own session — `(subject, from_email)` keys the session so returning to the same message reuses the session. `subject` is null for calendar invites (Outlook AX exposes no `HeaderMainContainer` there). `view_type` is one of `email`, `calendar_invite`, `compose`, `other` (see `AxOutlookViewType` in `shared/types/span_event_types.py`). It is stored as a free string at the wire boundary so a future client-side addition does not 422 every Outlook span; downstream parsing logs and skips unknown values.
        - `type` 'email_message', required
        - `subject` string, nullable
        - `from_email` string, nullable
        - `view_type` string, required
    - `events` SpanEventData[]
      - `type` string, required
      - `at` string, date-time, required
      - `data` object
    - `is_closed` boolean

## Response `200`

Successful Response

- SpanUpdateBatchResponse
  - `processed` integer, required
  - `failed_span_ids` string[]
  - `rejections` SpanRejectionResponse[]
    - `span_id` string, required
    - `code` 'INVALID_CREDENTIALS' | 'REFRESH_TOKEN_REQUIRED' | 'INVALID_REFRESH_TOKEN' | 'REFRESH_TOKEN_REUSE_DETECTED' | 'USER_NOT_FOUND' | 'NOT_AUTHENTICATED' | 'MS365_CONFIG_NOT_FOUND' | 'MS365_INVALID_STATE' | 'MS365_TOKEN_EXCHANGE_FAILED' | 'MS365_INVALID_USER' | 'MS365_GRAPH_REQUEST_FAILED' | 'MS365_CREDENTIALS_REJECTED' | 'MS365_PERMISSION_DENIED' | 'WRONG_AUTH_METHOD' | 'CAPABILITY_REQUIRED' | 'LAST_ADMINISTRATOR' | 'TOO_MANY_REQUESTS' | 'VALIDATION_ERROR' | 'NOT_FOUND' | 'CONFLICT' | 'ALREADY_EXISTS' | 'STALE_CURSOR' | 'INVALID_CURSOR' | 'DATA_WIPE_FORBIDDEN_ENV' | 'TIME_BLOCK_INVALID_TIME_RANGE' | 'TIME_BLOCK_INVALID_SEGMENT' | 'SEGMENT_NOT_ASSIGNABLE' | 'TIME_BLOCK_INVALID_ACTIVITY_CODE' | 'TIME_ENTRY_RELEASE_NOT_FOUND' | 'TIME_ENTRY_RELEASE_EMPTY' | 'TIME_ENTRY_RELEASE_NO_MATTER' | 'TIME_ENTRY_RELEASE_NO_ACTIVITY_CODE' | 'TIME_ENTRY_RELEASE_NARRATIVE_PENDING' | 'TIME_ENTRY_NO_BLOCKS' | 'TIME_ENTRY_MERGE_NOT_FOUND' | 'TIME_ENTRY_MERGE_RELEASED' | 'TIME_ENTRY_MERGE_DIFFERENT_DATE' | 'TIME_ENTRY_MERGE_SELF' | 'GENERATION_UNAVAILABLE' | 'GENERATION_AI_INVALID_RESPONSE' | 'GENERATION_AI_PROVIDER_ERROR' | 'GENERATION_INPUT_TOO_LARGE' | 'GENERATION_CONCURRENT' | 'GENERATION_IN_PROGRESS' | 'GENERATION_TIMEOUT_RECOVERED' | 'GENERATION_DISPATCH_FAILED' | 'GENERATION_DISPATCH_LOST' | 'GENERATION_FAILED' | 'SPAN_EVENT_DISPATCH_EXHAUSTED' | 'EXTRACTION_IMAGE_NOT_FOUND' | 'EXTRACTION_IMAGE_INVALID' | 'EXTRACTION_AI_INVALID_RESPONSE' | 'EXTRACTION_AI_PROVIDER_ERROR' | 'EXTRACTION_INPUT_TOO_LARGE' | 'REASSEMBLY_ENRICH_FAILED' | 'REASSEMBLY_REBUILD_FAILED' | 'REASSEMBLY_TIMEZONE_INVALID' | 'REASSEMBLY_FAILED' | 'SPAN_OWNERSHIP_MISMATCH' | 'SPAN_INGEST_FAILED' | 'CAPTURE_IMAGE_OWNERSHIP_MISMATCH' | 'DATABASE_UNAVAILABLE' | 'UNKNOWN_ERROR', required: Centralized error codes for the API.

## Other responses

- `400`: Validation error or domain rule violation.
- `401`: Missing, expired, or invalid authentication.
- `403`: Authenticated but not permitted.
- `404`: Resource not found.
- `409`: Resource state conflict (duplicate, illegal transition).
- `422`: Request body failed schema validation.
- `500`: Server-side failure.

---

[API](https://skmtc.dev/tokumalabs/apis/tokuma-user-api.md) - [All operations](https://skmtc.dev/tokumalabs/apis/tokuma-user-api/llms.txt) - [OpenAPI document](https://skmtc.dev/tokumalabs/apis/tokuma-user-api/revisions/1f8201af459b?raw)
