---
title: "Create Event Handler"
method: POST
path: "/extensions/roboledger/{graph_id}/operations/create-event-handler"
tags: ["RoboLedger: Ledger & Events"]
---

# Create Event Handler

`POST /extensions/roboledger/{graph_id}/operations/create-event-handler`

## Request

- Base URL: `https://api.robosystems.ai` (the document declares no server; this is the origin it was published from)
- URL: `https://api.robosystems.ai/extensions/roboledger/{graph_id}/operations/create-event-handler`
- Auth: one of:
  - API key in header `X-API-Key`
  - HTTP bearer

Define a rule that fires GL transactions when a matching event block is created with apply_handlers=True. Match criteria (event_type, event_category, match_source, match_agent_type, etc.) act as AND-joined filters — null fields match anything. The highest-priority matching handler wins. AI-suggested handlers (suggested_by='ai') require approval before they are eligible for matching.

**Idempotency**: supply an `Idempotency-Key` header to make safe retries; replays within 24 hours return the same envelope. Reusing the key with a different body returns HTTP 409 Conflict.

## Path parameters

- `graph_id` string, required

## Headers

- `Idempotency-Key` string, nullable

## Request body

- CreateEventHandlerRequest: Register a new event-type → transaction-template rule. When `create-event-block` runs with `apply_handlers=True`, the registry resolves the *highest-priority active* handler whose match criteria all match the event, then evaluates the `transaction_template` to produce GL rows. Match precedence: among active handlers for the same `event_type`, the one with the most specific match (more match fields satisfied) wins; ties broken by `priority` desc, then `created_at` asc. All match fields except `event_type` are optional — leaving them unset matches anything. Use `match_metadata_expression` for fine-grained discrimination (e.g. only payroll categories).
  - `name` string, required: Human-readable handler name (unique per graph).
  - `description` string, nullable: Free-form description shown in admin UIs.
  - `event_type` string, required: Event type to match. Matches the `event_type` field on incoming events from `create-event-block`.
  - `event_category` string, nullable: Optional category filter (e.g. 'expense', 'revenue').
  - `match_source` string, nullable: Match the event's `source` field (e.g. 'quickbooks', 'plaid'). Useful when the same event_type comes from multiple integrations.
  - `match_agent_type` string, nullable: Match agent-emitted events by agent_type.
  - `match_resource_type` string, nullable: Match resource-bound events by resource_type.
  - `match_metadata_expression` object, nullable: JSONPath-style equality map against event.metadata, e.g. {"category": "payroll"} or {"metadata.category": "payroll"}
  - `transaction_template` TransactionTemplate, required: The handler's output spec — one or more balanced entries to post. Wire shape:: { "transactions": [{ "entry_template": { "debit": {"element_id": "elem_...", "amount": "{{ event.amount }}"}, "credit": {"element_id": "elem_...", "amount": "{{ event.amount }}"} } }] }
    - `transactions` TransactionTemplateItem[], required: At least one debit/credit entry pair
      - `entry_template` TransactionTemplateEntry, required: One balanced entry (debit + credit pair) — the inner shape of entry_template.
        - `debit` TransactionTemplateLeg, required: One side of a journal entry leg (debit or credit).
          - `element_id` string, required: Element ULID (elem_ prefixed) identifying the account to post to
          - `amount` string, required: Amount expression. Supports: '{{ event.amount }}' — raw event amount (cents); '{{ event.amount }} / 2' — half of event amount; '{{ event.metadata.fee_cents }}' — field from event metadata
        - `credit` TransactionTemplateLeg, required: One side of a journal entry leg (debit or credit).
          - `element_id` string, required: Element ULID (elem_ prefixed) identifying the account to post to
          - `amount` string, required: Amount expression. Supports: '{{ event.amount }}' — raw event amount (cents); '{{ event.amount }} / 2' — half of event amount; '{{ event.metadata.fee_cents }}' — field from event metadata
  - `priority` integer: Tiebreaker when multiple equally-specific handlers match. Higher = wins.
  - `is_active` boolean: Inactive handlers are ignored at match time.
  - `origin` 'hub' | 'tenant': Provenance of the handler. `tenant` = author by graph owner; `hub` = platform-shipped template (immutable for tenants).
  - `metadata` object: Free-form metadata stored alongside the handler.

## Response `200`

Successful Response

- OperationEnvelopeEventHandlerResponse
  - `operation` string, required: Kebab-case operation name
  - `operationId` string, required: op_-prefixed ULID for audit and SSE correlation
  - `status` 'completed' | 'pending' | 'failed', required: Operation lifecycle state
  - `result` EventHandlerResponse
    - `id` string, required
    - `name` string, required
    - `description` string, nullable
    - `event_type` string, required
    - `event_category` string, nullable
    - `match_source` string, nullable
    - `match_agent_type` string, nullable
    - `match_resource_type` string, nullable
    - `match_metadata_expression` object, nullable
    - `transaction_template` object, required
    - `priority` integer, required
    - `is_active` boolean, required
    - `origin` string, required
    - `suggested_by` string, nullable
    - `confidence` number, nullable
    - `approved_by` string, nullable
    - `approved_at` string, date-time, nullable
    - `created_at` string, date-time, nullable
    - `updated_at` string, date-time, nullable
    - `created_by` string, nullable
  - `at` string, required: ISO-8601 UTC timestamp
  - `createdBy` string, nullable: User ID that initiated the operation
  - `idempotentReplay` boolean: True when this envelope came from the idempotency cache — the underlying command did not execute again. False on fresh executions.

## Other responses

- `400`: Invalid request
- `401`: Authentication required
- `403`: Access denied
- `404`: Resource not found
- `409`: Idempotency-Key conflict — key reused with different body
- `422`: Validation error
- `429`: Rate limit exceeded
- `500`: Internal server error

---

[API](https://skmtc.dev/robosystems/apis/robosystems-api.md) · [All operations](https://skmtc.dev/robosystems/apis/robosystems-api/llms.txt) · [OpenAPI document](https://skmtc.dev/robosystems/apis/robosystems-api/revisions/e661e2d5ca6a?raw)
