Events

Your server tells Aimdoc what happened (POST /api/v1/events). Event triggers and automations react. You can also run one service for one user without going through the event bus.

Events are free. Only the service runs they cause consume credits. Authenticate with the same API key as the rest of the External API.

Emit an event

curl -X POST https://api.aimdoc.ai/api/v1/events \
  -H "Authorization: Bearer $AIMDOC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "trial.expiring",
    "contact": {"external_id": "u_123", "email": "jason@acme.com"},
    "account": {"external_id": "acct_9", "domain": "acme.com", "name": "Acme Inc."},
    "properties": {"days_left": 3},
    "occurred_at": "2026-08-22T14:00:00Z",
    "idempotency_key": "trial.expiring:u_123:2026-08-22"
  }'

The first time you send a name it appears on Services → Events, where you can describe it and point automations or event triggers at it. Names are yours (trial.expiring). The aimdoc. namespace is reserved.

Send {"events": [...]} with up to 100 events; the response carries per-event results.

Subjects

  • contact is your user (external_id and/or email).
  • account is your company or tenant (external_id, optional domain / name).
  • Unknown contacts and accounts are created.
  • An event with neither is a broadcast — it is stored, but it cannot run a service that needs a user.

contact.external_id is required for a service to run as that person. Your API key is the identity authority, the same way a signed identify is. Email-only contacts stay as journey records until they carry an external_id. Sending both contact.external_id and account.external_id on one event also records that the user belongs to that account.

What happens next

A committed event can start services two ways. The same event can feed any number of each. Every firing is idempotent per event and rule.

Event triggers (on a service's Event triggers tab, on Services → Events, or via the MCP server) are service-level: whenever trial.expiring fires, run this service for whoever the event names. No per-contact setup. Access is still decided per firing:

  • an Included service runs for every contact the event names
  • a gated service (request / manual / external) runs only for contacts who already have invoke access — everyone else is a visible skipped count, with no credits spent

A service can be live for event triggers while appearing in no catalog — set Discover to deny. That is the shape for internal features.

Event automations (Services → Automations, trigger type event) are the per-contact form: one specific contact, explicit input bindings. Use them for standing work on one account; use event triggers for product behavior.

Blocked firings of either kind record a visible reason (access, missing input, cooldown) instead of spending anything. Pause a trigger or automation to stop matching events without deleting the rule.

Inputs

For event triggers, properties on the event whose name and type match a declared service input are passed through. Explicit per-input overrides on the trigger win when set. A firing missing a required input is skipped, not run — it shows on the trigger's skipped counter with the input's name.

Keep required inputs present in the events you emit, give them defaults on the service, or bind them on the trigger. Optional guards (cooldown_seconds, max_per_day) apply per contact.

For event automations, event properties never become inputs unless you bind them. Bind each input to a fixed value or to a field on the event.

Idempotency and dry run

Pass idempotency_key on every write so retries are safe. Choose a key that encodes the business fact, not the attempt — for example trial.expiring:${user.id}:${today}. A repeat returns deduplicated: true and stores nothing.

Add ?dry_run=true while integrating: full validation and subject resolution, nothing stored, nothing triggered.

const idempotencyKey = `trial.expiring:${user.id}:${today}`

await fetch('https://api.aimdoc.ai/api/v1/events', {
  method: 'POST',
  headers: {
    Authorization: `Bearer ${process.env.AIMDOC_API_KEY}`,
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    name: 'trial.expiring',
    contact: { external_id: user.id, email: user.email },
    properties: { days_left: daysLeft },
    idempotency_key: idempotencyKey,
  }),
})

Trigger a service

Run one service for one user without emitting a product event:

curl -X POST https://api.aimdoc.ai/api/v1/services/weekly-account-report/trigger \
  -H "Authorization: Bearer $AIMDOC_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contact": {"external_id": "u_123"},
    "inputs": {"focus": "usage"},
    "mode": "invoke",
    "idempotency_key": "weekly-report:u_123:2026-08-24"
  }'

contact.external_id is required. The call follows ordinary invocation: invoke access is authorized for that user, and inputs validate against the published input schema. 403 with invoke_denied means that user may not run that service.

  • mode: "invoke" (default) runs the service once and returns the run.
  • mode: "event" stores a service.<key>.triggered event instead, so that user's event automations can react. One request never does both.

Repeat the same idempotency_key to get the original run back (invoke) or deduplicated: true (event).

To render the catalog in your app instead of calling from the server, see Services catalog.

Native Aimdoc events

Aimdoc also records what happens inside the product (new session, contact captured, meeting booked, service run completed, and the rest of the webhook catalog). Those names appear on Services → Events with the aimdoc. prefix. You can subscribe services to them the same way you subscribe to your own names.

You cannot emit names in the aimdoc. namespace from your API key.

Webhooks out

To be notified when things happen inside Aimdoc (a service run completing, native product events), create webhook subscriptions under Integrations → Webhooks. Treat deliveries as at-least-once — the event id is your dedup key.

See the Webhooks guide and the Webhooks reference.

Was this page helpful?