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
contactis your user (external_idand/oremail).accountis your company or tenant (external_id, optionaldomain/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 aservice.<key>.triggeredevent 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.