Connect a custom-built store with the API
For a store that is not WooCommerce or Shopify. Send events, contacts, consent, orders and products from your own server, with a signed request.
Where it is in the app
Open Home, choose Settings, then General › API keys.
- Home
- Settings
- General
- API keys
If your store is your own code — a Next.js app, a Rails app, anything that is not WooCommerce or Shopify — this is how you connect it. Your server sends us what happens in the store: page views, carts, checkouts, orders, sign-ups. We store it against the right person, and your flows run from it. Abandoned cart, after-they-order and the rest work the same as they do for a WooCommerce shop.
You need two things:
- An Amino Engine account.
- A server key, made in the app. The next section shows where.
Every example on this page is a whole request. Each one is shown twice: once with curl, once with Node's built-in fetch.
#Get a key
Go to Settings → General → API keys.
On screenSettings › General › API keysPress Create key.
Give it a name, such as "Store", and pick when it expires.
Press Create.
The key starts with ae_live_. It is shown once. Copy it before you leave the page. We only keep a fingerprint of it, so if you lose it, make a new one.
Keep it on your server, in an environment variable. Never put it in a web page or in anything the browser downloads. In Next.js that means a server-only variable (not one starting with NEXT_PUBLIC_), used from a route handler or a server action.
The examples below read the key from AE_KEY:
export AE_KEY='ae_live_PASTE-YOUR-KEY-HERE'#Sign every request
A key made on that screen only works on a signed request. The key alone is not enough. Every request carries three headers:
| Header | Value |
|---|---|
Authorization |
Bearer followed by your key |
X-AE-Timestamp |
The time now, in Unix seconds. Milliseconds also work. |
X-AE-Signature |
sha256= followed by the hex HMAC-SHA256 of timestamp + "." + body, using your key as the secret |
Three rules:
- Sign the exact bytes you send. Turn your JSON into a string once, sign that string, and send that same string. If you sign one string and send another, the signature fails.
- A request with no body (a GET) signs the timestamp followed by a dot and nothing else.
- The timestamp must be within 5 minutes of our clock. Keep your server's clock synced.
This small Node helper (Node 18 or newer) does all of it. The rest of this page uses it.
import crypto from 'node:crypto'
const KEY = process.env.AE_KEY
const BASE = 'https://app.aminoengine.com'
export async function ae(method, path, body) {
const raw = body === undefined ? '' : JSON.stringify(body)
const ts = String(Math.floor(Date.now() / 1000))
const sig = 'sha256=' + crypto
.createHmac('sha256', KEY)
.update(`${ts}.${raw}`)
.digest('hex')
const res = await fetch(BASE + path, {
method,
headers: {
Authorization: `Bearer ${KEY}`,
'X-AE-Timestamp': ts,
'X-AE-Signature': sig,
...(raw ? { 'Content-Type': 'application/json' } : {}),
},
body: raw || undefined,
})
return { status: res.status, json: await res.json() }
}#Check the key works
GET /api/v1/ping changes nothing. It is the right first request.
TS=$(date +%s)
SIG=$(printf '%s' "$TS." \
| openssl dgst -sha256 -hmac "$AE_KEY" -hex | sed 's/^.* //')
curl -sS https://app.aminoengine.com/api/v1/ping \
-H "Authorization: Bearer $AE_KEY" \
-H "X-AE-Timestamp: $TS" \
-H "X-AE-Signature: sha256=$SIG"const { status, json } = await ae('GET', '/api/v1/ping')A good answer is 200 with "ok": true, your brand's name, and "signingRequired": true.
#What a bad signature looks like
A missing signature, a wrong signature and an old timestamp all get the same answer. It does not mention the signature, so learn to recognise it:
If you see it and the key is right, check the signature and your server's clock first.
#Optional: tie the key to your site
You may send one more header, X-AE-Site-URL, with your store's address, such as https://yourstore.com.
- Without it, everything on this page works.
- The first value you ever send is kept for good. After that, a request from the same key with a different site address is refused (
403, or409on/ping). Requests with no header still work. - Binding a site is what lets the browser snippet on that site work without errors. See "Browser tracking" below.
- To move to a new domain, make a new key.
So: do not send this header from staging with your live key. Use a separate key for staging, or leave the header off there.
#Send events
Everything that happens in your store goes to one address: POST /api/v1/events.
The body is a source (any name for your app) and a list of events:
BODY='{
"source":"my-store",
"events":[
{
"name":"product_viewed",
"email":"buyer@example.com",
"occurredAt":"2026-09-24T16:50:00Z",
"dedupeKey":"pv-LUMO-1-buyer",
"payload":{
"product_id":"LUMO-1",
"name":"Lumo Vial",
"url":"https://yourstore.com/p/lumo-1",
"price":4900,
"in_stock":true
}
}
]
}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" \
| openssl dgst -sha256 -hmac "$AE_KEY" -hex | sed 's/^.* //')
curl -sS https://app.aminoengine.com/api/v1/events \
-H "Authorization: Bearer $AE_KEY" \
-H "Content-Type: application/json" \
-H "X-AE-Timestamp: $TS" \
-H "X-AE-Signature: sha256=$SIG" \
--data "$BODY"await ae('POST', '/api/v1/events', {
source: 'my-store',
events: [{
name: 'product_viewed',
email: 'buyer@example.com',
occurredAt: new Date().toISOString(),
dedupeKey: 'pv-LUMO-1-buyer',
payload: {
product_id: 'LUMO-1',
name: 'Lumo Vial',
url: 'https://yourstore.com/p/lumo-1',
price: 4900,
in_stock: true,
},
}],
})#Each event
| Field | What it is |
|---|---|
name |
Required. One of the standard names below, or your own. |
email or contactId |
Who it was. Use one of these to tie the event to a person. |
anonymousId |
A visitor id you choose, 8 to 200 characters, for someone you do not know yet. |
occurredAt |
When it happened, as an ISO date. A date we cannot read is rejected. |
dedupeKey |
Recommended. Send the same key twice and the second is ignored. |
payload |
An object, up to 32 KB. Keys we do not know are kept. |
#The answer
Once the key and signature are good, a batch always answers 200, with one result per event, in the same order:
{
"received": 1,
"results": [
{
"index": 0,
"status": "accepted",
"eventId": "8c53…",
"contactId": "d653…",
"deduplicated": false
}
]
}Each result's status is one of:
accepted— stored and tied to a person.anonymous— stored against theanonymousId, no person yet.duplicate— we already had it.rejected— not stored. Areasonsays why.
#Limits
- Up to 100 events per request, and a body up to 1 MB. Send 101 and you get
413with "101 events, over the 100 limit. Split the batch and send it again." Split it and send again. - 600 requests a minute and 6,000 events a minute per brand.
- Over a limit you get
429with aRetry-Afterheader. Wait that many seconds, then carry on.
#Event reference
There are sixteen standard names. Use them exactly as written — we store them as sent, and the ready-made flows listen for them.
| Event | Send it when |
|---|---|
page_visited |
Someone opens a page. |
product_viewed |
Someone opens a product. |
collection_viewed |
Someone opens a category or collection. |
search_submitted |
Someone searches. |
added_to_cart |
Something goes into the cart. |
cart_updated |
The cart changes. |
cart_emptied |
The cart is emptied. |
checkout_started |
Someone reaches checkout. |
order_created |
An order is placed. |
order_paid |
It is paid. |
order_fulfilled |
It ships. |
order_cancelled |
It is cancelled. |
order_refunded |
It is refunded. |
customer_created |
A customer account is made. |
signed_up |
Someone signs up with a form. |
form_submitted |
Someone sends any other form. |
#Money is in whole cents
Every amount is a whole number of cents (or the smallest unit of your currency), with a currency. $49.00 is 4900. Do not send 49.00.
#Orders are de-duplicated by order id
For the order events, we keep one of each event per order_id. Send the same order_created twice and the second is absorbed, whatever dedupeKey you send. That makes a retry safe.
#Examples
Each example below is one item in the events list. Add email (or anonymousId for a visitor you do not know yet), occurredAt and dedupeKey as described above.
Most events can also carry an attribution object in the payload: page_url, referrer, utm (source, medium, campaign, term, content, id), anon_id and tz.
page_visited
{
"name": "page_visited",
"email": "buyer@example.com",
"payload": {
"attribution": {"page_url": "https://yourstore.com/", "referrer": null}
}
}product_viewed
{
"name": "product_viewed",
"email": "buyer@example.com",
"payload": {
"product_id": "LUMO-1",
"sku": "LUMO-BPC-5",
"name": "Lumo Vial",
"url": "https://yourstore.com/p/lumo-1",
"price": 4900,
"in_stock": true
}
}collection_viewed
{
"name": "collection_viewed",
"email": "buyer@example.com",
"payload": {
"collection": "peptides",
"attribution": {"page_url": "https://yourstore.com/c/peptides"}
}
}search_submitted
{
"name": "search_submitted",
"email": "buyer@example.com",
"payload": {"query": "bpc"}
}added_to_cart — the whole cart, plus the item just added in added.
{
"name": "added_to_cart",
"email": "buyer@example.com",
"payload": {
"cart_token": "cart-123",
"items": [
{
"product_id": "LUMO-1",
"sku": "LUMO-BPC-5",
"name": "Lumo Vial",
"quantity": 2,
"unit_price": 4900,
"line_total": 9800,
"url": "https://yourstore.com/p/lumo-1",
"image_url": "https://yourstore.com/i.png"
}
],
"item_count": 2,
"money": {
"currency": "USD",
"subtotal": 9800,
"discount": 0,
"shipping": 500,
"tax": 0,
"total": 10300
},
"recovery_url": "https://yourstore.com/cart?r=1",
"added": {
"product_id": "LUMO-1",
"sku": "LUMO-BPC-5",
"name": "Lumo Vial",
"quantity": 2,
"unit_price": 4900,
"line_total": 9800
}
}
}cart_updated — the whole cart as it is now.
{
"name": "cart_updated",
"email": "buyer@example.com",
"payload": {
"cart_token": "cart-123",
"items": [
{
"product_id": "LUMO-1",
"name": "Lumo Vial",
"quantity": 2,
"unit_price": 4900,
"line_total": 9800
}
],
"item_count": 2,
"money": {"currency": "USD", "subtotal": 9800, "total": 10300},
"recovery_url": "https://yourstore.com/cart?r=1",
"cart_url": "https://yourstore.com/cart",
"checkout_url": "https://yourstore.com/checkout"
}
}cart_emptied
{
"name": "cart_emptied",
"email": "buyer@example.com",
"payload": {"cart_token": "cart-123", "previous_item_count": 2}
}checkout_started — the cart, plus a checkout id.
{
"name": "checkout_started",
"email": "buyer@example.com",
"payload": {
"cart_token": "cart-123",
"checkout_token": "co-123",
"email_captured": true,
"items": [
{
"product_id": "LUMO-1",
"name": "Lumo Vial",
"quantity": 2,
"unit_price": 4900,
"line_total": 9800
}
],
"item_count": 2,
"money": {"currency": "USD", "subtotal": 9800, "total": 10300},
"recovery_url": "https://yourstore.com/cart?r=1"
}
}order_created — the full order.
{
"name": "order_created",
"email": "buyer@example.com",
"payload": {
"order_id": "LUMO-1001",
"order_number": "1001",
"status": "processing",
"placed_at": "2026-09-24T16:50:00Z",
"items": [
{
"product_id": "LUMO-1",
"sku": "LUMO-BPC-5",
"name": "Lumo Vial",
"quantity": 2,
"unit_price": 4900,
"line_total": 9800
}
],
"item_count": 2,
"money": {
"currency": "USD",
"subtotal": 9800,
"discount": 0,
"shipping": 500,
"tax": 0,
"total": 10300
}
}
}order_paid
{
"name": "order_paid",
"email": "buyer@example.com",
"payload": {
"order_id": "LUMO-1001",
"order_number": "1001",
"status": "processing",
"placed_at": "2026-09-24T16:50:00Z",
"items": [],
"item_count": 2,
"money": {"currency": "USD", "total": 10300}
}
}order_fulfilled — the order, plus the shipment.
{
"name": "order_fulfilled",
"email": "buyer@example.com",
"payload": {
"order_id": "LUMO-1001",
"order_number": "1001",
"status": "completed",
"placed_at": "2026-09-24T16:50:00Z",
"items": [],
"item_count": 2,
"money": {"currency": "USD", "total": 10300},
"fulfillment_id": "F1",
"tracking_numbers": ["1Z999"]
}
}order_cancelled
{
"name": "order_cancelled",
"email": "buyer@example.com",
"payload": {
"order_id": "LUMO-1001",
"order_number": "1001",
"status": "cancelled",
"placed_at": "2026-09-24T16:50:00Z",
"items": [],
"item_count": 2,
"money": {"currency": "USD", "total": 10300}
}
}order_refunded — the order, plus the refund, in cents.
{
"name": "order_refunded",
"email": "buyer@example.com",
"payload": {
"order_id": "LUMO-1001",
"order_number": "1001",
"status": "refunded",
"placed_at": "2026-09-24T16:50:00Z",
"items": [],
"item_count": 2,
"money": {"currency": "USD", "total": 10300},
"refund_id": "R1",
"refund_amount": 10300,
"is_partial_refund": false
}
}customer_created
{
"name": "customer_created",
"email": "buyer@example.com",
"payload": {
"customer_id": "cust-123",
"accepts_marketing": false,
"source": "nextjs_signup"
}
}signed_up — a consent block here is ignored. See "Contacts and consent".
{
"name": "signed_up",
"email": "buyer@example.com",
"payload": {"form_id": "footer", "placement": "footer"}
}form_submitted
{
"name": "form_submitted",
"email": "buyer@example.com",
"payload": {"form_id": "contact-us", "fields": {"message": "hi"}}
}In the short order examples above, items is left empty to save space. Send the real items.
#Your own events
You can send any name you like, such as quiz_completed. It is stored as sent, and it shows up by itself in the list of things that can start a flow. Nothing to set up.
#Contacts and consent
#Add or update people
POST /api/v1/contacts makes a person, or updates one we already have. Up to 100 per request. attributes are merged with what we already hold, not replaced.
BODY='{
"contacts":[
{
"email":"buyer@example.com",
"firstName":"Sam",
"lastName":"Lee",
"phone":"+15555550123",
"attributes":{"plan":"gold"},
"source":"my-store"
}
]
}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" \
| openssl dgst -sha256 -hmac "$AE_KEY" -hex | sed 's/^.* //')
curl -sS https://app.aminoengine.com/api/v1/contacts \
-H "Authorization: Bearer $AE_KEY" \
-H "Content-Type: application/json" \
-H "X-AE-Timestamp: $TS" \
-H "X-AE-Signature: sha256=$SIG" \
--data "$BODY"await ae('POST', '/api/v1/contacts', {
contacts: [{
email: 'buyer@example.com',
firstName: 'Sam',
lastName: 'Lee',
phone: '+15555550123',
attributes: { plan: 'gold' },
source: 'my-store',
}],
})Each result is upserted (with created: true or false) or rejected with a reason, such as an address that is not a usable email.
#Link a visitor to a person
If you tracked someone with an anonymousId before you knew who they were, POST /api/v1/identify joins the two. It never writes consent.
BODY='{
"anonymousId":"visitor-123456",
"email":"buyer@example.com",
"tz":"America/New_York"
}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" \
| openssl dgst -sha256 -hmac "$AE_KEY" -hex | sed 's/^.* //')
curl -sS https://app.aminoengine.com/api/v1/identify \
-H "Authorization: Bearer $AE_KEY" \
-H "Content-Type: application/json" \
-H "X-AE-Timestamp: $TS" \
-H "X-AE-Signature: sha256=$SIG" \
--data "$BODY"await ae('POST', '/api/v1/identify', {
anonymousId: 'visitor-123456',
email: 'buyer@example.com',
tz: 'America/New_York',
})#Record consent
POST /api/v1/consent is the only way to record that someone agreed to hear from you. A consent block inside a signed_up event is ignored.
Send it when someone ticks a box. Send the exact words the box showed — we keep them with the record as proof.
BODY='{
"email":"buyer@example.com",
"scope":"email_marketing",
"checkbox":{
"rendered":true,
"checked":true,
"wordingShown":"Email me news and offers."
},
"sourceKind":"checkout",
"ip":"203.0.113.9",
"userAgent":"Mozilla/5.0"
}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" \
| openssl dgst -sha256 -hmac "$AE_KEY" -hex | sed 's/^.* //')
curl -sS https://app.aminoengine.com/api/v1/consent \
-H "Authorization: Bearer $AE_KEY" \
-H "Content-Type: application/json" \
-H "X-AE-Timestamp: $TS" \
-H "X-AE-Signature: sha256=$SIG" \
--data "$BODY"await ae('POST', '/api/v1/consent', {
email: 'buyer@example.com',
scope: 'email_marketing',
checkbox: {
rendered: true,
checked: true,
wordingShown: 'Email me news and offers.',
},
sourceKind: 'checkout',
ip: '203.0.113.9',
userAgent: 'Mozilla/5.0',
})The rules:
scopeis required:email_marketing,email_transactional,sms_marketingorsms_transactional.checkboxis eithernull, which records nothing, or{ "rendered": true, "checked": true or false, "wordingShown": "the exact text" }.renderedmust betrue.- For texts, send
scope: "sms_marketing"with thephone. If you checked their age, add"dobVerified": trueand"minAge": 21. - A good answer is
{"written": "consent"}. Withcheckbox: nullit is{"written": "none"}. - If your account asks people to confirm by email first, an
email_marketingtick answers{"written": "pending"}and we send them the confirmation email. - A ticked
email_marketingbox also adds the person to your default list. If you have a welcome flow on that list, it starts.
#Product catalog
POST /api/v1/catalog adds or updates your products, so emails can show them. Up to 100 per request. externalId is required — it is your own product id.
BODY='{
"products":[
{
"externalId":"LUMO-1",
"sku":"LUMO-BPC-5",
"title":"Lumo Vial 5mg",
"url":"https://yourstore.com/p/lumo-1",
"imageUrl":"https://yourstore.com/i.png",
"price":4900,
"currency":"USD",
"published":true,
"inStock":true,
"stockQty":12
}
]
}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" \
| openssl dgst -sha256 -hmac "$AE_KEY" -hex | sed 's/^.* //')
curl -sS https://app.aminoengine.com/api/v1/catalog \
-H "Authorization: Bearer $AE_KEY" \
-H "Content-Type: application/json" \
-H "X-AE-Timestamp: $TS" \
-H "X-AE-Signature: sha256=$SIG" \
--data "$BODY"await ae('POST', '/api/v1/catalog', {
products: [{
externalId: 'LUMO-1',
sku: 'LUMO-BPC-5',
title: 'Lumo Vial 5mg',
url: 'https://yourstore.com/p/lumo-1',
imageUrl: 'https://yourstore.com/i.png',
price: 4900,
currency: 'USD',
published: true,
inStock: true,
stockQty: 12,
}],
})We also accept external_id, name, image_url, in_stock and inventory_quantity, and items in place of products.
#Import your history
To load past orders and events, add ?mode=backfill to the events address. Everything is stored, so your numbers and segments are right. No flow starts from it. Nobody gets an abandoned-cart email about a cart from last year.
BODY='{
"source":"my-store",
"events":[
{
"name":"order_created",
"email":"buyer@example.com",
"occurredAt":"2025-03-02T10:00:00Z",
"payload":{
"order_id":"LUMO-0042",
"order_number":"42",
"status":"completed",
"placed_at":"2025-03-02T10:00:00Z",
"items":[],
"item_count":1,
"money":{"currency":"USD","total":4900}
}
}
]
}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" \
| openssl dgst -sha256 -hmac "$AE_KEY" -hex | sed 's/^.* //')
curl -sS "https://app.aminoengine.com/api/v1/events?mode=backfill" \
-H "Authorization: Bearer $AE_KEY" \
-H "Content-Type: application/json" \
-H "X-AE-Timestamp: $TS" \
-H "X-AE-Signature: sha256=$SIG" \
--data "$BODY"await ae('POST', '/api/v1/events?mode=backfill', {
source: 'my-store',
events: pastOrders,
})Here pastOrders is your own list of up to 100 events, shaped exactly like the examples above.
The same 100-per-request limit applies. Backfill has its own, lower limit: 60 requests a minute.
Send real occurredAt dates. An event dated before a flow was switched on never starts that flow, even without backfill mode.
#Send one email
POST /api/v1/send sends a single email — an order confirmation, a shipping notice. You need a verified sending domain first; without one the answer is 422 with no_verified_domain.
Required: to and subject, plus the email's html. Add text for a plain version. idempotencyKey stops a retry sending it twice.
BODY='{
"to":{"email":"buyer@example.com","name":"Sam Lee"},
"subject":"Your order is confirmed",
"html":"Your order LUMO-1001 is confirmed.",
"text":"Your order LUMO-1001 is confirmed.",
"idempotencyKey":"LUMO-1001:confirmed"
}'
TS=$(date +%s)
SIG=$(printf '%s' "$TS.$BODY" \
| openssl dgst -sha256 -hmac "$AE_KEY" -hex | sed 's/^.* //')
curl -sS https://app.aminoengine.com/api/v1/send \
-H "Authorization: Bearer $AE_KEY" \
-H "Content-Type: application/json" \
-H "X-AE-Timestamp: $TS" \
-H "X-AE-Signature: sha256=$SIG" \
--data "$BODY"await ae('POST', '/api/v1/send', {
to: { email: 'buyer@example.com', name: 'Sam Lee' },
subject: 'Your order is confirmed',
html: 'Your order LUMO-1001 is confirmed.',
text: 'Your order LUMO-1001 is confirmed.',
idempotencyKey: 'LUMO-1001:confirmed',
})The answer:
{"messageId": "25c9…", "state": "queued", "deduplicated": false}To see what happened to it, ask for the message by id — GET /api/v1/messages/{messageId}, signed like any GET:
await ae('GET', `/api/v1/messages/${messageId}`)It answers with its state (such as sent) and a timeline of what happened. Forgetting the subject gets 422 with no_subject.
There is more on this in "Send a receipt from your own code".
#Browser tracking
Our browser script tracks page views, product views and carts from the visitor's browser, including page changes in a single-page app like Next.js. It uses a separate public key that starts with ae_pub_. A public key can only report browsing. It cannot name a person by email, write consent or create contacts. It is meant to sit in your page source, so it is not a secret.
#Get the tag
Go to Settings → General → API keys.
On screenSettings › General › API keysUnder Browser tracking, type your site's address in Your site's address, such as
https://yourstore.com.Press Create browser key.
Under "Paste into the
On screen"Paste into the `<head › ` of every page"<head>of every page", press Copy.Paste it into the
<head>of every page. In Next.js, that is your root layout.
An owner or admin of the account can do this. The tag you copy looks like this, with your own key in it:
<script data-cfasync="false">window.ae=window.ae||function(){(window.ae.q=window.ae.q||[]).push(arguments)};</script>
<script async data-cfasync="false" src="https://app.aminoengine.com/ae.js" data-key="ae_pub_YOUR-KEY"></script>Copy it from the screen rather than from here. If your own tracking address is set up, the screen's tag loads the script from there instead.
The site address is what stops the browser console showing a red CORS error on every event. You can leave it empty to get the tag first, and add or change it later on the same card with Save. Without it, events are still stored, but the error is noisy and the script sends each one a second time. Subdomains of the address also work.
#Send your own events
Once the tag is on the page, send an event from your own code like this:
window.ae('track', 'product_viewed', {
product_id: 'LUMO-1',
name: 'Lumo Vial',
price: 4900,
})Things to know:
- Only these events can come from the browser:
page_visited,product_viewed,collection_viewed,search_submitted,added_to_cart,cart_updated,signed_upandidentify. Anything about an order is refused there. Send orders from your server. - Up to 20 events per post, and 120 posts a minute from one visitor.
#Check it worked
- Settings → Other → Integrations, under "What your shop is sending", lists every event name we have received from you, with counts. Your own custom names show there too.
- A person's page shows their consent under "Where their consent came from", with the wording they saw.
- An email you sent can be looked up by its
messageId(see "Send one email").
If an event is missing, read the results in the answer to the request that sent it. A rejected event always says why.
#If it didn't work
- 401 saying the key is "missing, malformed, revoked, or does not exist". Almost always the signature: sign the exact string you send, use the key as the secret, and check your server's clock is within 5 minutes. If all of that is right, make a new key.
- 403 or 409 about a different site. This key is already tied to another site address. Stop sending
X-AE-Site-URLfrom this server, or use a new key. - 413. More than 100 in one request. Split it.
- 429. Too fast. Wait the number of seconds in
Retry-After. - A flow did not start. Check the event was not sent with
?mode=backfill, that itsoccurredAtis after the flow was switched on, and that the event name matches the flow's trigger exactly.