Event names are entirely free-form. There is no schema to register and no reserved vocabulary — send
first_deposit and it exists.Sending your first event
Ingestion is authenticated with a secret key, so it happens server-side. The organization is derived from the key.202 Accepted:
202 means accepted for processing, not that automations have already run.
Request fields
string
required
Event name. Must match
^[a-zA-Z0-9_.:-]{1,64}$ and is lowercased on arrival, so First_Deposit and first_deposit are the same event.object
required
Who the event belongs to. Must contain at least one of
email, walletAddress, or externalId.string
Valid email, max 320 characters.
string
Max 128 characters. Lowercased on arrival, so casing never causes a mismatch.
string
Your own user ID. Max 128 characters, case preserved.
object
Contact traits merged onto the contact record. Max 50 keys; values must be string, number, or boolean; string values max 1024 bytes.
object
Event-specific data, kept with the event rather than the contact. Max 16 KB serialized. Nested objects allowed.
string
Your dedup key, max 256 characters. Strongly recommended — see Idempotency.
string
ISO 8601 timestamp of when the event happened. Defaults to now. Rejected if older than 7 days or more than 5 minutes in the future.
boolean
Dry run — see Testing safely.
attributes vs payload
This distinction matters more than it looks:
Put things that describe the person in
attributes, and things that describe the event in payload.
Batch ingestion
Send up to 100 events in one request:Identity resolution
Each event is matched to a contact in strict priority order:1
Wallet address
Case-insensitive match on
walletAddress.2
Matched through a blind index, so encrypted email still resolves.
3
External ID
Matched against
externalId stored in contact metadata.attributes you send are merged into the new contact’s metadata, and externalId is backfilled onto existing contacts that lack one.
Because the first matching identifier wins, sending both
walletAddress and email resolves on the wallet. To merge an email onto an existing wallet contact, send both — the wallet finds the record, and the email is stored on it.Idempotency
Every event gets an idempotency key, scoped to your organization. A repeat of the same key is accepted but not reprocessed, and returnsdeduplicated: true.
When you don’t supply one, it’s derived by hashing the event name, all three identifiers, and occurredAt.
Testing safely
Two ways to dry-run:- Send with a
sk_test_*key — everything from that key is a dry run automatically. - Send
"test": truewith a live key.
app_events SQL table, so they never contaminate a segment or report.
Triggering automations
Custom events are a first-class automation trigger. Use trigger typeapp_event (aliases custom_event, app-event, custom-event, appevent are all accepted).
Filter paths
Filters read a flat context where yourpayload keys are spread in at the top level. So the path is amountUsd, not payload.amountUsd:
event, contactId, walletAddress, email, externalId. A payload key with one of those names overrides the built-in value, so avoid naming a payload field email.
Operators are eq, neq, in, contains, and exists — the same set used by on-chain triggers, and all filters must pass. See Triggers and Conditions for the details, including the note that unrecognized operators are silently dropped.
End-to-end: notify a user from an offchain event
This is the most common reason to send custom events — something happens in your product, and you want the user to see an in-app notification in your dApp. Here’s the whole path, and how to wire it up. There is no direct event-to-push shortcut — an automation is always the bridge. That’s a feature: it means the same event can fan out to in-app, email, tags, and webhooks without your backend knowing about any of them.1
Add the in-app SDK to your dApp
Follow the in-app notifications quickstart. Two lines authenticate the wallet and open the socket:
2
Build an automation: app_event trigger → send_inapp
Create an automation whose trigger is your event and whose only action sends an in-app push. See Create an Automation.
recipientMode: "triggering_profile" sends to the contact the event resolved to — you don’t specify a wallet on the node.3
Activate AND publish it
Both are required. An active-but-unpublished automation matches your event and creates an entry, but runs an empty graph and sends nothing. See the publish step.
4
Fire the event from your backend
The wallet is the key
The in-app SDK authenticates by wallet, so the push can only reach a wallet. This is the one requirement that catches people out: The wallet is taken from the event’scontact.walletAddress if present, otherwise from the matched contact record. To also reach walletless contacts, branch the automation to a send_email step — see Connect to Campaigns.
Offchain vs onchain triggers
Both drive the same actions, includingsend_inapp. The difference is where the event comes from:
Reach for
app_event when the thing that happened lives in your systems. Reach for onchain_event when it’s something the chain already knows — see Contract-Based Flows.
Querying events in SQL
Events land in theapp_events table in the Intelligence SQL editor:
occurred_at for time windows — it’s when the event happened in your system. created_at is when we received it.
Discovering event names
GET /api/v1/events/catalog returns the distinct event names seen in the last 30 days with counts, ordered by frequency. This powers autocomplete in the builder and is useful for spotting typos in event names.
Limits and errors
Ingestion itself is free and not metered against your plan — only messages sent as a result are.
429 responses do not currently include a Retry-After header. Use exponential backoff, and keep the same idempotencyKey across retries so a late success doesn’t double-fire.Things to know
Events are retained indefinitely
Events are retained indefinitely
There is currently no automatic pruning of custom events. Plan for the table to grow, and prefer
payload fields you’ll actually query.The rate limit is per instance
The rate limit is per instance
The 50/sec bucket is held in memory per API instance, so effective throughput scales with the number of running instances. Treat 50/sec as the safe floor rather than a hard ceiling.
Unknown top-level fields are dropped silently
Unknown top-level fields are dropped silently
Fields outside the documented set are stripped rather than rejected. If data seems to be missing, check that it’s nested inside
attributes or payload rather than sitting at the top level.Put the trigger node first in the builder
Put the trigger node first in the builder
Custom-event trigger nodes aren’t yet recognized by the builder’s trigger-node detection, which falls back to the first node in the graph. Keep your
app_event trigger as the first node so run inspection labels steps correctly.
