Integrate once. Two calls in the hot path.
Send events as they happen, and ask a question when you need the answer. Plain HTTP and JSON, an OpenAPI specification for every API, and errors your code can branch on.
- A call arrives
your API - Send the event
POST /v1/events - Ask
…:evaluate - Respond
answer in hand
# 1. Something happened: send the event
curl -X POST "$P/events" \
-H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{"id":"call_8812","type":"api_call",
"ts":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
"attrs":{"account_id":"acme","key_id":"key_42"}}'
# 2. Before you serve the next call: ask
curl -X POST \
"$P/entities/account/acme/profiles/usage_check:evaluate" \
-H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{"context":{"api_key":"key_42"}}'// 1. Something happened: send the event.
attrs := map[string]any{"account_id": "acme", "key_id": "key_42"}
_, err := c.Ingest.IngestEventWithResponse(ctx, nil, common.Event{
Id: "call_8812", Type: "api_call", Ts: time.Now().UTC(), Attrs: &attrs,
})
// 2. Before you serve the next call: ask.
ids := map[string]string{"api_key": "key_42"}
res, err := c.Query.EvaluateProfileWithResponse(ctx,
"account", "acme", "usage_check",
query.EvaluateRequest{Context: &ids})
if err == nil && res.JSON200 != nil &&
slices.Contains(res.JSON200.Reasons, "near_monthly_quota") {
// warn the account, slow it down, or offer more
}{
"entity": {"type": "account", "id": "acme"},
"values": {
"callsThisMinute": 412,
"callsThisMonth": 1180340,
"keyCallsThisMinute": 37,
"quotaUsedPct": 94
},
"reasons": ["near_monthly_quota"],
"degraded": [],
"skipped": [],
"profileVersion": "usage_check@v3",
"configVersion": 12,
"evaluatedAt": "2026-11-02T09:30:00.698155402Z"
}From nothing to a pushed result in four steps
A small example: flag a customer with 3 or more failed logins in 15 minutes, and tell your systems when it happens.
- Define
Say what to keep and what to ask, in the workspace with a form or its JSON editor, or import definitions kept in your repository. Self-hosted, the admin API takes the same JSON from your pipelines.
- Send an event
Post each event as it happens, one at a time or up to 500 in a batch. Each one counts once, even when you retry.
- Ask a question
Ask about any customer inside your own request. The answer lists the values, the conditions that hold and anything it could not compute.
- Receive a webhook
Add your https endpoint in the workspace. Results arrive as signed deliveries, retried until your side answers.
# $ADMIN: the admin API. $ADMIN_KEY: a key with admin.write
# Where the customer's id is in the event
curl -X POST "$ADMIN/event-types" \
-H "Authorization: Bearer $ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"login_failed",
"spec":{"entities":{"customer":"attrs.customer_id"}}}'
# What to keep: failed logins per customer, in 1-minute buckets
curl -X POST "$ADMIN/states" \
-H "Authorization: Bearer $ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"failed_logins",
"spec":{"on":"login_failed","key":["customer"],
"op":"count","resolution":"1m","retention":"24h"}}'
# What to ask, and when to ask it by itself
curl -X POST "$ADMIN/profiles" \
-H "Authorization: Bearer $ADMIN_KEY" \
-H 'Content-Type: application/json' \
-d '{"name":"login_check",
"spec":{"entity":"customer",
"features":{"failedLogins15m":{"state":"failed_logins",
"window":{"rolling":"15m"}}},
"reasons":[{"name":"too_many_failures",
"when":"failedLogins15m >= 3"}],
"triggers":[{"name":"on_failure","on":"login_failed",
"outputs":["stream"]}]}}'# $P: your workspace's public API. $KEY: a key with the ingest scope
# Three failed logins for one customer, each with its own id
for n in 1 2 3; do
curl -X POST "$P/events" \
-H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' \
-d '{"id":"evt_00'"$n"'","type":"login_failed",
"ts":"'"$(date -u +%Y-%m-%dT%H:%M:%SZ)"'",
"attrs":{"customer_id":"c_1001"}}'
done
# 200 each time: counted. Send one again and it comes back "duplicate"
# {"id": "evt_003", "status": "accepted", "states": 1}curl -X POST \
"$P/entities/customer/c_1001/profiles/login_check:evaluate" \
-H "Authorization: Bearer $KEY" \
-H 'Content-Type: application/json' -d '{}'
# 200: the answer, with the versions that gave it
# {
# "entity": {"type": "customer", "id": "c_1001"},
# "values": {"failedLogins15m": 3},
# "reasons": ["too_many_failures"],
# "degraded": [],
# "skipped": [],
# "profileVersion": "login_check@v1",
# "configVersion": 4,
# "evaluatedAt": "2026-11-02T09:30:00.286144752Z"
# }# Aggrelis calls your endpoint, signed to the Standard Webhooks spec
POST https://ops.example.com/hooks/aggrelis
webhook-id: d7c1e9a0-52f4-4b8e-9c1a-6f0e2b7d3a15
webhook-timestamp: 1793611801
webhook-signature: v1,K5oZfzN9…=
{
"type": "trigger.result",
"timestamp": "2026-11-02T09:30:01.469783324Z",
"data": {
"trigger": "on_failure",
"profile": "login_check",
"status": "ok",
"entity": {"type": "customer", "id": "c_1001"},
"values": {"failedLogins15m": 3},
"reasons": ["too_many_failures"],
"event": {"id": "evt_003", "type": "login_failed",
"ts": "2026-11-02T09:30:00Z"},
"profileVersion": "login_check@v1"
}
}
# Verify the signature over the raw body, then answer 2xx quicklyFive APIs, each with one job
Your services talk to the public address. Each API has its own scopes, so a credential gets only the job it needs.
- Ingest
Send events, one or a batch, synchronous or queued.
POST /v1/events:batch - Entities
Facts about anything you track, and the links between them.
PUT /v1/entities/{type}/{id} - Query
Ask questions, explain an answer, read metrics, follow the result stream.
POST …/profiles/{name}:evaluate - Decide
Run an automated decision and get its answer, with a record of how it got there.
POST …/decisions/{name}:decide
- Admin
Definitions and their versions, jobs, webhooks, keys and the audit log: the same things you manage in the workspace. Self-hosted, call it from your pipelines to apply definitions kept in a repository.
admin.read · admin.write - OpenAPI and a Go client
An OpenAPI specification per API, checked in the engine's build, so clients for any language can be generated. The Go client covers every API, with retries built in.
openapi-generator · oapi-codegen
Credentials that fit how you run
Give each service its own credential with only the scopes it needs. Use a key where nothing else works, or short-lived tokens where you can, so no long-lived secret travels on the wire.
- Scoped API keys: shown once, stored hashed, expiring after 90 days by default, revoked within seconds, recognisable by secret scanners
- Machine clients: a key pair whose private half never leaves your service, signed in for short-lived tokens (RFC 7523)
- Workload identity: a Kubernetes, CI or cloud token exchanged for a 5-minute access token (RFC 8693), with no secret to manage at all
- Self-hosted, tokens from your own OAuth 2.0 or OpenID Connect provider, such as Keycloak, Auth0 or Microsoft Entra ID
- Least privilege: a service that only sends events cannot read answers or change configuration
- API key
Services that cannot do OAuth
aek_<id>_<secret> - Machine client
No long-lived secret on the wire
key pair → short-lived token - Workload identity
Kubernetes, CI and cloud workloads
platform token → 5 min token - Your OAuth providerSelf-hosted
Tokens from the provider you run
JWT · agg:ingest
scopes: ingestPOST /v1/events200 acceptedPOST …/login_check:evaluate403 insufficient_scopeGET /v1/results403 insufficient_scope
A key or token can grant only scopes it holds itself.
Every request carries one bearer credential; the workspace comes from the credential, never from the request.
What every call can rely on
The rules are the same across the APIs, so the edge cases are handled once in your client, not per endpoint.
- Idempotent events
Each event id is remembered for at least 24 hours. Retry after a timeout and the event is answered as a duplicate, never counted twice.
{"status": "duplicate"} - Partial batches
A batch is never all or nothing. Good events apply; each bad one comes back with a stable reason you can fix and resend.
"accepted": 498, "rejected": 2 - Async ingest, with backpressure
For high volume, queue events and get 202 at once. When the engine falls behind it answers 503 with Retry-After instead of losing data.
POST /v1/events?mode=async → 202 - ETags
Every change to a definition or webhook names the version it read, so two changes can never overwrite each other.
If-Match → 412 precondition_failed - Problem Details
Every error is an RFC 9457 document with a stable code to branch on and a request id that matches the server's logs.
"code": "rate_limited" - Bounded answers
Each question has a time budget: 20 ms by default, from 1 ms to 1 s. What is not ready in time is listed, not waited for.
"skipped": [{"reason": "deadline_exceeded"}]
Built on what you already run
Open standards wherever one exists, so Aggrelis connects to your tools and your libraries already know how to talk to it.
- OpenAPI
A specification per API, to read or generate clients from.
- OAuth 2.0 and OpenID Connect
Short-lived tokens for machine clients and workloads, with JWT profile grants and token exchange.
- Standard Webhooks
Signed deliveries that any Standard Webhooks library verifies.
- RFC 9457
Problem Details for every error, with stable codes.
- CEL
The Common Expression Language, for conditions, filters and trigger gates.
- Prometheus
Self-hosted: metrics, a dashboard and alert rules with runbooks.
- OpenTelemetry
Self-hosted: traces and logs to your own collector.
The bounds you will meet
What a client meets in practice. Most are fixed by the engine; request rates and a few counts follow your plan.
| Events per batch | 500A batch is one request against the rate limit |
|---|---|
| Request body | 1 MiB for one event, 5 MiB for a batchAttributes up to 32 KiB per event |
| Entities per event | 16One event can update the customer's, the card's and the device's numbers at once |
| Event time | Up to 35 days late, 5 minutes aheadEvents count in the minute they happened |
| Retries recognised | At least 24 hoursThe same event id is answered as a duplicate |
| Time budget per question | 1 ms to 1 s, 20 ms by default |
| Result stream | Up to 500 results per page, long-polling up to 20 sKeeps about 100,000 results for up to 7 days |
| Webhooks | Up to 20 per workspaceYour plan sets how many; https and public addresses only |
| API keys | Expire after 90 days by default |
| Request rates | Basic, Standard or Premium, by planEach API has its own rate and burst. Over it, you get 429 with Retry-After |
Monthly event and question volumes and the number of keys and webhooks are set per plan. Compare plans
Read the docs, then send your first event
Concepts, guides and every endpoint, with examples taken from the running engine.
Ready for a workspace of your own? Request access