This is the full developer documentation for Azimea # Azimea documentation > What each feature does for your business, what every event and status means, and how to connect your own systems. Use Azimea For shop owners and marketers. Each feature explained the same way: what it does, how it works, what every status means and what to do about it. [Connect your store](/use/connect-your-store/) [Recover abandoned carts](/use/recover-abandoned-carts/) [Consent and GDPR](/use/consent-and-gdpr/) Build with Azimea For developers. Send contacts and events from your product, start flows, send transactional email, receive webhooks. [What you can build today](/build/) Press `Ctrl` `K` (or `⌘` `K`) anywhere to search. Event names, statuses and error codes are found exactly as you type them: `cart.abandoned`, `gate_no_basis`, `Recovered`. # What you can build today > The public API and webhooks that exist today, and what each one is for. Everything below works with an **API key** made in **Settings › API keys**. Each key carries only the permissions (scopes) you give it. All routes start with `https://dashboard.azimea.com/api/v1`; each one links to its page in the [API reference](/build/reference/), with request and response examples. | You want to | Call | Scope | | ----------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------- | | Add or update one contact, with attributes and consent | [`POST /contacts`](/build/reference/operations/upsertcontact/) | `contacts:write` | | Add or update many contacts at once | [`POST /contacts/batch`](/build/reference/operations/upsertcontactsbatch/) | `contacts:write` | | Read contacts, segments and topics | [`GET /contacts`](/build/reference/operations/listcontacts/), [`GET /contacts/{reference}`](/build/reference/operations/getcontactbyreference/), [`GET /segments`](/build/reference/operations/listsegments/), [`GET /topics`](/build/reference/operations/listtopics/) | `contacts:read` | | Erase a person at their request (GDPR) | [`DELETE /contacts/{reference}`](/build/reference/operations/erasecontact/) | `contacts:erase` | | Tell Azimea what happened in your product (start flows) | [`POST /events`](/build/reference/operations/trackevents/) | `events:write` | | Send a transactional email | [`POST /sending/emails`](/build/reference/operations/sendemail/) | `emails:send` | | Keep your catalogue (products, plans, services) in Azimea | [`POST /items`](/build/reference/operations/upsertitem/), [`POST /items/batch`](/build/reference/operations/upsertitemsbatch/), [`DELETE /items/{externalId}`](/build/reference/operations/deleteitem/) | `items:write` | | Read one catalogue item you sent | [`GET /items/{externalId}`](/build/reference/operations/getitem/) | `items:read` | | Define your own contact fields | [`GET /contact-attributes`](/build/reference/operations/getcontactattributes/), [`POST /contact-attributes`](/build/reference/operations/createcontactattribute/), [`PATCH /contact-attributes/{key}`](/build/reference/operations/updatecontactattribute/) | `contacts:read` / `contacts:write` | | Read orders (as a list, with filters) | [`GET /commerce/orders`](/build/reference/operations/getorders/) | `orders:read` | | Hear back about deliveries, bounces, complaints, opens and clicks | [signed webhooks](/build/webhooks/) (not self-service yet) | — | A flow whose trigger is an **event** starts when you send that event’s name with the person’s email to `POST /events`. ## Downloads [Section titled “Downloads”](#downloads) * **OpenAPI document**: [`/openapi/public-v1.json`](/openapi/public-v1.json) — the whole public API, for code generators and API tools. * **Postman collection**: [`/azimea-api.postman_collection.json`](/azimea-api.postman_collection.json) — import it, then add a `bearerToken` variable holding your API key (every request uses it; `baseUrl` is already set). * **For AI assistants**: [`/llms.txt`](/llms.txt) indexes the docs; [`/llms-full.txt`](/llms-full.txt) holds all of them. Every page also exists as Markdown: add `.md` to its address, for example [`/build/authentication.md`](/build/authentication.md). # API keys, responses and limits > How to authenticate, what every answer looks like, the rate limit, and which calls are safe to retry. Every call to the public API carries an **API key**. All routes start with `https://dashboard.azimea.com/api/v1`; the [API reference](/build/reference/) lists each one. ## Make a key [Section titled “Make a key”](#make-a-key) Owners and admins make keys in **Settings › API keys**: 1. Give the key a name you will recognise later (2–100 characters), such as the system that will use it. 2. Tick only the permissions that system needs (below). 3. Choose when it expires: in 30, 90 or 365 days, or never (chosen explicitly). 4. Copy the key. **It is shown once**; afterwards the list shows only its ends (`azm_live_7f3c…5e0c`) and when it was last used. A company can have up to 10 active keys. A key stops working the moment it is revoked or expires; Azimea answers the same way for a revoked key as for one that never existed. ### Permissions [Section titled “Permissions”](#permissions) | Permission | What it allows | | ---------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- | | `contacts:read` | Read contacts, segments, topics and contact fields: `GET /contacts`, `GET /contacts/{reference}`, `GET /segments`, `GET /topics`, `GET /contact-attributes` | | `contacts:write` | Create and update contacts and contact fields: `POST /contacts`, `POST /contacts/batch`, `POST /contact-attributes`, `PATCH /contact-attributes/{key}` | | `contacts:erase` | Erase a contact for good (GDPR): `DELETE /contacts/{reference}`. Not included in any other permission | | `events:write` | Report what people did in your product: `POST /events` | | `emails:send` | Send transactional email: `POST /sending/emails` | | `items:read` | Read a catalogue item you sent: `GET /items/{externalId}` | | `items:write` | Write your catalogue: `POST /items`, `POST /items/batch`, `DELETE /items/{externalId}` | | `orders:read` | Read orders: `GET /commerce/orders` | A call the key has no permission for is answered `403`. ### Keys for testing [Section titled “Keys for testing”](#keys-for-testing) Keys made inside a **sandbox** start with `azm_test_`. They work exactly like live keys, against the sandbox’s own data. No email sent from a sandbox reaches anyone: it lands in the sandbox’s inbox, and its delivery webhooks are still sent, so the whole integration can be tried end to end. ## Send the key [Section titled “Send the key”](#send-the-key) Put it in the `Authorization` header as a bearer token, on every call: * curl ```sh curl https://dashboard.azimea.com/api/v1/segments \ -H "Authorization: Bearer $AZIMEA_API_KEY" ``` * C# ```csharp using var http = new HttpClient { BaseAddress = new Uri("https://dashboard.azimea.com/api/v1/") }; http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AZIMEA_API_KEY")); var json = await http.GetStringAsync("segments"); ``` * Node ```js const response = await fetch('https://dashboard.azimea.com/api/v1/segments', { headers: { Authorization: `Bearer ${process.env.AZIMEA_API_KEY}` }, }); const body = await response.json(); ``` * PHP ```php $ch = curl_init('https://dashboard.azimea.com/api/v1/segments'); curl_setopt_array($ch, [ CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AZIMEA_API_KEY')], CURLOPT_RETURNTRANSFER => true, ]); $body = json_decode(curl_exec($ch), true); ``` * Python ```python import os, requests response = requests.get( "https://dashboard.azimea.com/api/v1/segments", headers={"Authorization": f"Bearer {os.environ['AZIMEA_API_KEY']}"}, ) body = response.json() ``` Keep keys on your server. A key in a browser, a mobile app or a public repository can be used by anyone who finds it: revoke it and make a new one. ## What every answer looks like [Section titled “What every answer looks like”](#what-every-answer-looks-like) Every answer from the API has the same envelope: ```json { "isSuccess": false, "value": null, "status": 5, "errors": [{ "code": "audience.contact_email_required", "message": "An email address is needed to create a new contact.", "params": null }], "fieldErrors": { "email": [{ "code": "audience.contact_email_required", "message": "An email address is needed to create a new contact.", "params": null }] } } ``` * `isSuccess` and the HTTP status say whether it worked. `value` holds the result when it did. * `errors` lists what went wrong. Each error has a **`code`** — stable, meant for your code — and an English `message` for logs; `params` carries the numbers in the message (a minimum, a maximum) when there are any. Every code is in the [error codes catalog](/catalogs/error-codes/). * `fieldErrors` groups validation errors by the request field that caused them. * `status` is an internal result number; rely on the HTTP status instead. | HTTP status | Meaning | | ------------- | ----------------------------------------------------------------------------------------- | | `200` / `201` | Done; `value` holds the result | | `400` | The request is not valid: see `errors[].code` and `fieldErrors` | | `401` | No key, an unknown or revoked key, or an expired one | | `403` | The key lacks the permission this call needs | | `404` | What you asked for does not exist in your account | | `409` | It conflicts with what exists (for example, a contact field with that key already exists) | | `429` | Too many calls for this key (below) | | `503` | Azimea cannot answer right now; try again later | `401`, `403` and `429` are answered before the call reaches Azimea’s logic: rely on the status code, not on a body. ## Rate limit [Section titled “Rate limit”](#rate-limit) Each key may make **600 calls per minute**, counted together across every public endpoint, in fixed one-minute windows. Two systems with two keys never share a limit. A batch call (`POST /contacts/batch`, `POST /items/batch`, `POST /events` with up to 100 events) counts as one call, so batches are the way to move volume. Past the limit, the answer is `429 Too Many Requests` with a `Retry-After` header: the number of seconds to wait before the next call is allowed. ## Retries [Section titled “Retries”](#retries) Retry on a network error, a timeout, `429` (after `Retry-After`) or `503`. Do not retry a `400`, `401`, `403` or `404` unchanged: the same request gets the same answer. Whether a retry is safe depends on the call: | Call | Safe to retry? | | --------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `POST /events` | Yes. An event’s `id` is yours and unique in your account: a second delivery is counted as a duplicate, not recorded twice. | | `POST /contacts`, `POST /contacts/batch` | Yes. They create or update the same contact (by id, then external id, then email). | | `POST /items`, `POST /items/batch` | Yes. They create or update the item with that `externalId`. | | `PATCH /contact-attributes/{key}`, `DELETE /items/{externalId}` | Yes. Doing it twice ends in the same state. | | `POST /contact-attributes` | Yes: the second call is refused with `409`, because the field already exists. | | `POST /sending/emails` | **Only with an `Idempotency-Key` header.** With it, a second call with the same key returns the first message and sends nothing more. Without it, each call queues a new email, and a retry after a timeout can send the person the same email twice. See [send your first email](/build/send-your-first-email/#make-retries-safe). | | `GET` calls | Yes. | # Your catalogue in emails > Send your products, plans or services to Azimea so emails can recommend them. Emails can show products: best sellers, the newest additions, a category, items you choose, or a pick for each person. They come from your catalogue in Azimea. A connected shop fills it on its own; if you sell from your own system (plans, services, a custom shop), send the items through the API. Calls use `https://dashboard.azimea.com/api/v1` and an API key with `items:write` (and `items:read` to read one back). ## Send an item [Section titled “Send an item”](#send-an-item) [`POST /items`](/build/reference/operations/upsertitem/) adds an item or updates it. `id` is **your own** id: sending the same id again updates the same item, and a field you leave out keeps its value. * curl ```bash curl https://dashboard.azimea.com/api/v1/items \ -H "Authorization: Bearer $AZIMEA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "id": "plan-pro", "kind": "plan", "name": "Pro plan", "description": "Unlimited projects and priority support.", "url": "https://example.com/pricing#pro", "imageUrl": "https://example.com/img/pro.png", "price": 49, "comparePrice": 59, "currency": "EUR", "categories": ["plans"], "attributes": { "billing": "monthly", "seats": 5 } }' ``` * C# ```csharp var response = await http.PostAsJsonAsync("items", new { id = "plan-pro", kind = "plan", name = "Pro plan", url = "https://example.com/pricing#pro", imageUrl = "https://example.com/img/pro.png", price = 49m, comparePrice = 59m, currency = "EUR", categories = new[] { "plans" } }); ``` * Node ```js await fetch('https://dashboard.azimea.com/api/v1/items', { method: 'POST', headers: { Authorization: `Bearer ${process.env.AZIMEA_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ id: 'plan-pro', kind: 'plan', name: 'Pro plan', url: 'https://example.com/pricing#pro', imageUrl: 'https://example.com/img/pro.png', price: 49, comparePrice: 59, currency: 'EUR', categories: ['plans'], }), }); ``` * PHP ```php $ch = curl_init('https://dashboard.azimea.com/api/v1/items'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AZIMEA_API_KEY'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode([ 'id' => 'plan-pro', 'kind' => 'plan', 'name' => 'Pro plan', 'url' => 'https://example.com/pricing#pro', 'price' => 49, 'currency' => 'EUR', 'categories' => ['plans'], ]), ]); $result = json_decode(curl_exec($ch), true); ``` * Python ```python requests.post( "https://dashboard.azimea.com/api/v1/items", headers={"Authorization": f"Bearer {os.environ['AZIMEA_API_KEY']}"}, json={ "id": "plan-pro", "kind": "plan", "name": "Pro plan", "url": "https://example.com/pricing#pro", "price": 49, "currency": "EUR", "categories": ["plans"], }, ) ``` ### The fields [Section titled “The fields”](#the-fields) | Field | Rules | | -------------------------- | ---------------------------------------------------------------------------------------------------------------------- | | `id` | Required: your own id, at most 100 characters (`catalog.item_id_missing`). | | `name` | Required, at most 300 characters (`catalog.item_name_missing`). | | `kind` | `product`, `plan`, `service` or `other`; a new item is a `product` unless you say otherwise. | | `description` | At most 5,000 characters. | | `url`, `imageUrl` | Full addresses starting with `https://` (`catalog.item_url_invalid`), at most 2,000 characters. | | `price`, `comparePrice` | Zero or more; `comparePrice` (the price before a discount) must be higher than `price` (`catalog.item_price_invalid`). | | `currency` | The three-letter code, such as `EUR` or `RON` (`catalog.item_currency_invalid`). | | `inStock`, `stockQuantity` | A new item is in stock. `stockQuantity` is a whole number of zero or more. | | `categories` | At most 20, each a short name: they drive the “one category” and “for each person” recommendations. | | `attributes` | At most 50 fields, each a short lowercase key with a text, number or yes/no value. | | `active` | A new item is active. `false` stops recommending it but keeps its history. | The catalogue holds at most **100,000** items (`catalog.items_limit`). ## Many items at once [Section titled “Many items at once”](#many-items-at-once) [`POST /items/batch`](/build/reference/operations/upsertitemsbatch/) takes up to **1,000** items, each written or refused on its own. The answer counts the created, updated and failed items and gives one result per item, by its position in your request. More than 1,000 refuses the whole call (`catalog.batch_too_large`). A batch counts as one call for the rate limit, so a nightly sync of your whole catalogue in batches of 1,000 is the usual pattern. ## Read or remove an item [Section titled “Read or remove an item”](#read-or-remove-an-item) * [`GET /items/{externalId}`](/build/reference/operations/getitem/) reads one item you sent, by your own id. * [`DELETE /items/{externalId}`](/build/reference/operations/deleteitem/) removes it, so emails stop recommending it. To keep its history instead (for example a plan you no longer sell), send it again with `"active": false`. Both see only the items **your system sent**. Items synced from a connected shop are not found or changed here: they change in the shop and follow on their own (`catalog.item_read_only`). ## How emails use the catalogue [Section titled “How emails use the catalogue”](#how-emails-use-the-catalogue) In the email editor, a **recommendation block** shows 2 to 8 items, chosen when the email is sent: | Choice | What it shows | | --------------- | -------------------------------------------------------------------------------------------------------------------------------------- | | Best sellers | What sold most in the last 30 days. | | Newest | The latest items added to the catalogue. | | One category | The newest items of the category you choose. | | Chosen by you | The items you pick, in your order. | | For each person | Items from the categories the person bought in, never what they already bought; someone who has not bought yet sees your best sellers. | Only items that are **active and in stock** are shown, at the price they have on the day the email goes out. So keep prices, stock and `active` up to date from your system, and the emails stay right without editing them. ## Related [Section titled “Related”](#related) * [API reference: items](/build/reference/operations/upsertitem/) * [Error codes](/catalogs/error-codes/) * [Campaigns](/use/campaigns/) # Contacts and consent from your system > Create and update contacts from your own product, with custom fields, consent and topics, one at a time or in batches. Your product knows your users; Azimea needs to know them too, so flows and campaigns can reach the right people. This guide shows how to keep the two in step: who the person is, what you know about them, and what they agreed to receive. All calls below use `https://dashboard.azimea.com/api/v1` and an API key with the `contacts:write` (and, for reading, `contacts:read`) permission, sent as `Authorization: Bearer azm_live_…`. ## Create or update one contact [Section titled “Create or update one contact”](#create-or-update-one-contact) [`POST /contacts`](/build/reference/operations/upsertcontact/) creates the person or updates them: there is no separate “create” and “update”. Send your own user id as `externalId` so you never have to store Azimea’s id. * curl ```bash curl https://dashboard.azimea.com/api/v1/contacts \ -H "Authorization: Bearer $AZIMEA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "externalId": "user_4821", "email": "ana.popescu@example.com", "firstName": "Ana", "language": "ro", "attributes": { "plan": "pro", "seats": 12 } }' ``` * C# ```csharp using System.Net.Http.Headers; using System.Net.Http.Json; var http = new HttpClient { BaseAddress = new Uri("https://dashboard.azimea.com/api/v1/") }; http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AZIMEA_API_KEY")); var response = await http.PostAsJsonAsync("contacts", new { externalId = "user_4821", email = "ana.popescu@example.com", firstName = "Ana", language = "ro", attributes = new { plan = "pro", seats = 12 } }); response.EnsureSuccessStatusCode(); ``` * Node ```js const response = await fetch('https://dashboard.azimea.com/api/v1/contacts', { method: 'POST', headers: { Authorization: `Bearer ${process.env.AZIMEA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ externalId: 'user_4821', email: 'ana.popescu@example.com', firstName: 'Ana', language: 'ro', attributes: { plan: 'pro', seats: 12 }, }), }); const { isSuccess, value, errors } = await response.json(); ``` * PHP ```php $ch = curl_init('https://dashboard.azimea.com/api/v1/contacts'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('AZIMEA_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'externalId' => 'user_4821', 'email' => 'ana.popescu@example.com', 'firstName' => 'Ana', 'language' => 'ro', 'attributes' => ['plan' => 'pro', 'seats' => 12], ]), ]); $result = json_decode(curl_exec($ch), true); ``` * Python ```python import os, requests response = requests.post( "https://dashboard.azimea.com/api/v1/contacts", headers={"Authorization": f"Bearer {os.environ['AZIMEA_API_KEY']}"}, json={ "externalId": "user_4821", "email": "ana.popescu@example.com", "firstName": "Ana", "language": "ro", "attributes": {"plan": "pro", "seats": 12}, }, ) result = response.json() ``` The answer carries the contact and `created` (`true` the first time). Sending the same contact again leaves it as the first call did, so retries after a timeout are safe. ### How the person is found [Section titled “How the person is found”](#how-the-person-is-found) 1. By `id` (Azimea’s id), if you send one. 2. Otherwise by `externalId` (your user id). 3. Otherwise by `email`. 4. If none matches, the contact is created; it then needs an `email` (`audience.contact_email_required`). A user id and an email that point at two **different** contacts are refused, never merged: `409` `audience.contact_identity_conflict`. Fix the data on your side, then send again. Fields you leave out keep their value. `attributes` is a patch: a key you send is set, a key sent as `null` is removed, the others stay. ### Contacts that come from a store [Section titled “Contacts that come from a store”](#contacts-that-come-from-a-store) If the person is also a customer of a connected shop, the shop owns their email, id and consent: a call that tries to change them is refused with `audience.contact_store_managed`. One exception: an **unsubscribe is always honoured**, whatever its source. ## Read contacts [Section titled “Read contacts”](#read-contacts) | You want | Call | | -------------------------------------- | -------------------------------------------------------------------------------------------------------- | | One person, by Azimea’s id | [`GET /contacts/{id}`](/build/reference/operations/getcontactbyreference/) | | One person, by your user id | `GET /contacts/ext:user_4821` | | One person, by email | `GET /contacts/email:ana.popescu@example.com` | | Everyone, newest first, 1–200 per page | [`GET /contacts?page=1&pageSize=200`](/build/reference/operations/listcontacts/) | | The people of one segment | `GET /contacts?segment={id}`, with ids from [`GET /segments`](/build/reference/operations/listsegments/) | The `ext:` and `email:` forms find only contacts **your own systems** created (through the API or an import). A shop’s customers are found by their Azimea id, which you get from `GET /contacts` or from a webhook. ## Custom fields [Section titled “Custom fields”](#custom-fields) Anything you know about a person beyond name and email (a plan, a seat count, a renewal date) is a custom field. Define each one before sending it, so its type is fixed and a rule like “seats greater than 10” means the same thing in every segment: ```bash curl https://dashboard.azimea.com/api/v1/contact-attributes \ -H "Authorization: Bearer $AZIMEA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "key": "seats", "label": "Seats", "type": "Number" }' ``` * **Types:** `Text`, `Number`, `Boolean`, `Date`, `DateTime`, `TextList`. The key and the type never change afterwards. * **Keys:** lowercase letters, digits and `_`, starting with a letter, up to 40 characters. A built-in field’s name is refused (`audience.attribute_reserved`); an existing key gives `409` `audience.attribute_exists`. * **At most 100 fields** per company (`audience.attribute_limit_reached`). * **Rename or archive** with [`PATCH /contact-attributes/{key}`](/build/reference/operations/updatecontactattribute/); archiving hides the field from pickers and keeps every stored value. * **List** with [`GET /contact-attributes`](/build/reference/operations/getcontactattributes/) (archived fields included). A contact sent with a key you never defined is refused with `audience.attribute_unknown`, unless **Create new fields from the API** is on in **Settings › Contact fields**: then the field is created, with its type taken from the first value. Leave it off in production, so a typo in your code never becomes a new field. ## Consent [Section titled “Consent”](#consent) Every contact has a [consent status](/catalogs/consent-statuses/) that decides what Azimea may send them. From your system you set the **basis** in `consent`: | `basis` | What it means | What you must send | | -------------- | ------------------------------------------------------------ | -------------------------------------------------------------------------------------------- | | `Customer` | The person is your customer (existing-customer rules apply). | Nothing more. | | `Subscribed` | The person agreed to receive your emails. | `proof.text`: the exact words they agreed to. Without it: `audience.consent_proof_required`. | | `Unsubscribed` | The person no longer wants marketing. | Nothing more. Service emails still reach them. | ```json { "externalId": "user_4821", "email": "ana.popescu@example.com", "consent": { "basis": "Subscribed", "proof": { "text": "Send me product news and offers from Notely.", "textVersion": "2", "locale": "ro_RO", "ipAddress": "203.0.113.7", "givenAt": "2026-10-05T09:30:00Z" } } } ``` The proof is kept in the person’s consent history, which is what you show if someone asks how you got their consent. Rules worth knowing: * **Consent only moves up.** Sending `Customer` for a subscriber changes nothing. * **Resubscribing** someone who unsubscribed needs `Subscribed` **with** proof: a new agreement, not a status change. * **A bounce or a spam complaint is never lifted** from the API. * **Leaving consent out** leaves it as it is. What each status allows is in [Consent and GDPR](/use/consent-and-gdpr/). ### Topics [Section titled “Topics”](#topics) If your company sends several kinds of email (product news, tips, offers), people can turn each topic on or off. Read the keys with [`GET /topics`](/build/reference/operations/listtopics/), then send the person’s choices: ```json { "externalId": "user_4821", "topics": { "product_news": true, "offers": false } } ``` An unknown or archived topic key is refused (`topics.not_found`, `topics.archived`). ## Many contacts at once [Section titled “Many contacts at once”](#many-contacts-at-once) [`POST /contacts/batch`](/build/reference/operations/upsertcontactsbatch/) takes up to **1,000** contacts, each written exactly as `POST /contacts` would write it. One invalid contact does not stop the others: the answer lists every contact in the order you sent them, with `Created`, `Updated` or `Failed`, its id or its errors, and the totals. More than 1,000 is refused with `audience.batch_too_large`. A batch counts as one call for the rate limit. ```json { "created": 1, "updated": 0, "failed": 1, "results": [ { "index": 0, "status": "Created", "id": "01a10a3c-5e2f-7b41-9c0d-4f7a2b6e1d90", "errors": null }, { "index": 1, "status": "Failed", "id": null, "errors": [{ "code": "validation.email", "field": "Email", "params": null }] } ] } ``` ### A sync pattern that works [Section titled “A sync pattern that works”](#a-sync-pattern-that-works) 1. **Once:** send your existing users with `POST /contacts/batch`, 1,000 at a time. 2. **In real time:** call `POST /contacts` when a user signs up or changes their profile, and when they change their email preferences in your product (send `consent` and `topics`). 3. **Nightly (optional):** send the users changed that day in batches, to catch anything a real-time call missed. Because writes are idempotent, sending someone twice is harmless. ## Plan limits [Section titled “Plan limits”](#plan-limits) A new contact counts toward your plan. When the plan is full, a new contact is refused with `audience.contact_limit_reached`; updates to existing contacts keep working. ## Erasing a person (GDPR) [Section titled “Erasing a person (GDPR)”](#erasing-a-person-gdpr) When someone asks you to erase their data, erase them in Azimea too with [`DELETE /contacts/{reference}`](/build/reference/operations/erasecontact/): the same erasure as the dashboard’s. Their name, address, custom fields, tags, consent history and activity are removed, a single anonymous row stays so a later import does not bring them back, and the rest of Azimea erases its part (timeline, flow runs, messages, orders, reports). It cannot be undone, and it is recorded in your audit log. The call needs the **`contacts:erase`** permission, which no other permission includes: give it only to the key of the system that handles erasure requests, not to the one that keeps contacts in sync. ```bash curl -X DELETE "https://dashboard.azimea.com/api/v1/contacts/email:ana@example.com" \ -H "Authorization: Bearer $AZIMEA_API_KEY" ``` The answer is the erased contact’s `id`. `email:` and `ext:` find only contacts your system or an import created; a store’s customers are erased by their Azimea id. A contact already erased answers `404 audience.contact_not_found`: there is nothing left to erase. To stop marketing without erasing anyone, send `"consent": { "basis": "Unsubscribed" }` instead. ## Related [Section titled “Related”](#related) * [API reference: contacts](/build/reference/operations/upsertcontact/) * [Consent statuses](/catalogs/consent-statuses/) * [Error codes](/catalogs/error-codes/) * [Events and flows](/build/events-and-flows/) # Events that start flows > Tell Azimea what people do in your product, and let flows react with the right email at the right time. An event is something a person did in your product: signed up, finished onboarding, started a trial, upgraded. Send it to Azimea, and every published flow that starts on that event name starts for that person. This is how onboarding series, activation nudges and trial reminders work without any scheduling code on your side. Calls use `https://dashboard.azimea.com/api/v1` and an API key with the `events:write` permission. ## Send an event [Section titled “Send an event”](#send-an-event) [`POST /events`](/build/reference/operations/trackevents/) takes up to 100 events per call. * curl ```bash curl https://dashboard.azimea.com/api/v1/events \ -H "Authorization: Bearer $AZIMEA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "events": [{ "id": "evt_7f3c2a", "name": "user.signed_up", "occurredAt": "2026-10-05T09:30:00Z", "contact": { "externalId": "user_123", "email": "ana@example.com" }, "properties": { "plan": "trial", "source": "landing-page" } }] }' ``` * C# ```csharp var response = await http.PostAsJsonAsync("events", new { events = new[] { new { id = "evt_7f3c2a", name = "user.signed_up", occurredAt = DateTimeOffset.UtcNow, contact = new { externalId = "user_123", email = "ana@example.com" }, properties = new { plan = "trial", source = "landing-page" } } } }); ``` * Node ```js await fetch('https://dashboard.azimea.com/api/v1/events', { method: 'POST', headers: { Authorization: `Bearer ${process.env.AZIMEA_API_KEY}`, 'Content-Type': 'application/json' }, body: JSON.stringify({ events: [{ id: 'evt_7f3c2a', name: 'user.signed_up', occurredAt: new Date().toISOString(), contact: { externalId: 'user_123', email: 'ana@example.com' }, properties: { plan: 'trial', source: 'landing-page' }, }], }), }); ``` * PHP ```php $ch = curl_init('https://dashboard.azimea.com/api/v1/events'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true, CURLOPT_HTTPHEADER => ['Authorization: Bearer ' . getenv('AZIMEA_API_KEY'), 'Content-Type: application/json'], CURLOPT_POSTFIELDS => json_encode(['events' => [[ 'id' => 'evt_7f3c2a', 'name' => 'user.signed_up', 'occurredAt' => gmdate('c'), 'contact' => ['externalId' => 'user_123', 'email' => 'ana@example.com'], 'properties' => ['plan' => 'trial', 'source' => 'landing-page'], ]]]), ]); $result = json_decode(curl_exec($ch), true); ``` * Python ```python from datetime import datetime, timezone requests.post( "https://dashboard.azimea.com/api/v1/events", headers={"Authorization": f"Bearer {os.environ['AZIMEA_API_KEY']}"}, json={"events": [{ "id": "evt_7f3c2a", "name": "user.signed_up", "occurredAt": datetime.now(timezone.utc).isoformat(), "contact": {"externalId": "user_123", "email": "ana@example.com"}, "properties": {"plan": "trial", "source": "landing-page"}, }]}, ) ``` The answer says how many events were `accepted`, how many were `duplicates`, and which were `rejected`, each with its position in the call and the reasons (`code`, `field`). Each event is accepted or rejected on its own. ### The event [Section titled “The event”](#the-event) | Field | Rules | | ------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | Your own id for this event, unique in your account. Sending it again counts as a duplicate, so **retries are safe**. | | `name` | Lowercase letters, digits, `_` and `.`, starting with a letter, at most 64 characters. The prefixes `order.`, `cart.`, `customer.`, `subscriber.`, `contact.`, `email.` and `azimea.` are reserved for Azimea’s own events. | | `occurredAt` | When it happened: at most 30 days in the past, never in the future. | | `contact` | Who did it: the same object [`POST /contacts`](/build/contacts-and-consent/) takes, with at least `id`, `externalId` or `email` (`activity.event_contact_required`). The person is found or created from it, and the fields you send (name, attributes, consent) update them. | | `properties` | Anything about the event, as a JSON object of at most 16 KB and 3 levels deep. Flows can filter on them and emails can show them. | The checks above happen in the call itself. Finding or creating the person happens a moment later, in the background; if that fails (for example a new person with no email), the event shows as failed in **Settings › Events**, under **Recent events**, with the reason. ### Which names to use [Section titled “Which names to use”](#which-names-to-use) Any name works. If you use the [suggested product events](/catalogs/events/) (`user.signed_up`, `user.activated`, `trial.ending`, `subscription.started`), the ready-made product flows work without changes. **Settings › Events** lists every event name your account has received, with its properties and how many arrived in the last 30 days. ## Start a flow from an event [Section titled “Start a flow from an event”](#start-a-flow-from-an-event) 1. In **Flows**, create a new flow and choose **An event** as its trigger. 2. Pick the event name. Names already received are suggested; you can also type one your product has not sent yet. 3. Optional: **Only when** narrows it to events whose properties match (for example `plan` equals `pro`). Leave it empty to start for every one of these events. Filters on properties appear once the event has been received with some. 4. Add the steps (waits, conditions, emails) and **Publish**. From then on, every matching event starts the flow for the person who did it. A person already in this flow (a run still going) is not started a second time by another event; the next event after their run ends starts a new one. ### Stop when… [Section titled “Stop when…”](#stop-when) A flow can end early when the person does something else: **Stop the workflow when…** takes up to 10 events, each with an optional filter. For example: an activation series stops on `user.activated`, trial reminders stop on `subscription.started`. The run ends wherever it is, and its history says so. ### Use the event in the email [Section titled “Use the event in the email”](#use-the-event-in-the-email) An email in the flow can show what the event said: `{{event.plan}}` is replaced by the `plan` property of the event that started the run. Text, numbers and true/false are shown as sent; lists and nested objects are not. The email step lists the placeholders it can use once the event has been received. ## Recipes [Section titled “Recipes”](#recipes) **Activation nudge.** Trigger `user.signed_up`, stop when `user.activated`. Wait a day, send a tip; wait two more days, send another. People who activate on their own never get the nudges. **Trial ending.** Trigger `trial.ending` (send it a few days before the end), stop when `subscription.started`. One email with what they lose and how to keep it. **Plan upgrade thank-you.** Trigger `subscription.started` with **Only when** `plan` equals `pro`, then an email that says `{{event.plan}}`. ## Start one flow from outside, with its own token [Section titled “Start one flow from outside, with its own token”](#start-one-flow-from-outside-with-its-own-token) For a script, Zapier or n8n that should start **one specific flow** without a general API key, a flow can have an **API** trigger: 1. Create the flow with **API** as its trigger. Its editor shows the **Trigger URL**. 2. Press **Generate a token**. Copy it at once: it is shown only once (it starts with `azm_wf_`). **Generate a new token** later replaces it, and the old one stops working at that moment. 3. Publish the flow, then call the URL: ```bash curl -X POST "$TRIGGER_URL" \ -H "Authorization: Bearer azm_wf_…" \ -H "Content-Type: application/json" \ -d '{ "id": "order-10234-review", "email": "ana@example.com", "firstName": "Ana", "externalId": "user_123", "attributes": { "plan": "pro" }, "properties": { "orderNumber": "10234" } }' ``` * Only `email` or `externalId` is required. The person is found or created as [`POST /contacts`](/build/contacts-and-consent/) does it; `attributes` update them. * `consent` takes the same object as the contacts API. **Left out, the person is treated as your customer**: whoever holds the token vouches for them. * `properties` are what this call is about; conditions and emails read them as `event.*`, with the same limits as an event’s properties. * `id` (1–100 characters) is your id for this call: the same id twice starts the flow once. * The answer is the person’s `contactId` and `duplicate` (whether this `id` was already used). * Errors: `401` `automation.api_trigger_invalid` (wrong token), `automation.workflow_not_published`, `automation.trigger_call_id_invalid`, `automation.trigger_properties_invalid`. Use the API trigger for one flow and one job; use `POST /events` with an API key when your product reports what people do and several flows may react. ## Related [Section titled “Related”](#related) * [API reference: track events](/build/reference/operations/trackevents/) * [How flows work](/use/flows/) * [Events catalog](/catalogs/events/) * [Flow runs: statuses and steps](/catalogs/flow-runs/) # Send your first email > Send a transactional email from your own code in five minutes, and read what Azimea answers. A transactional email is one your own system asks for, about the person’s own account or order: an order confirmation, an access code, a password link. Azimea queues it, sends it from your domain and tells you what happened to it. ## Before you start [Section titled “Before you start”](#before-you-start) 1. **A verified sending domain.** Your emails go out from your company’s default sender, on a domain you have verified in **Settings › Sending domains** (see [sending and deliverability](/use/sending-and-deliverability/)). Without one, the email is accepted but waits in the queue — it is retried every few minutes and goes out as soon as a domain is verified. 2. **An API key with the `emails:send` permission**, made in **Settings › API keys** ([API keys, responses and limits](/build/authentication/)). To try things without reaching anyone, make the key in a sandbox: its emails land in the sandbox’s inbox. ## Send it [Section titled “Send it”](#send-it) [`POST /sending/emails`](/build/reference/operations/sendemail/) takes three fields: `to` (one address), `subject` (up to 300 characters) and `html` (up to 500,000 characters). * curl ```sh curl https://dashboard.azimea.com/api/v1/sending/emails \ -H "Authorization: Bearer $AZIMEA_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "to": "ana@example.com", "subject": "Your order 1042 is confirmed", "html": "

Hi Ana,

Thank you for your order 1042. We will let you know when it ships.

" }' ``` * C# ```csharp using System.Net.Http.Headers; using System.Net.Http.Json; using var http = new HttpClient { BaseAddress = new Uri("https://dashboard.azimea.com/api/v1/") }; http.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", Environment.GetEnvironmentVariable("AZIMEA_API_KEY")); var response = await http.PostAsJsonAsync("sending/emails", new { to = "ana@example.com", subject = "Your order 1042 is confirmed", html = "

Hi Ana,

Thank you for your order 1042. We will let you know when it ships.

" }); var body = await response.Content.ReadAsStringAsync(); ``` * Node ```js const response = await fetch('https://dashboard.azimea.com/api/v1/sending/emails', { method: 'POST', headers: { Authorization: `Bearer ${process.env.AZIMEA_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ to: 'ana@example.com', subject: 'Your order 1042 is confirmed', html: '

Hi Ana,

Thank you for your order 1042. We will let you know when it ships.

', }), }); const body = await response.json(); ``` * PHP ```php $ch = curl_init('https://dashboard.azimea.com/api/v1/sending/emails'); curl_setopt_array($ch, [ CURLOPT_POST => true, CURLOPT_HTTPHEADER => [ 'Authorization: Bearer ' . getenv('AZIMEA_API_KEY'), 'Content-Type: application/json', ], CURLOPT_POSTFIELDS => json_encode([ 'to' => 'ana@example.com', 'subject' => 'Your order 1042 is confirmed', 'html' => '

Hi Ana,

Thank you for your order 1042. We will let you know when it ships.

', ]), CURLOPT_RETURNTRANSFER => true, ]); $body = json_decode(curl_exec($ch), true); ``` * Python ```python import os, requests response = requests.post( "https://dashboard.azimea.com/api/v1/sending/emails", headers={"Authorization": f"Bearer {os.environ['AZIMEA_API_KEY']}"}, json={ "to": "ana@example.com", "subject": "Your order 1042 is confirmed", "html": "

Hi Ana,

Thank you for your order 1042. We will let you know when it ships.

", }, ) body = response.json() ``` ## Read the answer [Section titled “Read the answer”](#read-the-answer) ```json { "isSuccess": true, "value": { "id": "01a10900-0000-7000-8000-000000000301", "status": "Queued", "reason": null }, "status": 0, "errors": [], "fieldErrors": null } ``` * **`id`** is the email’s id. Keep it: the [delivery webhooks](/build/webhooks/) carry it as `message_id`. * **`status`** is `Queued` — accepted, it goes out within seconds — or `Suppressed`: stopped before sending. * **`reason`** says why an email was stopped. It starts with the reason’s code (`sending.gate_bounced`), followed by detail separated by `|`: read it up to the first `|`. A transactional email needs no marketing consent, so the sending gate stops it only for an address that no longer works or that complained: | `reason` starts with | What it means | What you do | | ------------------------- | --------------------------------------------- | --------------------------------------------- | | `sending.gate_bounced` | The address permanently refused your emails. | Ask the person for another address. | | `sending.gate_complained` | The person marked one of your emails as spam. | Nothing: Azimea will not write to them again. | After it is queued, the email moves through the [email statuses](/catalogs/email-statuses/): `Sent`, then `Delivered` or `Bounced`. ## Transactional, not marketing [Section titled “Transactional, not marketing”](#transactional-not-marketing) Use this call only for messages about the person’s own account or order. Newsletters, offers and reminders to buy are marketing: send them as [campaigns](/use/campaigns/) or [flows](/use/flows/), which check each person’s consent before sending. A transactional email carries no unsubscribe footer, because there is nothing to unsubscribe from; its links and opens are tracked like any other email’s. ## Make retries safe [Section titled “Make retries safe”](#make-retries-safe) Send an `Idempotency-Key` header with your own id for this email (an order number, a UUID): up to 150 visible ASCII characters, no spaces. If the call times out and you send it again with the same key, Azimea returns the first message and sends nothing more. The key holds for your whole account, whichever API key sends it, and a second call with the same key returns the first message even if its body differs. ```bash curl https://dashboard.azimea.com/api/v1/sending/emails \ -H "Authorization: Bearer $AZIMEA_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: order-1001-confirmation" \ -d '{"to": "ana@example.com", "subject": "Your order 1001", "html": "

Thanks for your order.

"}' ``` Without the header, each call queues a new email, and a retry after a timeout can send it twice. More in [retries](/build/authentication/#retries). ## Next [Section titled “Next”](#next) * Hear back about delivery, bounces and clicks: [webhooks](/build/webhooks/). * Every field and answer: [`POST /sending/emails` in the reference](/build/reference/operations/sendemail/). # Webhooks > The notifications Azimea sends to your server about your emails, and how to verify that they come from Azimea. A webhook is an address on your server that Azimea calls when something happens to one of your emails: it was delivered, it bounced, the person opened it or clicked a link. Each notification is signed, so your server can prove it comes from Azimea before acting on it. ## Add a webhook [Section titled “Add a webhook”](#add-a-webhook) Only the owner and admins of the company can do this. 1. In the dashboard, open **Settings › API keys** and scroll to **Webhooks**. 2. Click **Add webhook**. 3. In **Address**, enter the URL on your server that receives the notifications. It must start with `https://`. 4. Under **Events**, tick the events you want. Leave all of them unticked to receive every event, including the ones added later. 5. Click **Add webhook**. The signing secret (it starts with `whsec_`) appears once, above the list: copy it now and keep it with your server’s secrets. Azimea never shows it again; if you lose it, delete the webhook and add it again. 6. Click **Send test** next to the new webhook. Azimea sends a `webhook.test` notification right away and tells you whether your server answered with a `2xx`. A company can have up to 20 active webhooks. To stop one, click **Delete**: notifications stop at once, including those still waiting to be retried. ## The events [Section titled “The events”](#the-events) | Event | When it is sent | | ------------------ | ------------------------------------------------------------------------------ | | `email.sent` | The email left Azimea for the sending server. It does not mean it arrived yet. | | `email.delivered` | The recipient’s mail server received it. | | `email.bounced` | The recipient’s mail server refused it for good. `data.reason` says why. | | `email.complained` | The person marked it as spam. | | `email.opened` | The email was opened for the first time. | | `email.clicked` | A link in it was clicked for the first time. `data.url` is the link. | | `webhook.test` | A test, asked for to check that your address answers. | A webhook can listen to some of these events or, with an empty list, to all of them. **Each event is sent at most once per email**: the first open and the first click, not every one. An open or a click may come from a mail privacy feature or a link scanner rather than a person. ## What arrives [Section titled “What arrives”](#what-arrives) A `POST` with a JSON body and three headers: ```http POST /azimea HTTP/1.1 Content-Type: application/json webhook-id: 01a10912-5b3c-7d41-9e2f-6b8c1d0a4f77 webhook-timestamp: 1791198664 webhook-signature: v1,K5oZfzN95Z9UVu1EsfQmfVNQhnkZ2pj9o9NDN/H/pI4= {"id":"01a10912-5b3c-7d41-9e2f-6b8c1d0a4f77","type":"email.delivered","created_at":"2026-10-05T09:31:04.1180000+00:00","data":{"message_id":"01a10900-0000-7000-8000-000000000301","email":"ana@example.com"}} ``` | Field | What it is | | ----------------- | ------------------------------------------------------------------------------------------------------------------------------------- | | `id` | The notification’s id, the same as the `webhook-id` header. Use it to ignore a notification you already handled. | | `type` | The event, from the table above. | | `created_at` | When it happened, ISO 8601 in UTC. | | `data.message_id` | The email: the `id` you got back from [`POST /sending/emails`](/build/send-your-first-email/), or the id of a campaign or flow email. | | `data.email` | The recipient’s address. | | `data.contact_id` | The contact in Azimea, when the email went to a contact (campaigns and flows). | | `data.source_id` | The campaign or flow that sent it, when there is one. | | `data.url` | `email.clicked` only: the link. | | `data.reason` | `email.bounced`, `email.complained` and `webhook.test`: the detail Azimea received. | Fields without a value for the event are left out, not sent empty. The names are in `snake_case`. ## Answer quickly [Section titled “Answer quickly”](#answer-quickly) Answer with any `2xx` status within **10 seconds**, then do the work. Anything else — another status, a timeout, a connection that cannot be made — counts as a failure, and the notification is sent again with the **same body and the same `webhook-id`**: | Attempt | When | | ------- | -------------------------------- | | 1 | Right away | | 2 | 1 minute after the first failure | | 3 | 5 minutes later | | 4 | 15 minutes later | | 5 | 1 hour later | | 6 | 3 hours later, the last one | After 15 failed deliveries in a row, the webhook is stopped: the dashboard shows it as **Stopped**, with the reason. Fix your server, then delete the webhook and add it again. Redirects are not followed, and addresses on private networks are refused: give the final public `https` address. Because of retries, a notification can arrive more than once: keep the `webhook-id`s you handled and skip repeats. Notifications about the same email are not guaranteed to arrive in order. ## Verify the signature [Section titled “Verify the signature”](#verify-the-signature) The scheme is [Standard Webhooks](https://www.standardwebhooks.com/), so any Standard Webhooks library verifies it with your secret. By hand: 1. Read the **raw body**, exactly as it arrived — before any JSON parsing, which would change spaces and order. 2. Refuse the notification if `webhook-timestamp` (Unix seconds) is more than **5 minutes** away from now, in the past or the future: it is a replay. 3. Your secret looks like `whsec_` followed by base64. The signing key is the part after `whsec_`, **decoded from base64**. 4. Compute HMAC-SHA256 with that key over `{webhook-id}.{webhook-timestamp}.{raw body}`, and encode it in base64. 5. `webhook-signature` holds one or more signatures separated by spaces, each written `v1,`. Accept the notification if one of them equals yours, compared in constant time. * C# ```csharp using System.Security.Cryptography; using System.Text; static bool IsFromAzimea(string secret, string id, string timestamp, string signatures, string rawBody) { if (!long.TryParse(timestamp, out var sentAt) || Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - sentAt) > 300) { return false; } var key = Convert.FromBase64String(secret.StartsWith("whsec_") ? secret["whsec_".Length..] : secret); var expected = HMACSHA256.HashData(key, Encoding.UTF8.GetBytes($"{id}.{timestamp}.{rawBody}")); foreach (var candidate in signatures.Split(' ', StringSplitOptions.RemoveEmptyEntries)) { if (!candidate.StartsWith("v1,")) { continue; } var given = new byte[candidate.Length]; if (Convert.TryFromBase64String(candidate[3..], given, out var written) && CryptographicOperations.FixedTimeEquals(expected, given.AsSpan(0, written))) { return true; } } return false; } // ASP.NET Core: read the body as text before anything parses it. app.MapPost("/azimea", async (HttpRequest request) => { var rawBody = await new StreamReader(request.Body).ReadToEndAsync(); var valid = IsFromAzimea( Environment.GetEnvironmentVariable("AZIMEA_WEBHOOK_SECRET")!, request.Headers["webhook-id"].ToString(), request.Headers["webhook-timestamp"].ToString(), request.Headers["webhook-signature"].ToString(), rawBody); return valid ? Results.Ok() : Results.Unauthorized(); }); ``` * Node ```js import crypto from 'node:crypto'; import express from 'express'; function isFromAzimea(secret, id, timestamp, signatures, rawBody) { if (!/^\d+$/.test(timestamp) || Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false; const key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64'); const expected = crypto.createHmac('sha256', key).update(`${id}.${timestamp}.${rawBody}`).digest(); return signatures.split(' ').some((candidate) => { if (!candidate.startsWith('v1,')) return false; const given = Buffer.from(candidate.slice(3), 'base64'); return given.length === expected.length && crypto.timingSafeEqual(given, expected); }); } const app = express(); // express.raw keeps the body as it arrived: express.json() would parse it first. app.post('/azimea', express.raw({ type: 'application/json' }), (req, res) => { const valid = isFromAzimea( process.env.AZIMEA_WEBHOOK_SECRET, req.header('webhook-id') ?? '', req.header('webhook-timestamp') ?? '', req.header('webhook-signature') ?? '', req.body.toString('utf8'), ); if (!valid) return res.sendStatus(401); const event = JSON.parse(req.body.toString('utf8')); res.sendStatus(200); // handle event.type here, after answering }); ``` * PHP ```php function is_from_azimea(string $secret, string $id, string $timestamp, string $signatures, string $rawBody): bool { if (!ctype_digit($timestamp) || abs(time() - (int) $timestamp) > 300) { return false; } $key = base64_decode(preg_replace('/^whsec_/', '', $secret), true); if ($key === false) { return false; } $expected = hash_hmac('sha256', "{$id}.{$timestamp}.{$rawBody}", $key, true); foreach (explode(' ', $signatures) as $candidate) { if (str_starts_with($candidate, 'v1,')) { $given = base64_decode(substr($candidate, 3), true); if ($given !== false && hash_equals($expected, $given)) { return true; } } } return false; } $rawBody = file_get_contents('php://input'); $valid = is_from_azimea( getenv('AZIMEA_WEBHOOK_SECRET'), $_SERVER['HTTP_WEBHOOK_ID'] ?? '', $_SERVER['HTTP_WEBHOOK_TIMESTAMP'] ?? '', $_SERVER['HTTP_WEBHOOK_SIGNATURE'] ?? '', $rawBody ); http_response_code($valid ? 200 : 401); ``` * Python ```python import base64, hashlib, hmac, os, time from flask import Flask, request def is_from_azimea(secret: str, msg_id: str, timestamp: str, signatures: str, raw_body: bytes) -> bool: if not timestamp.isdigit() or abs(time.time() - int(timestamp)) > 300: return False key = base64.b64decode(secret.removeprefix("whsec_")) expected = hmac.new(key, f"{msg_id}.{timestamp}.".encode() + raw_body, hashlib.sha256).digest() for candidate in signatures.split(" "): if candidate.startswith("v1,"): try: given = base64.b64decode(candidate[3:], validate=True) except ValueError: continue if hmac.compare_digest(expected, given): return True return False app = Flask(__name__) @app.post("/azimea") def azimea(): valid = is_from_azimea( os.environ["AZIMEA_WEBHOOK_SECRET"], request.headers.get("webhook-id", ""), request.headers.get("webhook-timestamp", ""), request.headers.get("webhook-signature", ""), request.get_data(), # the raw body, before any parsing ) return ("", 200) if valid else ("", 401) ``` The secret is shown once, when the webhook is created. Keep it with your other secrets, never in client-side code. ## Test notifications [Section titled “Test notifications”](#test-notifications) When you click **Send test**, Azimea sends it a `webhook.test` notification, signed like any other, to confirm that the address answers and that your verification accepts it. It carries a made-up `message_id` and the address `proba@azimea.com`: answer `2xx` and otherwise ignore it. ## From a sandbox [Section titled “From a sandbox”](#from-a-sandbox) Emails sent from a sandbox never reach anyone, but they are marked sent and delivered, and their `email.sent` and `email.delivered` notifications are sent like real ones: an integration can be tested end to end with an `azm_test_` key. # About the catalogs > Every event, status and refusal reason Azimea uses, with what it means and what to do. The catalogs list every event, status and reason Azimea uses, each with one meaning, when it happens and what to do about it. They are generated from Azimea’s code each time the docs are built: a new status appears here with the release that adds it, and the build stops if its explanation has not been written. * [Why an email was not sent](/catalogs/why-an-email-was-not-sent/) * [Consent statuses](/catalogs/consent-statuses/) * [Carts and orders](/catalogs/carts-and-orders/) * [Email statuses](/catalogs/email-statuses/) * [Flow runs](/catalogs/flow-runs/) * [Events](/catalogs/events/) * [Error codes](/catalogs/error-codes/) # Carts and orders > The statuses of carts and orders, the same for every shop platform. ## Carts [Section titled “Carts”](#carts) | Status or reason | What it means | What you do | | ---------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | `Open` | Someone is filling the cart, or left it less than 30 minutes ago. | Nothing. | | `Abandoned` | Left with products and an address for 30 minutes without becoming an order. The abandoned-cart flow starts here. | Make sure the Abandoned cart flow is published. | | `Recovered` | It became an order. It counts in your recovered revenue and stops the reminders. | Nothing. | | `Cleared` | The person emptied it. | Nothing: nobody is reminded of an empty cart. | ## Orders [Section titled “Orders”](#orders) Every platform has its own words for an order’s state; Azimea translates them into these, so flows, segments and reports read the same whatever your shop runs on. | Status or reason | What it means | What you do | | ---------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | `Pending` | Placed, but not paid yet: waiting for a bank transfer, cash on delivery, or a payment still being confirmed. | Nothing: flows that start on an order start when it is placed; the buyer becomes a paying customer when it is Paid. | | `Paid` | The payment came in. The buyer becomes a paying customer in Azimea. | Nothing. | | `Completed` | Shipped or delivered: the shop has finished the order. | Nothing. | | `Cancelled` | Cancelled by the shop or the buyer, or the shipment was returned. | Nothing: it no longer counts in revenue. | | `Refunded` | The money was given back. | Nothing: it no longer counts in revenue. | | `Failed` | The payment failed. | Nothing on Azimea's side; the shop decides whether to ask the buyer again. | | `Draft` | An order still being made at checkout, before the buyer confirmed it. | Nothing: it is not counted as an order until it is placed. | # Consent statuses > What each contact's consent status means and what it lets you send. Every contact has one consent status. It decides what Azimea may send them, and every change is kept in the contact’s consent history with its proof. | Status or reason | What it means | What you do | | --------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `Unknown` | Azimea has no basis to send this person marketing: imported or added without a consent. | Only service emails reach them. To send more, collect consent (a form, a checkbox) and import it with its proof. | | `SoftOptIn` | An existing customer, in a shop whose checkout tells buyers it may write to them about similar products. | Campaigns and flows reach them once your consent method is declared as notice at checkout. Every email carries an unsubscribe link. | | `Subscribed` | They ticked a box or signed up to hear from you; the proof is kept in their consent history. | Everything reaches them, once you have declared how you collect consent (Settings › Consent). | | `CheckoutStarted` | They typed their address while placing an order and did nothing more: no purchase yet, no tick. | Only the reminder about that cart, and service emails. They become a subscriber if they tick the newsletter box. | | `PendingConfirmation` | They asked to subscribe and have not clicked the confirmation link yet (double opt-in). | Nothing to do: the confirmation email is on its way. Cart reminders still reach them; marketing waits for the click. | | `Unsubscribed` | They said they do not want your marketing emails. | Nothing: only service emails about their own orders or account still reach them. | | `Bounced` | Their address permanently refused emails: it no longer exists or no longer accepts them. | Nothing: Azimea stops writing to it. A new, working address can be added as a new contact. | | `Complained` | They marked one of your emails as spam. | Nothing: they never receive anything again, because each complaint hurts the reputation of every email you send. | # Email statuses > What happens to an email after Azimea queues it, step by step. An email moves from `Queued` to `Sent` and then to `Delivered` (or `Bounced`). `Suppressed` means it never left: see [why an email was not sent](/catalogs/why-an-email-was-not-sent/). | Status or reason | What it means | What you do | | ---------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `Queued` | Accepted by Azimea and waiting its turn to go out. | Nothing. | | `Sent` | The sending server took it from Azimea. | Nothing: Delivered or Bounced follows. | | `Failed` | It could not be sent and Azimea is not retrying; the reason is written on the message. | Read the reason on the message; contact support if it repeats. | | `Suppressed` | Stopped before sending by the sending gate; the reason is kept on the message. | See "Why an email was not sent" for the reason and what to do. | | `Delivered` | The recipient's mail server received it. | Nothing. | | `Bounced` | The recipient's mail server refused it after it was sent. | A permanent refusal stops future emails to that address; see the contact's status. | | `HandingOff` | Being handed to the sending server right now. | Nothing: it moves on within seconds. | | `Accepted` | Azimea's sending server confirmed it holds the email in its queue. | Nothing: Delivered or Bounced follows. | | `Unknown` | It left Azimea, but no confirmation came back in the expected time. | Nothing: a late confirmation still moves it to its real status. | # Error codes > Every error code Azimea answers with, and the sentence the dashboard shows for it. API answers carry a code, not a sentence: `{"code": "stores.not_found", "message": "…"}`. The dashboard translates the code into the reader’s language. This list is generated from Azimea’s code on every docs build. ## Activity | Code | What the dashboard says | | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------- | | `activity.batch_too_large` | Send at most {{max}} events in one call. | | `activity.conversion_currency_property_invalid` | The currency must come from one of the event's text properties. | | `activity.conversion_limit_reached` | You already count {{max}} event types as conversions. Turn one off to add another. | | `activity.conversion_not_allowed` | This event type's conversion setting can't be changed: your store's events are fixed, and a paid order always counts. | | `activity.conversion_value_property_invalid` | The value must come from one of the event's number properties. | | `activity.event_contact_required` | Say who did it: send the contact’s user id or email address. | | `activity.event_id_required` | Every event needs an id, unique in your account, so a retry is safe. | | `activity.event_id_too_long` | The event id can be at most {{max}} characters. | | `activity.event_in_future` | The event’s time is in the future. Check the sender’s clock. | | `activity.event_name_invalid` | Event names use lowercase letters, digits, “\_” and “.”, and start with a letter. | | `activity.event_name_reserved` | Names starting with “{{prefix}}” are reserved by Azimea. | | `activity.event_properties_invalid` | Properties must be an object of at most {{maxBytes}} bytes and {{maxDepth}} levels. | | `activity.event_too_old` | Events older than {{days}} days aren’t accepted. | | `activity.event_type_not_found` | No event called “{{name}}” has been received yet. | ## Ai | Code | What the dashboard says | | ------------------------ | ------------------------------------------------------------------------------------------ | | `ai.credits_exhausted` | You have used your AI credits for this period. They renew next month. | | `ai.failed` | The AI help did not answer right now. Try again in a moment. | | `ai.image_refused` | This description cannot be turned into a picture. Try describing something else. | | `ai.monthly_cap_reached` | The AI help is paused for your account until next month. Contact us if you need it sooner. | | `ai.not_in_plan` | This AI feature is available from the {{plan}} plan. | | `ai.unavailable` | The AI help is not available right now. | | `ai.usage_limit_reached` | You have used this {{max}} times this month. It is available again next month. | ## Analytics | Code | What the dashboard says | | -------------------------------------- | --------------------------------------------------------- | | `analytics.attribution_window_invalid` | Choose 1, 3, 5, 7, 14 or 30 days. | | `analytics.report_range_invalid` | Pick a start and an end date, at most {{max}} days apart. | | `analytics.report_tab_unknown` | There is no such report to export. | ## Audience | Code | What the dashboard says | | ----------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------- | | `audience.attribute_exists` | A contact field called “{{key}}” already exists. | | `audience.attribute_key_invalid` | Use lowercase letters, digits and underscores for the field key, starting with a letter (at most {{max}} characters). | | `audience.attribute_limit_reached` | You’ve reached the limit of {{max}} contact fields. Archive one you no longer use. | | `audience.attribute_not_found` | There is no contact field called “{{key}}”. | | `audience.attribute_reserved` | “{{key}}” is already a built-in contact field. Choose another name. | | `audience.attribute_type_mismatch` | The value for “{{key}}” doesn’t match the field’s type. | | `audience.attribute_unknown` | There is no contact field called “{{key}}”. Create it first. | | `audience.attribute_value_too_large` | The value is too large for this field (at most {{max}}). | | `audience.batch_too_large` | Send at most {{max}} contacts in one call. | | `audience.consent_double_opt_in_not_applicable` | Double opt-in only applies when people opt in with a checkbox or your own form. | | `audience.consent_not_configured` | Choose how you collect consent first, then declare it. | | `audience.consent_proof_required` | A subscription needs its proof: the exact text the person agreed to. | | `audience.contact_email_required` | An email address is needed to create a new contact. | | `audience.contact_identity_conflict` | The user id and the email address belong to two different contacts. | | `audience.contact_limit_reached` | You have {{count}} active contacts (people you can email) and your plan includes {{limit}}. Remove contacts you no longer need, or move to a larger plan. | | `audience.contact_not_found` | That contact does not exist. | | `audience.contact_reference_invalid` | Refer to a contact by its id, ext:\ or email:\
. | | `audience.contact_reference_missing` | Send the contact’s id, your own user id or an email address. | | `audience.contact_shape_invalid` | The contact isn’t in the expected format: use the same object as POST /contacts. | | `audience.contact_store_managed` | This contact comes from a store: its email, id and consent are kept in sync by the store. | | `audience.demo_already_added` | This sandbox already has its demo contacts. Delete the contacts tagged "demo" to add them again. | | `audience.demo_only_in_sandbox` | Demo contacts can only be added inside a sandbox. | | `audience.export_selection_too_large` | Too many contacts selected: at most {{max}} at a time. | | `audience.export_too_large` | The export has more than {{max}} contacts. Narrow the list with a filter. | | `audience.file_email_column_missing` | Choose which column holds the email address. | | `audience.file_empty` | The file has no rows to import. | | `audience.file_too_large` | This file is larger than {{max}} MB. Split it, or save it as CSV, which is much smaller. | | `audience.file_too_many_rows` | This file has too many rows. Split it into files of up to {{max}} rows. | | `audience.file_unsupported` | Upload a CSV or Excel (.xlsx) file. | | `audience.import_attestation_required` | Confirm how these people agreed to hear from you before importing them with a consent basis. | | `audience.import_column_invalid` | A column is mapped twice, or to a column the file doesn’t have. | | `audience.import_email_duplicate` | This address already appears earlier in the file. | | `audience.import_email_invalid` | This row has no valid email address. | | `audience.import_file_missing` | The uploaded file is no longer available. Upload it again. | | `audience.import_in_progress` | Another import is still running. Start this one when it finishes. | | `audience.import_interrupted` | The import stopped because of an error on our side. The rows saved before it stopped are kept; upload the file again to finish. | | `audience.import_not_found` | Import not found. | | `audience.segment_built_in` | A built-in segment can’t be deleted. You can rename it or change its conditions. | | `audience.segment_empty` | Add at least one condition. | | `audience.segment_in_use` | A campaign or a flow still uses this segment. Give it another audience first. | | `audience.segment_name_taken` | A segment with this name already exists. | | `audience.segment_not_found` | This segment no longer exists. | | `audience.segment_rule_invalid` | Condition {{index}} isn’t complete: check its field, its condition and its value. | | `audience.segment_text_invalid` | Describe the contacts in a few words, up to {{max}} characters. | | `audience.segment_text_unclear` | We could not turn this into conditions. Try saying it with the fields you see in the list. | | `audience.segment_too_many_rules` | A segment can have at most {{max}} conditions. | | `audience.store_disconnected` | The store was disconnected. | | `audience.store_not_active` | The store is not active. Check the connection before importing. | | `audience.store_responded` | The store responded with an error ({{status}}). | ## Audit | Code | What the dashboard says | | ------------------------------ | --------------------------------------------- | | `audit.range_end_before_start` | The end of the range must be after its start. | ## Automation | Code | What the dashboard says | | --------------------------------------- | ----------------------------------------------------------------------------------- | | `automation.api_trigger_invalid` | This trigger token is not valid. | | `automation.instance_not_found` | That run does not exist. | | `automation.instance_not_retryable` | Only a run that has failed can be retried. | | `automation.no_draft` | There is no draft to publish or discard. | | `automation.nothing_changed` | Nothing has changed since the live version. | | `automation.run_already_pending` | This workflow is already starting. Wait until it finishes, then run it again. | | `automation.starter_code_invalid` | This is not one of the starter emails. | | `automation.step_test_not_found` | This email step is not testing versions. | | `automation.step_version_unknown` | That version is not one of this step’s. | | `automation.test_contact_missing` | Choose an existing contact or describe a test contact. | | `automation.trigger_call_id_invalid` | The call id must be between 1 and 100 characters. | | `automation.trigger_properties_invalid` | Properties must be an object of at most {{maxBytes}} bytes and {{maxDepth}} levels. | | `automation.version_not_found` | That version of the workflow no longer exists. | | `automation.workflow_archived` | Restore this workflow before making changes to it. | | `automation.workflow_graph_invalid` | This workflow has problems to fix before it can be published. | | `automation.workflow_name_invalid` | Give the workflow a name of up to {{max}} characters. | | `automation.workflow_name_taken` | A workflow with this name already exists. | | `automation.workflow_not_found` | That workflow does not exist. | | `automation.workflow_not_published` | Publish this workflow with this trigger before using it. | ## Billing | Code | What the dashboard says | | --------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------- | | `billing.account_unavailable` | The payment details could not be loaded from Stripe. Try again in a moment. | | `billing.already_on_plan` | You are already on this plan. | | `billing.checkout_failed` | The payment page could not be opened. Try again. | | `billing.no_stripe_customer` | There is no billing history yet. Choose a plan first. | | `billing.not_configured` | Payments are not set up on this server. | | `billing.not_in_sandbox` | A sandbox uses your live company’s plan. Change the plan from the live company. | | `billing.not_on_trial` | This company is not on a trial. | | `billing.payment_method_is_default` | This is the payment method your subscription is charged on. Make another one the default before removing it. | | `billing.payment_method_not_found` | This payment method is not on your account. | | `billing.payment_method_update_failed` | Stripe did not accept the change. Try again. | | `billing.payments_unreachable` | We could not reach the payment service. Try again in a moment. | | `billing.plan_price_missing` | The {{plan}} plan does not have a price set up yet. | | `billing.portal_failed` | The billing portal could not be opened. Try again. | | `billing.sending_blocked_canceled` | The subscription is cancelled, so emails are not going out. Your data stays intact: pick a plan and sending restarts where it left off. | | `billing.sending_blocked_trial_ended` | The trial period has ended. Pick a plan to restart sending; your contacts, campaigns and flows are waiting for you just as you left them. | | `billing.subscription_not_found` | This company has no subscription. | | `billing.subscription_not_found_for_customer` | No subscription matches this Stripe customer. | | `billing.trial_not_activatable` | The trial starts on its own when the company is created; it cannot be activated as a paid plan. | | `billing.trial_not_payable` | The trial is free; there is nothing to pay for. | ## Campaigns | Code | What the dashboard says | | -------------------------------------- | ------------------------------------------------------------------------------------------------------------------------- | | `campaigns.ab_no_versions` | Add version B to the A/B test, or turn the test off. | | `campaigns.ab_not_found` | This email has no such A/B test version. | | `campaigns.ab_not_waiting` | This A/B test is not waiting for its winner. | | `campaigns.ab_percent_invalid` | A test takes 10% to 50% of the audience, or everyone in a plain split. | | `campaigns.ab_version_incomplete` | Version {{label}} of the A/B test is not finished. | | `campaigns.ab_versions_full` | An A/B test has at most three versions. | | `campaigns.ab_wait_too_long` | That wait is too long for what decides the winner. | | `campaigns.ab_with_languages` | An email with language versions cannot have an A/B test yet. | | `campaigns.already_sending` | The campaign is being sent right now. | | `campaigns.already_sent` | The campaign has already been sent. | | `campaigns.approval_not_requested` | This email is not waiting for approval. | | `campaigns.archive_while_scheduled` | A scheduled campaign can’t be archived. Cancel the schedule first. | | `campaigns.archive_while_sending` | A campaign can't be archived while it's being sent. | | `campaigns.archived` | This campaign is archived. Bring it back to the list before sending it. | | `campaigns.best_time_with_ab` | Turn off “each person’s best time” or the A/B test: they cannot run together. | | `campaigns.body_missing` | Write the email content before you send. | | `campaigns.calendar_range_invalid` | The calendar shows one month at a time. | | `campaigns.compare_ids_invalid` | Name one to four campaigns to compare. | | `campaigns.consent_not_declared` | You have not declared how you collect consent yet. Do it in Settings, under Consent, then start the campaign. | | `campaigns.holdout_invalid` | Choose 5, 10 or 20 percent for the control group. | | `campaigns.holdout_not_found` | This email has no control group to measure. | | `campaigns.initiative_emails_invalid` | A new campaign can start with at most 5 planned emails. | | `campaigns.initiative_goal_invalid` | Choose what the goal counts and a target above zero. | | `campaigns.initiative_no_emails` | Add an email to the campaign before you start it. | | `campaigns.initiative_not_found` | This campaign no longer exists. | | `campaigns.initiative_not_ready` | Some emails of the campaign are not ready yet. | | `campaigns.initiative_not_started` | This campaign has not been started. | | `campaigns.initiative_period_invalid` | Choose a start and an end, with the end after the start. | | `campaigns.initiative_started` | Some emails of this campaign are scheduled or already sent. Archive it instead. | | `campaigns.no_recipients` | Nobody in the chosen audience can receive emails right now. | | `campaigns.not_found` | That campaign does not exist. | | `campaigns.not_paused` | This email is not paused. | | `campaigns.not_scheduled` | This campaign isn’t scheduled. | | `campaigns.not_sending` | This email is not sending right now. | | `campaigns.pause_failed` | Sending could not be paused or resumed. Try again. | | `campaigns.proposal_no_more_ideas` | That was the last idea for this week: {{max}} a week. | | `campaigns.proposal_not_found` | There is no proposal for this week. | | `campaigns.proposal_unclear` | We could not put a usable proposal together this time. Try again later. | | `campaigns.recipients_unreadable` | The list of recipients could not be read. | | `campaigns.resend_not_sent` | Only an email that finished sending can be resent. | | `campaigns.resend_of_resend` | A resend cannot be resent again. | | `campaigns.resend_source_missing` | The email this resend follows no longer exists. | | `campaigns.schedule_in_past` | Pick a time at least {{minutes}} minutes from now. | | `campaigns.schedule_too_far` | A campaign can be scheduled at most {{days}} days ahead. | | `campaigns.scheduled` | This campaign is scheduled. Cancel the schedule to change it. | | `campaigns.segment_unavailable` | The segment this campaign goes to no longer exists. Choose another audience. | | `campaigns.send_interrupted` | Sending stopped. Start the campaign again; it picks up where it left off. | | `campaigns.send_stopped` | The campaign stopped while it was being sent. | | `campaigns.share_expiry_invalid` | Choose 7, 30 or 90 days, or no end. | | `campaigns.share_nothing_sent` | Nothing of this campaign has gone out yet, so there is no report to share. | | `campaigns.shared_report_not_found` | This report is no longer shared. | | `campaigns.start_no_time` | Choose when this email goes out. | | `campaigns.start_outside_period` | This email goes out outside the campaign’s period. | | `campaigns.subject_ideas_text_invalid` | Write the email first: the ideas are written for what it says. | | `campaigns.subject_ideas_unclear` | We could not write usable ideas this time. Try again. | | `campaigns.subject_missing` | Write a subject before you send. | | `campaigns.test_contact_not_found` | That contact is no longer in your list. | | `campaigns.test_not_sent` | The test email could not be sent. | | `campaigns.token_unavailable` | The email uses tokens a campaign can't fill: {{tokens}}. Campaigns know the person and the store, not a cart or an order. | | `campaigns.variant_language_invalid` | Choose the language of this version. | | `campaigns.variant_limit` | A campaign can have at most {{max}} language versions. | | `campaigns.variant_not_found` | There is no version in that language. | ## Catalog | Code | What the dashboard says | | --------------------------------- | --------------------------------------------------------------------------------------- | | `catalog.batch_too_large` | Send at most {{max}} items in one call. | | `catalog.import_file_missing` | Choose a CSV file to import. | | `catalog.import_file_too_large` | The file is too large: at most {{max}} MB. | | `catalog.import_file_unreadable` | The file could not be read as a table: save it as CSV, with a header row. | | `catalog.import_mapping_invalid` | Choose one column for the id and one for the name; every other field at most once. | | `catalog.import_too_many_rows` | The file has too many rows: at most {{max}} items at a time. | | `catalog.import_value_invalid` | This value could not be read. | | `catalog.item_attributes_invalid` | At most {{max}} fields, each a short lowercase key with a text, number or yes/no value. | | `catalog.item_categories_invalid` | At most {{max}} categories, each a short name. | | `catalog.item_currency_invalid` | Write the currency as its three-letter code, such as RON or EUR. | | `catalog.item_field_too_long` | Too long: at most {{max}} characters. | | `catalog.item_id_missing` | Every item needs your own id for it, so it can be updated later. | | `catalog.item_kind_invalid` | The kind is product, plan, service or other. | | `catalog.item_name_missing` | Every item needs a name. | | `catalog.item_not_found` | That item is not in the catalogue. | | `catalog.item_price_invalid` | A price is a number of zero or more; a price before discount is higher than the price. | | `catalog.item_read_only` | This item comes from your store: change it there and it updates here. | | `catalog.item_stock_invalid` | The quantity in stock is a whole number of zero or more. | | `catalog.item_url_invalid` | Write a full address, starting with https\://. | | `catalog.items_limit` | The catalogue is full: it holds at most {{max}} items. | | `catalog.store_sync_failed` | The store's products could not be read ({{status}}). | ## Commerce | Code | What the dashboard says | | ------------------------------------- | ----------------------------------------------------------------------- | | `commerce.cart_email_invalid` | The cart’s email address is not valid. | | `commerce.cart_not_found` | That cart does not exist. | | `commerce.export_selection_too_large` | Too many orders selected: at most {{max}} at a time. | | `commerce.export_too_large` | The export has more than {{max}} orders. Narrow the list with a filter. | | `commerce.import_store_disconnected` | The store was disconnected. | | `commerce.import_store_responded` | The store responded with {{status}}. | | `commerce.order_not_found` | That order does not exist. | | `commerce.store_not_active` | The store is not active. Check the connection before importing. | ## Common | Code | What the dashboard says | | -------------------------- | ------------------------------------------------------------------------------------------------ | | `common.conflict` | Something changed in the meantime. Reload the page and try again. | | `common.forbidden` | You do not have access to this. Ask someone on your team for rights. | | `common.not_found` | We could not find that. It may have been deleted. | | `common.too_many_requests` | Too many requests in a short time. Try again in a moment. | | `common.unauthorized` | Your session has ended. Sign in again. | | `common.unavailable` | The service is not responding right now. Try again in a few minutes. | | `common.unexpected` | Something went wrong on our side. If you write to support, give them the code {{correlationId}}. | ## Content | Code | What the dashboard says | | ----------------------------------------- | -------------------------------------------------------------------------------------------- | | `content.ai_bad_answer` | The result was not usable, so your text was left as it is. Try again. | | `content.ai_image_data_invalid` | This picture could not be saved. Try making it again. | | `content.ai_image_prompt_invalid` | Describe the picture in a few words, up to {{max}} characters. | | `content.ai_request_invalid` | Choose what to do with the text. | | `content.ai_text_invalid` | Write some text first. The help works on up to {{max}} characters at a time. | | `content.image_heic_unsupported` | This looks like an iPhone HEIC photo. Export or convert it to JPEG first, then upload it. | | `content.image_limit_reached` | You reached the limit of {{max}} images. | | `content.image_missing` | Choose an image to upload. | | `content.image_storage_unavailable` | Uploading images is not available right now. | | `content.image_too_large` | The image is too large. The most we accept is {{maxMb}} MB. | | `content.image_type_unsupported` | Use a PNG, JPEG, GIF, WebP, BMP or AVIF image. | | `content.model_body_required` | Design the email before saving it. | | `content.model_built_in` | This model came with Azimea: hide it instead of deleting it. | | `content.model_code_invalid` | Use lower-case letters, digits and dashes, up to {{max}} characters. | | `content.model_code_taken` | Another model already uses this code. | | `content.model_description_too_long` | Keep the description under {{max}} characters. | | `content.model_english_required` | A published model needs its English version: companies whose language it lacks see that one. | | `content.model_language_invalid` | Pick one of the six languages. | | `content.model_not_found` | That model does not exist any more. | | `content.model_order_invalid` | The list changed meanwhile. Reload it and order it again. | | `content.model_subject_required` | Write a subject. | | `content.model_words_unknown` | These personalised words cannot be filled: {{words}}. | | `content.saved_row_invalid` | This row could not be saved. | | `content.saved_row_limit_reached` | You keep {{max}} rows already. Remove one you no longer use. | | `content.saved_row_name_invalid` | Give the row a name of up to {{max}} characters. | | `content.saved_row_not_found` | That row does not exist any more. | | `content.starter_code_invalid` | This is not one of the starter emails. | | `content.system_email_not_editable` | This system email cannot be rewritten. | | `content.system_email_nothing_to_publish` | There is no draft to publish. | | `content.system_email_version_not_found` | That version does not exist any more. | | `content.system_email_word_missing` | The email needs this word to work: {{word}}. | | `content.system_email_words_unknown` | This email cannot fill these words: {{words}}. | | `content.template_body_missing` | Write the email content before you send a test. | | `content.template_limit_reached` | You reached the limit of {{max}} templates. Delete one you no longer use. | | `content.template_name_invalid` | Give the template a name of up to {{max}} characters. | | `content.template_not_found` | That template doesn't exist anymore. | | `content.template_subject_missing` | Write a subject before you send a test. | | `content.template_test_not_sent` | The test email could not be sent. | | `content.template_too_large` | This email is too long to save as a template. | | `content.version_not_found` | That version does not exist any more. | | `content.version_scope_invalid` | This version could not be kept. | | `content.version_too_large` | This email is too large to keep a version of. | ## Deliverability | Code | What the dashboard says | | -------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `deliverability.default_sender_cannot_be_removed` | The default sender can't be removed. Make another one the default first. | | `deliverability.dkim_keys_not_found` | This domain has no saved keys. Remove it and add it again. | | `deliverability.dkim_public_key_missing` | The saved key is incomplete. Remove the domain and add it again. | | `deliverability.dns_records_lost` | The SPF and DKIM records are no longer in DNS. Publish them again to keep sending from this domain. | | `deliverability.domain_already_added` | This domain is already on the list. | | `deliverability.domain_delete_failed` | The domain could not be removed at the sending provider. Try again. | | `deliverability.domain_invalid` | Enter a domain, like “mystore.com”. | | `deliverability.domain_limit_reached` | You already have {{max}} domains. Remove one before adding another. | | `deliverability.domain_missing_at_provider` | The domain no longer exists at the sending provider. Remove it and add it again. | | `deliverability.domain_not_found` | That domain does not exist. | | `deliverability.domain_rejected_by_provider` | The sending provider refused this domain. | | `deliverability.domain_reserved` | This domain belongs to Azimea and cannot be added. | | `deliverability.domain_taken` | This domain is already used by another Azimea account. Write to suport\@azimea.com if it is yours. | | `deliverability.from_email_not_on_verified_domain` | The address must be on a verified domain. “{{domain}}” is not verified yet. | | `deliverability.key_vault_forbidden` | We were not allowed to read the domain keys. We have been notified. | | `deliverability.key_vault_not_configured` | We cannot prepare the domain keys on this server. We have been notified. | | `deliverability.key_vault_response_unexpected` | The key vault answered with something we did not expect. We have been notified. | | `deliverability.key_vault_unreachable` | We could not reach the key vault. Try again in a few minutes. | | `deliverability.key_vault_write_failed` | The domain keys could not be saved. Try again. | | `deliverability.provider_error` | The sending provider answered with an error. Try again in a few minutes. | | `deliverability.provider_response_unreadable` | We could not read the sending provider’s answer. Try again. | | `deliverability.provider_unreachable` | The sending provider did not answer. Try again in a few minutes. | | `deliverability.sender_already_saved` | This address is already one of your senders. | | `deliverability.sender_domain_not_verified` | The domain “{{domain}}” is no longer verified, so nothing can be sent from it. | | `deliverability.sender_limit_reached` | Your plan allows {{max}} senders. Remove one or move to a larger plan. | | `deliverability.sender_not_found` | This sender no longer exists. | | `deliverability.sender_not_saved` | “{{email}}” isn't one of your senders. Add it in Settings · Sending, or choose another. | | `deliverability.sender_not_set` | You do not have a sender address yet. Pick one in Settings · Sending domains. | | `deliverability.sending_not_configured` | Sending is not configured on this server, so domains cannot be verified. | ## Identity | Code | What the dashboard says | | -------------------------------------- | --------------------------------------------------------------------------------------------------- | | `identity.account_blocked` | This account is blocked. Write to us at suport\@azimea.com. | | `identity.account_locked` | The account is locked for a few minutes after too many attempts. | | `identity.account_not_created` | The account could not be created. Try again. | | `identity.api_key_expiry_not_allowed` | Choose 30, 90 or 365 days. | | `identity.api_key_limit` | You already have {{max}} active keys. Revoke one before creating another. | | `identity.api_key_not_found` | That key does not exist. | | `identity.api_key_scope_unknown` | That permission does not exist. | | `identity.confirmation_link_invalid` | This confirmation link is no longer valid. Ask for a new one. | | `identity.email_already_confirmed` | This email is already confirmed. | | `identity.email_not_confirmed` | Confirm your email before signing in. | | `identity.email_taken` | There is already an account with this email. | | `identity.erase_email_mismatch` | Type the person's email exactly to confirm. | | `identity.erase_last_owner` | They are the only owner of a company with other people. Make someone else owner first. | | `identity.invalid_credentials` | Email or password is not correct. | | `identity.language_invalid` | This is not one of the app's languages. | | `identity.not_member_of_tenant` | You are not part of this company. | | `identity.ops_access_denied` | This account cannot open the Azimea console. | | `identity.ops_break_glass_owner` | This owner is set in the server's configuration and can only be changed there. | | `identity.ops_cannot_reset_operator` | An operator's two-factor authentication cannot be reset from the console. | | `identity.ops_last_owner` | The console needs at least one owner. Make someone else owner first. | | `identity.ops_operator_exists` | This person is already an operator. | | `identity.ops_operator_not_found` | This person is not an operator. | | `identity.ops_owner_email_unconfirmed` | This account's email is not confirmed. Confirm it from the dashboard first, or use another address. | | `identity.ops_setup_done` | The console already has an owner. Sign in with your own account. | | `identity.ops_two_factor_required` | Turn on two-factor authentication in the dashboard before opening the console. | | `identity.ops_user_is_operator` | This person is an operator. Remove them from Operators first. | | `identity.password_incorrect` | That is not your current password. | | `identity.password_weak` | That password is too weak. Pick a longer one. | | `identity.reset_link_invalid` | This link is no longer valid. Ask for a new one. | | `identity.support_link_invalid` | This support link is no longer valid. Open the company again from the console. | | `identity.support_read_only` | Support view is read-only: nothing can be changed here. | | `identity.two_factor_already_enabled` | Two-step verification is already on. | | `identity.two_factor_code_invalid` | That code is not valid. Check the app and try again. | | `identity.two_factor_not_enabled` | Two-step verification is not on. | | `identity.two_factor_session_expired` | The sign-in took too long. Enter your password again. | | `identity.two_factor_setup_missing` | Start the setup again to get a new QR code. | | `identity.user_already_blocked` | This account is already blocked. | | `identity.user_erased` | This account was erased; nothing can be changed on it. | | `identity.user_not_blocked` | This account is not blocked. | | `identity.user_not_found` | That user does not exist. | | `identity.user_not_locked` | This account is not locked. | ## Ingestion | Code | What the dashboard says | | -------------------------------------- | ---------------------------------------------------------------------------------- | | `ingestion.batch_too_large` | Too many events in one delivery: at most {{max}}. | | `ingestion.body_invalid` | The delivery body is not the expected JSON. | | `ingestion.channel_broken` | The store’s secret can no longer be read. Generate new keys in the store screen. | | `ingestion.channel_not_found` | This store has no plugin keys yet. | | `ingestion.delivery_stale` | The delivery is too old or the store’s clock is off. Check the server time. | | `ingestion.event_too_old` | The event is older than a week and was not processed. | | `ingestion.event_unknown` | We do not know this event type yet. Update the app or the plugin. | | `ingestion.origin_not_allowed` | Events for this store are accepted only from the store’s own site. | | `ingestion.payload_invalid` | The event is missing a required field. | | `ingestion.platform_signature_invalid` | The webhook is not signed with this platform’s key. | | `ingestion.signature_invalid` | The delivery is not signed with this store’s secret. Check the keys in the plugin. | | `ingestion.store_not_found` | That store does not exist. | ## Notices | Code | What the dashboard says | | ------------------------------- | ------------------------------------------------------------------------------------- | | `notices.action_both` | Lead either to a page in the app or to a link outside it, not both. | | `notices.action_incomplete` | An action needs both a label and a place to lead. | | `notices.action_route_invalid` | A page in the app starts with / (and not //). | | `notices.action_url_invalid` | A link outside the app must start with https\://. | | `notices.audience_invalid` | That audience names a role, pack, plan or language that does not exist. | | `notices.ended` | This message has ended and cannot be changed. | | `notices.english_required` | Write the message in English at least: it is what every other language falls back to. | | `notices.escalation_incomplete` | Say after how many days, and what the message becomes. | | `notices.key_invalid` | Use lowercase letters, digits, dots and dashes (3 to 100). | | `notices.key_taken` | There is already a message with this key. | | `notices.language_unknown` | That is not one of the app's languages. | | `notices.not_dismissible` | This message stays until it is dealt with. | | `notices.not_found` | That message is no longer there. | | `notices.snooze_limit` | This message was already put off as many times as it allows. | | `notices.snooze_not_allowed` | This message cannot be put off. | | `notices.status_invalid` | A message cannot go back to draft. | | `notices.window_invalid` | The end must come after the start. | ## Notifications | Code | What the dashboard says | | ----------------------------------- | ------------------------------------------ | | `notifications.not_found` | That notification is no longer there. | | `notifications.preferences_invalid` | Those notification settings are not valid. | | `notifications.unknown_type` | That is not a kind of notification. | ## Purposes | Code | What the dashboard says | | ---------------------------------------- | ---------------------------------------------------------------------------------------- | | `purposes.service_confirmation_required` | Confirm that these emails only inform and carry no offer before marking them as service. | ## Sending | Code | What the dashboard says | | -------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------------------- | | `sending.address_invalid` | The email address is not valid: {{detail}} | | `sending.address_rejected` | The address permanently rejected the email. | | `sending.already_suspended` | Sending is already paused by Azimea. | | `sending.daily_cap_reached` | Today’s sending limit ({{cap}} emails) is reached. The rest goes out tomorrow. | | `sending.daily_cap_used_up` | Today’s sending limit is reached. The rest goes out tomorrow. | | `sending.gate_bounced` | Earlier emails bounced from this address. | | `sending.gate_complained` | The person marked an email as spam. | | `sending.gate_no_basis` | No basis to send: the person did not subscribe and did not buy. | | `sending.gate_not_declared` | You have not declared how you collect consent yet. Do it in Settings, under Consent. | | `sending.gate_soft_opt_in_off` | This store does not email existing customers without a checkbox, and this person did not tick one. | | `sending.gate_topic_opted_out` | The person turned off this email's topic. | | `sending.gate_unsubscribed` | The person unsubscribed. | | `sending.mta_auth_refused` | The mail server refused our login: {{detail}} | | `sending.mta_connection_lost` | The connection to the mail server dropped. The rest resumes on a new one. | | `sending.mta_not_answering` | The mail server is not answering: {{detail}} | | `sending.mta_slow_down` | The mail server asks us to send more slowly: {{detail}} | | `sending.no_handover_answer` | No hand-over to the mail server was answered, not even the last one. We do not know whether the email went out. | | `sending.no_news_asked_again` | No news from the mail server; asking again. | | `sending.no_news_for_hours` | No news for over {{hours}} hours. We do not know whether it arrived. | | `sending.no_verified_sender` | This company has no verified sender yet, so the emails are waiting. | | `sending.not_suspended` | Sending is not paused by Azimea. | | `sending.plan_limit_reached` | You have sent all {{limit}} emails included in your plan this month. The rest goes out at the start of next month, or sooner if you move to a bigger plan. | | `sending.platform_email_failed` | The email could not be sent. | | `sending.platform_email_kind_unknown` | There is no platform email of this kind. | | `sending.provider_busy` | The sending provider is busy. We will try again shortly. | | `sending.provider_cannot_be_contacted` | The sending provider cannot be reached right now. We will try again shortly. | | `sending.provider_not_answering` | The sending provider is not answering. We will try again shortly. | | `sending.provider_rejected` | The sending provider did not accept the email. | | `sending.reputation_bounce_stop` | Too many addresses bounced ({{rate}}%). The list needs cleaning before the next campaign. | | `sending.reputation_bounce_warning` | More bounces than usual ({{rate}}%). The limit stays where it is until things settle. | | `sending.reputation_complaint_stop` | Too many people marked the emails as spam ({{rate}}%). Above this level, Gmail and Yahoo start blocking everybody sending from the same IP. | | `sending.reputation_complaint_warning` | More complaints than usual ({{rate}}%). The limit stays where it is until things settle. | | `sending.reputation_good` | Good figures. The limit went up. | | `sending.reputation_new_company` | New company. The limit grows as the figures look good. | | `sending.reputation_paused` | Sending is stopped. A person restarts it, after finding out what happened. | | `sending.reputation_restarted` | Restarted by hand. The limit starts over. | | `sending.reputation_too_few_sends` | Too few emails sent to draw a conclusion. The limit stays where it is. | | `sending.sandbox_email_not_found` | That email is not in this sandbox’s inbox any more. | | `sending.sending_stopped` | Sending is stopped. | | `sending.signature_invalid` | Invalid signature. | | `sending.suppression_not_found` | That address is no longer on the do-not-send list. | | `sending.suppression_not_removable` | Only bounced addresses can be taken off the list. An unsubscribe or a complaint can only be undone by the person, by subscribing again. | | `sending.suspended_by_operator` | Azimea paused your sending. Contact support to resume it. | | `sending.webhook_auto_disabled` | Turned off automatically after {{failures}} failed deliveries in a row. | | `sending.webhook_event_unknown` | That event type does not exist. | | `sending.webhook_http_error` | The address answered with error {{status}}. | | `sending.webhook_limit_reached` | You already have {{max}} active webhooks. Delete one before adding another. | | `sending.webhook_no_answer` | The address did not answer. | | `sending.webhook_not_found` | That webhook does not exist. | | `sending.webhook_secret_unreadable` | The webhook secret can no longer be read. Delete the webhook and add it again. | | `sending.webhook_stopped` | The webhook was stopped or deleted. | | `sending.webhook_timeout` | The address did not answer in time. | | `sending.webhook_unreachable` | We could not reach the address. | | `sending.webhook_url_invalid` | The address must be an https URL (http is accepted only on localhost). | | `sending.webhook_url_not_allowed` | The address is not allowed: it points to a private network or it redirects. | ## Stores | Code | What the dashboard says | | ---------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `stores.already_connected` | This store is already connected. Check the connection or disconnect it before using other keys. | | `stores.checkouts_not_kept` | This platform does not keep abandoned checkouts. | | `stores.connection_failed` | The connection to the store failed. | | `stores.consumer_key_format` | A consumer key starts with “ck\_” and is 43 characters long. | | `stores.consumer_secret_format` | A consumer secret starts with “cs\_” and is 43 characters long. | | `stores.credentials_rejected` | The store refused the API keys. Check them and make sure they have read and write access. | | `stores.credentials_unreadable` | The saved keys can no longer be read. Connect the store again with your API keys. | | `stores.disconnected` | This store is disconnected. Connect it again with your API keys. | | `stores.gomag_key_format` | Paste the API key exactly as Gomag shows it, in one piece. | | `stores.gomag_key_rejected` | Gomag did not accept this API key for this shop. Check the key and that the address is your shop’s own. | | `stores.gomag_shop_not_found` | Gomag does not know a shop at this address. Use the address your customers open. | | `stores.http_error` | The store answered with error {{status}}. Try again later. | | `stores.integration_already_available` | This platform can be connected already; there is no waiting list for it. | | `stores.integration_not_found` | That platform is not in the catalogue. | | `stores.limit_reached` | Your plan includes {{max}} connected stores. Disconnect one or move to a larger plan. | | `stores.magento_api_not_found` | The Magento REST API did not answer at this address. Check the store’s address. | | `stores.magento_permission_missing` | The integration cannot read “{{resource}}”. Tick it in the integration’s API resources (System › Integrations) and save. | | `stores.magento_token_format` | The access token is 32 letters and digits. | | `stores.magento_token_rejected` | Magento did not accept this access token. Check the token, and that “Allow OAuth Access Tokens to be used as standalone Bearer tokens” is set to Yes in Stores › Configuration › Services › OAuth. | | `stores.merchantpro_api_not_found` | The MerchantPro API did not answer at this address. Use your shop’s own address, the one customers open. | | `stores.merchantpro_keys_rejected` | MerchantPro did not accept this API user. Check the name and the secret, and that the user is active. | | `stores.merchantpro_permission_missing` | The API user cannot read “{{resource}}”. Give it read access and try again. | | `stores.merchantpro_secret_format` | Paste the API user’s secret exactly as MerchantPro shows it. | | `stores.merchantpro_user_format` | Paste the API user’s name exactly as MerchantPro shows it. | | `stores.not_a_woocommerce_api` | The store did not answer like a WooCommerce API. A security plugin or a firewall may be blocking it. | | `stores.not_active` | This store is not active. | | `stores.not_found` | That store does not exist. | | `stores.opencart_extension_not_found` | The Azimea extension did not answer at this address. Check the address, and that the extension is installed and enabled in OpenCart. | | `stores.opencart_key_format` | The read key starts with “azr\_”. Copy it again from the Azimea extension in OpenCart. | | `stores.opencart_keys_rejected` | The shop did not accept these read keys. Copy both again from the Azimea extension: making new keys stops the old ones. | | `stores.opencart_secret_format` | The read secret starts with “azs\_”. Copy it again from the Azimea extension in OpenCart. | | `stores.platform_not_available` | This platform cannot be connected yet. | | `stores.prestashop_key_format` | The webservice key is 32 letters and digits. | | `stores.prestashop_key_rejected` | PrestaShop did not accept this webservice key. Check that it is copied whole and that its status is on. | | `stores.prestashop_permission_missing` | The webservice key cannot read “{{resource}}”. Tick GET for it on the key and try again. | | `stores.prestashop_webservice_disabled` | The PrestaShop webservice is turned off. Turn it on in Advanced Parameters › Webservice. | | `stores.prestashop_webservice_not_found` | The PrestaShop webservice did not answer at this address. Check the address and that the webservice is on. | | `stores.response_unexpected` | The store answered with something we did not expect. | | `stores.rest_api_not_found` | We could not find the WooCommerce REST API at this address. Check the address, that WooCommerce is active and that permalinks are not set to “Plain”. | | `stores.shopify_domain_invalid` | Type your shop’s Shopify address, the one ending in .myshopify.com. | | `stores.shopify_install_invalid` | The approval from Shopify could not be checked or has expired. Start the install again from Azimea. | | `stores.shopify_not_configured` | Shopify cannot be connected from this installation yet. | | `stores.shopify_permission_missing` | The Azimea app is missing a permission on this shop. Open the app in Shopify and approve the update. | | `stores.shopify_shop_not_found` | Shopify does not know this shop. Check the .myshopify.com address. | | `stores.shopify_token_rejected` | Shopify no longer accepts Azimea’s access to this shop: the app was uninstalled. Install it again to reconnect. | | `stores.shopify_use_install` | A Shopify shop is connected by installing the Azimea app, not with keys. | | `stores.shopware_api_not_found` | The Shopware Admin API did not answer at this address. Use your shop’s own address. | | `stores.shopware_app_deactivated` | The Azimea app is deactivated in Shopware. Activate it again in Extensions › My extensions. | | `stores.shopware_keys_rejected` | Shopware no longer accepts the Azimea app’s keys. Reinstall the app in Shopware, then check the connection. | | `stores.shopware_link_code_invalid` | This link code does not match a Shopware shop at this address. Copy it again from the Azimea app’s settings in Shopware. | | `stores.shopware_not_confirmed` | The Shopware shop has not finished installing the Azimea app. Install it again in Shopware. | | `stores.shopware_permission_missing` | The Azimea app is not allowed to read “{{resource}}”. Update the app in Shopware and accept its permissions. | | `stores.shopware_registration_invalid` | The Shopware app registration is not signed correctly. | | `stores.timeout` | The store did not answer in time. Try again. | | `stores.unreachable` | We could not reach the store. Check the address and that the site is online. | | `stores.url_has_credentials` | The address cannot carry a user name or a password. | | `stores.url_not_https` | The store must use HTTPS: API keys are never sent unencrypted. | | `stores.url_not_public` | The address does not point to a public server. | | `stores.url_port_not_allowed` | The address cannot use a non-standard port. | | `stores.url_redirects` | The address redirects. Use the final address of the site. | | `stores.url_redirects_to` | The address redirects to {{location}}. Use the final address of the site. | | `stores.webhook_secret_format` | Paste the webhook secret exactly as your shop shows it. | ## Tenancy | Code | What the dashboard says | | --------------------------------------- | ---------------------------------------------------------------------------------------- | | `tenancy.already_invited` | There's already a pending invitation for this address. | | `tenancy.already_member` | This person is already on the team. | | `tenancy.brand_color_invalid` | Use a color like #2f6bff. | | `tenancy.cannot_change_own_role` | You can't change your own role. | | `tenancy.cannot_invite_as_owner` | An invitation can only grant Admin or Member. | | `tenancy.cannot_remove_self` | You can't remove yourself from the team. | | `tenancy.company_closed` | This company is closed. | | `tenancy.company_not_created` | The company could not be created. Try again. | | `tenancy.company_not_found` | That company does not exist. | | `tenancy.company_status_unchanged` | The company is already in that state. | | `tenancy.company_suspended` | This company is suspended. Contact Azimea support. | | `tenancy.copy_kind_not_supported` | Templates, flows and campaigns can be copied to live. | | `tenancy.copy_to_live_not_allowed` | Only the live company’s owners and administrators can copy to live. | | `tenancy.footer_language_invalid` | A footer text is for a language the app does not write in. | | `tenancy.identity_text_too_long` | That text is too long. Use at most {{max}} characters. | | `tenancy.invitation_already_accepted` | This invitation was already accepted. | | `tenancy.invitation_email_mismatch` | This invitation was sent to a different email address. | | `tenancy.invitation_expired` | This invitation has expired. Ask for a new one. | | `tenancy.invitation_not_found` | That invitation does not exist. | | `tenancy.invitation_revoked` | This invitation was cancelled. | | `tenancy.last_owner_required` | The company needs at least one owner. | | `tenancy.logo_url_invalid` | The logo must be an image address starting with https\://. | | `tenancy.market_unknown` | That country is not on the list. | | `tenancy.member_not_found` | That member does not exist. | | `tenancy.not_a_sandbox` | This is only possible inside a sandbox. | | `tenancy.only_owner_can_grant_owner` | Only an owner can grant or remove the owner role. | | `tenancy.sandbox_address_exists` | This address is already on the list. | | `tenancy.sandbox_address_limit_reached` | A sandbox can have up to ten test addresses. Remove one to add another. | | `tenancy.sandbox_address_not_found` | That test address is no longer on the list. | | `tenancy.sandbox_copy_failed` | We could not fill the sandbox with your company’s settings. Nothing was kept; try again. | | `tenancy.sandbox_in_sandbox` | Sandboxes are made from the live company. Leave this sandbox first. | | `tenancy.sandbox_limit_reached` | This company already has five sandboxes. Delete one to make another. | | `tenancy.sandbox_not_found` | That sandbox no longer exists. | | `tenancy.site_unreachable` | We could not open this site. Check the address and try again. | | `tenancy.website_url_invalid` | Write the full address of your site, starting with https\://. | ## Topics | Code | What the dashboard says | | ----------------------------- | ------------------------------------------------------------------------------------------------------ | | `topics.archived` | This topic is archived. Restore it in Settings, or pick another one. | | `topics.description_too_long` | Keep the description under {{max}} characters. | | `topics.key_invalid` | Use lowercase letters, digits and '\_' for the key, starting with a letter (up to {{max}} characters). | | `topics.key_taken` | Another topic already uses the key "{{key}}". Keys stay taken even when a topic is archived. | | `topics.limit_reached` | You already have {{max}} topics. Archive one to add another. | | `topics.name_invalid` | Give the topic a name of up to {{max}} characters. | | `topics.not_found` | That topic doesn't exist any more. | ## Validation | Code | What the dashboard says | | ------------------------- | --------------------------------------- | | `validation.email` | Enter a valid email address. | | `validation.enum` | Pick one of the available options. | | `validation.exact_length` | Exactly {{max}} characters. | | `validation.greater_than` | Must be greater than {{value}}. | | `validation.invalid` | This value is not valid. | | `validation.length` | Between {{min}} and {{max}} characters. | | `validation.less_than` | Must be less than {{value}}. | | `validation.max_length` | Too long: at most {{max}} characters. | | `validation.min_length` | Too short: at least {{min}} characters. | | `validation.not_equal` | This value cannot be used. | | `validation.pattern` | This is not the right format. | | `validation.range` | Choose between {{from}} and {{to}}. | | `validation.required` | Fill in this field. | | `validation.too_many` | Too many: at most {{max}}. | | `validation.url` | Enter a valid address. | # Events > Every event Azimea receives from shops and products, and the ones it records itself. ## Events from your shop [Section titled “Events from your shop”](#events-from-your-shop) Sent by the Azimea plugin or app in your shop, or read from the platform. They follow the same contract on every platform. | Event | What it means | When it happens | Who sends it | | --------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------- | --------------------------------------------------------------------------- | | `cart.updated` | The cart changed: a product added, removed or a quantity changed. | On every change to the cart, even before the visitor is known. | Shop plugin or storefront script; Azimea itself for Shopify and MerchantPro | | `cart.cleared` | The cart was emptied. | When the visitor removes the last product. | Shop plugin or storefront script | | `contact.identified` | The visitor became known: they typed their email at checkout. | When the email field is filled in. | Shop plugin or storefront script | | `order.placed` | An order was placed. | When the shop records the order. | Shop plugin or the platform's webhooks | | `order.updated` | An order's status changed (paid, shipped, cancelled, refunded). | On every status change. | Shop plugin or the platform's webhooks | | `customer.upserted` | A customer account was created or changed in the shop. | On registration or a profile change. | Shop plugin or the platform's webhooks | | `subscriber.created` | Someone signed up to the newsletter, with the proof of their consent when they ticked a box. | On a newsletter sign-up (form, popup, checkout box). | Shop plugin, the platform's webhooks or your API | | `subscriber.removed` | Someone left the shop's own newsletter: marketing to them stops. | When they unsubscribe in the shop. | MerchantPro webhooks or a shop plugin | | `contact.erasure_requested` | The shop asks for a person's data to be erased (GDPR). | When the shop receives an erasure request. | Shopify's GDPR webhook or a shop plugin | ## Events Azimea records [Section titled “Events Azimea records”](#events-azimea-records) Azimea’s own facts about a contact, shown on their timeline. Flows start on these. | Event | What it means | When it happens | Who sends it | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------ | | `order.placed` | Azimea recorded a new order on the contact's timeline. | When the order arrives, from any source. | Azimea | | `order.paid` | An order was paid. | When an order reaches the Paid status. | Azimea | | `order.cancelled` | An order was cancelled. | When an order reaches the Cancelled status. | Azimea | | `order.refunded` | An order was refunded. | When an order reaches the Refunded status. | Azimea | | `cart.abandoned` | Azimea's verdict that a cart was left: it starts the abandoned-cart flow. | 30 minutes without change, with products and an address, and no order. | Azimea | | `contact.subscribed` | A contact became a subscriber: a newsletter or form sign-up starts welcome flows; someone who ticked marketing while creating a shop account starts the flows on new customer accounts. | When a contact's consent becomes Subscribed. | Azimea | ## Suggested events from your product [Section titled “Suggested events from your product”](#suggested-events-from-your-product) Your product can send any event name to `POST /events`. These are the names the ready-made product flows start on; use them and those flows work without changes. | Event | What it means | When it happens | Who sends it | | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------- | | `user.signed_up` | A person created an account in your product. | Right after sign-up. | Your product, through POST /events | | `user.activated` | A person reached the moment your product counts as "they got it" (their first project, first payment, first report). | When you decide activation happened. | Your product, through POST /events | | `trial.ending` | A person's trial ends soon. | A few days before the end of the trial, as you choose. | Your product, through POST /events | | `subscription.started` | A person started paying. | On the first successful payment. | Your product, through POST /events | # Flow runs > A flow run's status, the steps in its history, and why a run can end early. Each contact that enters a flow has a run. Its history lists every step with the time it happened. ## Run statuses [Section titled “Run statuses”](#run-statuses) | Status or reason | What it means | What you do | | ---------------- | ----------------------------------------------------------------------------- | ------------------------------------------------- | | `Running` | The run is executing its current step. | Nothing. | | `Waiting` | The run is at a wait step, or pausing before retrying a step that failed. | Nothing: it continues at the time shown. | | `Succeeded` | The run reached its end, or ended early for a reason recorded in its history. | Nothing. | | `Failed` | A step failed even after every retry. | Open the run's history to see which step and why. | | `Terminated` | Stopped from outside: the flow was unpublished or the contact was erased. | Nothing. | ## Steps in a run’s history [Section titled “Steps in a run’s history”](#steps-in-a-runs-history) | Step | What it means | | --------------- | ---------------------------------------------------------------------------------------------------- | | `Enrolled` | The contact entered the flow: its trigger happened for them. | | `Entered` | The run arrived at a step. | | `Evaluated` | A condition, an audience check or a wait-for-event was decided; the history shows which way it went. | | `Sent` | The email step queued its email for sending. | | `Tagged` | A tag was added to the contact. | | `WebhookCalled` | The flow called your webhook address. | | `Waited` | A wait step was reached; the run continues at the time it shows. | | `Retried` | A step failed for a passing reason and will be tried again. | | `Failed` | A step failed after every retry; the run stops there. | | `Completed` | The run ended, at the end of the flow or early for the reason it shows. | | `Skipped` | The email step did not send: the sending gate stopped it, and the history shows the reason. | | `Simulated` | In a test run: what the step would have done, without doing it. | ## Why a run ended early [Section titled “Why a run ended early”](#why-a-run-ended-early) | Status or reason | What it means | What you do | | ---------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | | `audience_not_matched` | The contact did not fit the audience the flow is limited to, so the run ended right after the trigger. | Nothing, or widen the flow's audience. | | `cart_closed` | The cart the flow was about was ordered or emptied, so there was nothing left to remind anyone of. | Nothing: this is the reminder working as intended. | | `exit_event` | The contact did one of the things the flow stops on (for example they activated or subscribed). | Nothing. | # Why an email was not sent > Every reason the sending gate stops an email, what it means and what to do. Before an email is queued, Azimea checks whether the person’s [consent status](/catalogs/consent-statuses/) allows this kind of message. When it does not, the email is stopped: its status becomes `Suppressed`, the reason is written on it, and a flow’s history shows “Email not sent” with the same reason. | Status or reason | What it means | What you do | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sending.gate_bounced` | The person's address does not work any more. | Nothing: the address is no longer used. | | `sending.gate_complained` | The person marked one of your emails as spam. | Nothing: they are never written to again. | | `sending.gate_no_basis` | The person has not subscribed and is not an existing customer, so marketing may not reach them. | Expected for people who only typed their address at checkout. To reach them, ask for consent; or make a message about their own order a service email. | | `sending.gate_not_declared` | You have not yet declared how your shop collects consent, so no marketing goes out, even to subscribers. | Settings › Consent: choose your method and declare it. | | `sending.gate_soft_opt_in_off` | The person is an existing customer, but your shop uses a consent checkbox, not a notice at checkout, so buying alone does not allow marketing. | Expected with a checkbox: only those who ticked it receive marketing. Switch to notice at checkout only if your checkout really shows that notice. | | `sending.gate_topic_opted_out` | The person turned off the topic this email belongs to; they still receive your other topics. | Nothing: respect their choice. | | `sending.gate_unsubscribed` | The person unsubscribed from your marketing emails. | Nothing: they said no. Service emails still reach them. | Service emails (about the person’s own order or account, with no offer) only need a working address, so only `gate_bounced` and `gate_complained` can stop them. # Glossary > The words Azimea uses, in plain language. ## Abandoned cart [Section titled “Abandoned cart”](#abandoned-cart) A cart with at least one product and a known email address that has not changed for 30 minutes and has not become an order. It is Azimea’s own verdict, not something the shop reports. See [Recover abandoned carts](/use/recover-abandoned-carts/). ## Cart token [Section titled “Cart token”](#cart-token) The name a cart carries from the first product to the order, so Azimea can tell that the order placed later is this cart’s. It never contains anything personal. ## Consent status [Section titled “Consent status”](#consent-status) What a contact has agreed to, which decides what Azimea may send them: `Subscribed`, `SoftOptIn`, `CheckoutStarted`, `PendingConfirmation`, `Unsubscribed`, `Bounced`, `Complained` or `Unknown`. Each one, with what it allows and what to do, is in the [consent statuses catalog](/catalogs/consent-statuses/). ## Recovered cart [Section titled “Recovered cart”](#recovered-cart) An abandoned (or still open) cart that became an order. The order closes the flow that was reminding the person, so they never get a reminder for something they already bought. ## Sending gate [Section titled “Sending gate”](#sending-gate) The check every email passes just before it is queued. If the person’s consent does not allow this kind of message, the email is stopped and the reason is kept on the message and in the flow’s history. ## Service email [Section titled “Service email”](#service-email) A message about the person’s own order or account, with no offer in it (an order thank-you, a trial ending). It does not need marketing consent and still reaches someone who unsubscribed from marketing. # Start here > What Azimea does and the five ideas everything else is built on. Azimea sends the emails an online business would otherwise send by hand, or not at all: the reminder to someone who left a cart, the welcome to a new subscriber, the thank-you after an order, the campaign to everyone who agreed to hear from you. It reads what happens in your shop or your product, decides who should get which email and when, and checks every message against the person’s consent before it leaves. ## The five ideas [Section titled “The five ideas”](#the-five-ideas) | Idea | What it means for you | | -------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Contacts** | Everyone Azimea knows: from your shop, an import, a form or your own system. Each contact has a [consent status](/glossary/#consent-status) that decides what you may send them. | | **Events** | Things that happen: a cart changed, an order was placed, someone signed up. Events start and stop flows. | | **Flows** | Journeys that run on their own: an event starts them, waits and conditions shape them, emails go out at the right moment. | | **Campaigns** | One email to many people, sent now or at a time you choose. | | **The sending gate** | The last check before any email leaves: does this person’s consent allow this kind of message? If not, the email is not sent and the reason is recorded. | ## Where to go next [Section titled “Where to go next”](#where-to-go-next) **Running a shop** 1. [Connect your store](/use/connect-your-store/) — WooCommerce, PrestaShop, OpenCart, Magento or Shopware. 2. [Declare how you collect consent](/use/consent-and-gdpr/) — until you do, no marketing goes out. 3. [Add your sending domain](/use/sending-and-deliverability/). 4. Publish the ready-made flows: [abandoned carts](/use/recover-abandoned-carts/), [welcome](/use/welcome-and-onboarding/), [after the order](/use/after-the-order/). 5. Send your first [campaign](/use/campaigns/). **Building a product:** [what you can build today](/build/), then [flows](/use/flows/) started by your own events. **Something is not working:** search for the status or the error you see (`Ctrl` `K`), or start from [why an email was not sent](/catalogs/why-an-email-was-not-sent/). A word you do not know: the [glossary](/glossary/). # After the order > Thank every buyer, and know which post-purchase emails count as service and which as marketing. ## What it does and when to use it [Section titled “What it does and when to use it”](#what-it-does-and-when-to-use-it) After an order, a short thank-you tells the buyer their order is in good hands. Later emails (a review request, products that go with what they bought, a “we miss you” after months of silence) bring them back. Azimea sends all of these from flows that start on the order. The difference that matters is **what the email is for**. A thank-you about their own order, with no offer in it, is a **service message** and reaches every buyer. Anything that sells is **marketing** and reaches only people who agreed to it. ## How it works [Section titled “How it works”](#how-it-works) 1. **The order arrives** from your shop and Azimea records [`order.placed`](/catalogs/events/). It counts when it is placed, paid or not: an order waiting for a bank transfer starts it too. 2. **The ready-made “Thank you after an order” flow** (trigger **Store events › Order placed**) waits **60 minutes** and sends the thank-you. It is created as a **service message**: its email is written with no offer in it, so it reaches every buyer, including people who never subscribed. 3. **Marketing after the order** lives in flows of its own, marked **Marketing**: the **Post-purchase follow-up** template (a thank-you, then a review request a week later) and **Win back inactive customers** (on a schedule, to the customers you choose). These reach only subscribers, and existing customers where your checkout tells buyers you may write to them. ### Service or marketing [Section titled “Service or marketing”](#service-or-marketing) Every flow and every campaign has a purpose, set in its editor under **What it is for**: | Purpose | What it may contain | Who receives it | Footer | | --------------------------- | --------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------- | ----------------------------------- | | **Marketing** (the default) | Offers, news, products, discounts. | People who agreed: subscribers, and existing customers where your checkout shows a notice. | Unsubscribe and preferences links. | | **Service message** | Only information about their own order, account or the service. No offer, no discount, no product to buy. | Everyone with a working address who never marked you as spam, even people who unsubscribed from marketing. | A link to manage email preferences. | Choosing **Service message** asks you to confirm that the email only informs; the confirmation is kept in the audit log. If you later add an offer to a service email, change its purpose back to Marketing: a service email that sells breaks the rules your buyers’ consent relies on. ## What each status means and what to do [Section titled “What each status means and what to do”](#what-each-status-means-and-what-to-do) Orders reach Azimea with the same statuses whatever your platform: | Status or reason | What it means | What you do | | ---------------- | ------------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------- | | `Pending` | Placed, but not paid yet: waiting for a bank transfer, cash on delivery, or a payment still being confirmed. | Nothing: flows that start on an order start when it is placed; the buyer becomes a paying customer when it is Paid. | | `Paid` | The payment came in. The buyer becomes a paying customer in Azimea. | Nothing. | | `Completed` | Shipped or delivered: the shop has finished the order. | Nothing. | | `Cancelled` | Cancelled by the shop or the buyer, or the shipment was returned. | Nothing: it no longer counts in revenue. | | `Refunded` | The money was given back. | Nothing: it no longer counts in revenue. | | `Failed` | The payment failed. | Nothing on Azimea's side; the shop decides whether to ask the buyer again. | | `Draft` | An order still being made at checkout, before the buyer confirmed it. | Nothing: it is not counted as an order until it is placed. | A marketing email after the order can be stopped by the [sending gate](/glossary/#sending-gate); the most common reasons here: | Status or reason | What it means | What you do | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sending.gate_no_basis` | The person has not subscribed and is not an existing customer, so marketing may not reach them. | Expected for people who only typed their address at checkout. To reach them, ask for consent; or make a message about their own order a service email. | | `sending.gate_not_declared` | You have not yet declared how your shop collects consent, so no marketing goes out, even to subscribers. | Settings › Consent: choose your method and declare it. | | `sending.gate_soft_opt_in_off` | The person is an existing customer, but your shop uses a consent checkbox, not a notice at checkout, so buying alone does not allow marketing. | Expected with a checkbox: only those who ticked it receive marketing. Switch to notice at checkout only if your checkout really shows that notice. | A service message is stopped only for `sending.gate_bounced` or `sending.gate_complained`. ## Events involved [Section titled “Events involved”](#events-involved) | Event | What it means | When it happens | Who sends it | | ----------------- | ------------------------------------------------------ | ------------------------------------------- | ------------ | | `order.placed` | Azimea recorded a new order on the contact's timeline. | When the order arrives, from any source. | Azimea | | `order.paid` | An order was paid. | When an order reaches the Paid status. | Azimea | | `order.cancelled` | An order was cancelled. | When an order reaches the Cancelled status. | Azimea | | `order.refunded` | An order was refunded. | When an order reaches the Refunded status. | Azimea | Flows start on `order.placed`. The other three are recorded on the contact’s timeline. ## Set it up in the dashboard [Section titled “Set it up in the dashboard”](#set-it-up-in-the-dashboard) 1. **Flows › Thank you after an order**: check the email and press **Publish**. It was created as a draft, marked as a service message, when you set up your first emails. 2. For a review request or recommendations, create a separate flow from **Flows › New workflow › Post-purchase follow-up**, keep its purpose **Marketing**, and publish it. 3. To bring back quiet customers, use **Win back inactive customers**: choose who it reaches in its **Audience & filters** step. ## Do it from your code [Section titled “Do it from your code”](#do-it-from-your-code) Shops on other platforms send orders as events; products send their own (`subscription.started`, for example) to `POST /events`. See [What you can build today](/build/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | ------------------------------------------------------------- | ----------------------------------------------------------------------------- | ------------------------------------------------------------------------- | | Buyers get no thank-you | The flow is not published. | Publish **Thank you after an order**. | | The thank-you is “not sent” for some buyers (`gate_no_basis`) | The flow is marked Marketing (flows created before the service default were). | If its email only informs, set **What it is for** to **Service message**. | | A review request is “not sent” (`gate_soft_opt_in_off`) | Your shop uses a consent checkbox, so buying alone does not allow marketing. | Expected: only buyers who ticked the box receive it. | | A buyer who unsubscribed still got the thank-you | It is a service message. | Expected; keep offers out of it. | ## Related [Section titled “Related”](#related) * [How flows work](/use/flows/) * [Carts and orders](/catalogs/carts-and-orders/) * [Why an email was not sent](/catalogs/why-an-email-was-not-sent/) # Campaigns > One email to many people, now or at a time you choose, with tests, a control group and approval. ## What it does and when to use it [Section titled “What it does and when to use it”](#what-it-does-and-when-to-use-it) A campaign is one email sent to many people: a newsletter, a launch, a sale. You choose who gets it and when; Azimea checks each person’s consent and sends it. Several emails can share one campaign (for example “Black Friday”, with a period and a goal), so they are planned and measured together. Use a campaign when the moment is yours to choose. When the moment depends on what each person did, use a [flow](/use/flows/). ## How it works [Section titled “How it works”](#how-it-works) 1. **Who it goes to**: a segment, or everyone who may receive email. You can leave out another segment, and anyone who got a campaign, flow or cart email in the last days (1 to 90), so nobody gets two in a row. 2. **The people are fixed when it starts.** Whoever joins the segment later does not get this email. 3. **Every person passes the [sending gate](/glossary/#sending-gate)**: a marketing campaign reaches subscribers, and existing customers where your checkout tells buyers you may write to them. A **service message** campaign (a change to your terms, an outage) reaches everyone with a working address; choosing it asks you to confirm it carries no offer. 4. **It goes out** now, at a time you set (at least 5 minutes and at most a year ahead), or at **each person’s best time**: within 24 hours of the start, each person gets it at the hour they usually open; people Azimea knows too little about get it at the start. ### Options [Section titled “Options”](#options) | Option | What it does | | ----------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **A/B test** | Tests the subject, the whole email or the sender name, with one or two other versions. A share of the audience (10 to 50%) gets the versions; after 1 to 24 hours the best one by opens, clicks, orders or revenue goes to everyone else. | | **Control group** | Keeps 5, 10 or 20% of the people out on purpose, the same people for every email of the campaign, so the report shows what the emails really added. | | **Link tags** | Adds Google Analytics tags to the links, so visits from the email show in your analytics. On unless you turn it off. | | **Approval** | A member’s campaign waits for an owner or admin; sending or scheduling it is the approval. Sent back, it returns as a draft with a note. | | **Pause** | Stops a campaign while it is going out; what has already left cannot be recalled. Resume whenever you like. | | **Resend** | After it has gone out, a copy goes as a new draft to the people it reached who did not open it (or did not click), by default three days later. | ## What each status means and what to do [Section titled “What each status means and what to do”](#what-each-status-means-and-what-to-do) ### The campaign [Section titled “The campaign”](#the-campaign) | Status or reason | What it means | What you do | | ---------------- | ------------------------------------------------------------------------- | -------------------------------------------------------------------- | | `Draft` | Being written: it can still be changed or deleted. | Finish it, then send it or schedule it. | | `Sending` | Its emails are being queued now; what already went out cannot be stopped. | Pause it if something is wrong: what is not yet queued waits. | | `Sent` | Every email reached the queue; delivery follows on its own. | Read its report; resend to people who did not open, if you want. | | `Failed` | The start stopped midway; the reason is written on the campaign. | Read the reason on the campaign; contact support if it is not clear. | | `Scheduled` | Waiting for its time. It is locked like a sent campaign. | Cancel the schedule to edit it again: it becomes a draft. | ### When a person does not get it [Section titled “When a person does not get it”](#when-a-person-does-not-get-it) The reasons a person in the audience does not get the email: | Status or reason | What it means | What you do | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sending.gate_bounced` | The person's address does not work any more. | Nothing: the address is no longer used. | | `sending.gate_complained` | The person marked one of your emails as spam. | Nothing: they are never written to again. | | `sending.gate_no_basis` | The person has not subscribed and is not an existing customer, so marketing may not reach them. | Expected for people who only typed their address at checkout. To reach them, ask for consent; or make a message about their own order a service email. | | `sending.gate_not_declared` | You have not yet declared how your shop collects consent, so no marketing goes out, even to subscribers. | Settings › Consent: choose your method and declare it. | | `sending.gate_soft_opt_in_off` | The person is an existing customer, but your shop uses a consent checkbox, not a notice at checkout, so buying alone does not allow marketing. | Expected with a checkbox: only those who ticked it receive marketing. Switch to notice at checkout only if your checkout really shows that notice. | | `sending.gate_topic_opted_out` | The person turned off the topic this email belongs to; they still receive your other topics. | Nothing: respect their choice. | | `sending.gate_unsubscribed` | The person unsubscribed from your marketing emails. | Nothing: they said no. Service emails still reach them. | After it leaves, each email moves through the [email statuses](/catalogs/email-statuses/): `Queued`, `Sent`, `Delivered` or `Bounced`. ## Events involved [Section titled “Events involved”](#events-involved) A campaign is not started by an event. Its results (opens, clicks, orders and revenue) are in the campaign’s report and in **Reports**. ## Set it up in the dashboard [Section titled “Set it up in the dashboard”](#set-it-up-in-the-dashboard) 1. **Campaigns › New campaign**: write the email or start from a template. 2. Choose **who gets it**, and what to leave out. 3. Choose **What it is for**: **Marketing**, or **Service message** for an email that only informs. 4. Optional: an A/B test, a control group, link tags. 5. **Send the campaign** now, or **Schedule** it; turn on **Send at each person’s best time** if you want. A member presses **Ask for approval** instead. ## Do it from your code [Section titled “Do it from your code”](#do-it-from-your-code) Campaigns are started from the dashboard. Your product can keep the audience current through the contacts API, and send one-to-one emails through `POST /sending/emails`. See [What you can build today](/build/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | --------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------- | | Far fewer recipients than contacts | Many contacts have no basis for marketing (they only typed their address at checkout, or bought in a shop that uses a checkbox). | Expected; grow consent with a sign-up form or the checkout box. | | No one received it, every email `Suppressed` with `gate_not_declared` | Consent is not declared. | Settings › Consent: choose your method and declare it. | | The A/B test has no winner yet | The wait you chose has not passed. | Wait; the rest of the audience gets the winner when it is chosen. | | Someone who joined the segment today did not get it | People are fixed when the campaign starts. | Send them a resend or add them to a flow. | ## Related [Section titled “Related”](#related) * [Why an email was not sent](/catalogs/why-an-email-was-not-sent/) * [Email statuses](/catalogs/email-statuses/) * [How flows work](/use/flows/) # Connect your store > Which shop platforms Azimea connects to, what connecting brings in, and how plugins, apps and keys work together. ## What it does and when to use it [Section titled “What it does and when to use it”](#what-it-does-and-when-to-use-it) Connecting your shop is the first thing to do in Azimea. It brings in your customers, guest buyers, orders and products, keeps them up to date on their own, and sends Azimea what happens in the shop as it happens: carts, emails typed at checkout, orders, sign-ups. Every flow that runs on its own (abandoned carts, welcome, after the order) starts from this connection. ## Platforms [Section titled “Platforms”](#platforms) | Platform | Status | How it connects | | --------------------------------------------------- | ----------- | -------------------------------------------------------------- | | [WooCommerce](/use/connect-your-store/woocommerce/) | Available | Two REST API keys + the free Azimea plugin | | [PrestaShop](/use/connect-your-store/prestashop/) | Available | A read-only webservice key + the free Azimea module | | [OpenCart](/use/connect-your-store/opencart/) | Available | The free Azimea extension, which does both reading and sending | | [Magento](/use/connect-your-store/magento/) | Available | An integration access token + the free Azimea module | | [Shopware](/use/connect-your-store/shopware/) | Available | The free Azimea app + a link code | | Shopify | Coming soon | — | | Gomag | Coming soon | — | | MerchantPro | Coming soon | — | For a platform marked “Coming soon”, open it in **Integrations** and press **Notify me when it’s ready**: you get one email the day it can be connected. ## How it works [Section titled “How it works”](#how-it-works) A connection has two halves that do different jobs: 1. **Azimea reads your shop** through a key you create in the shop’s admin (or, on Shopware, through the app). Right after connecting it brings in your customers, guest buyers and the **orders of the last 12 months**, then keeps them current: orders every **5 minutes**, customers every **15 minutes**, products every **6 hours**. The keys only read: Azimea never changes products, prices, stock or orders. 2. **Your shop tells Azimea what happens**, through the Azimea plugin, module, extension or app installed in it: every change to a cart (anonymous visitors included), the email typed at checkout, orders and their status, new accounts and newsletter sign-ups. These arrive within seconds, signed with your store’s secret. Without the second half, orders still sync, but **abandoned carts cannot be seen**: no shop platform lets anyone read carts from the outside, so the abandoned-cart flow does not start. | Kind | Platforms | What you give Azimea | What you install in the shop | | ------------- | -------------------------------- | --------------------------------------- | ------------------------------------------------------------- | | Keys + plugin | WooCommerce, PrestaShop, Magento | A key created in the shop’s admin | The plugin or module, with a store key and secret from Azimea | | Extension | OpenCart | The extension’s own read key and secret | The extension, with a store key and secret from Azimea | | App | Shopware | A link code shown by the app | The app; Azimea writes its keys into it by itself | ### The “See my cart” link [Section titled “The “See my cart” link”](#the-see-my-cart-link) Every plugin and app sends, with each cart, a link that **rebuilds that cart in any browser or device** and lands on the checkout. It is what the button in a cart reminder opens. The link is signed with your store’s secret, expires after 30 days, carries products only (never an address or a login), and does nothing for a cart that was already ordered. ### Consent at checkout [Section titled “Consent at checkout”](#consent-at-checkout) How your shop asks for marketing consent is chosen in Azimea, in **Settings › Consent**: a checkbox at checkout, a notice at checkout, consent you collect elsewhere, or no marketing. The plugin reads that choice and shows the right thing at checkout, with your own words. An email typed at checkout is never consent by itself: it only allows the reminder about that cart. See [consent statuses](/catalogs/consent-statuses/). ## What each status means and what to do [Section titled “What each status means and what to do”](#what-each-status-means-and-what-to-do) ### The store [Section titled “The store”](#the-store) | Status or reason | What it means | What you do | | ---------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------ | | `Active` | Connected and checked: orders, customers and products are kept in sync. | Nothing. | | `Error` | The last check failed: keys revoked, permission removed or the shop not answering. Syncing is paused. | Open the shop in Integrations, read the error, fix the keys or the shop, then press Check. | | `Disconnected` | You disconnected it; its keys were deleted and its running imports stopped. Its contacts and orders stay in Azimea; its products are no longer shown in emails. | Connect it again with new keys when you want syncing back. | ### The plugin [Section titled “The plugin”](#the-plugin) The store’s page in Azimea shows, under **Store plugin**, whether the plugin is talking to Azimea: | Shown | What it means | What you do | | --------------------------- | ------------------------------------------------------- | ----------------------------------------------------------- | | Waiting for the first event | The keys exist, but nothing has arrived yet. | Add a product to the cart on your site. | | Connected · last event … | Events arrive. | Nothing. | | Nothing new since … | No event for more than a day. Normal for a quiet shop. | If the shop had visitors, check that the plugin is active. | | … deliveries refused | The keys in the plugin do not match the ones in Azimea. | Paste the store key and secret again, or generate new ones. | ## Set it up in the dashboard [Section titled “Set it up in the dashboard”](#set-it-up-in-the-dashboard) 1. Open **Integrations** and choose your platform. 2. Press **Connect your store** and follow the steps on the platform’s page in these docs. 3. On the platform’s page in Azimea, under **Store plugin**, press **Generate keys** and paste the store key and secret into the plugin (not needed on Shopware). The secret is shown only once. 4. Add a product to the cart on your site: the plugin status turns to **Connected**. Each plan includes a number of connected stores; one Azimea account can connect several shops, each with its own keys. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) These answers apply to every platform; each platform’s page lists its own. | Symptom | Likely cause | Fix | | -------------------------------------------------- | ---------------------------------------------------------------------------- | -------------------------------------------------------------------------- | | `stores.url_not_https` | The address starts with `http://`. | Use the `https://` address; keys are never sent unencrypted. | | `stores.url_redirects` / `stores.url_redirects_to` | The address you typed redirects elsewhere (often `www.` or a language path). | Use the final address the site opens at. | | `stores.unreachable` / `stores.timeout` | The site is down, or a firewall blocks Azimea. | Check that the site opens from outside; ask your host to allow Azimea. | | `stores.already_connected` | This shop is already connected in your account. | Use **Check** next to the store, or disconnect it first to use other keys. | | `stores.limit_reached` | Your plan’s number of stores is used up. | Disconnect one, or move to a larger plan. | Every code is listed in [error codes](/catalogs/error-codes/). ## Related [Section titled “Related”](#related) * [Recover abandoned carts](/use/recover-abandoned-carts/) * [Events from your shop](/catalogs/events/) * [Carts and orders](/catalogs/carts-and-orders/) # Magento > Connect a Magento 2, Adobe Commerce or Mage-OS store with an integration token and the free Azimea module. ## What it brings [Section titled “What it brings”](#what-it-brings) * **Customers with an account** and **guest buyers** (taken from their orders), as contacts. * **Orders of the last 12 months** and **products** with price and stock, read through the Magento REST API. * **Carts as they happen**, anonymous ones included, with the email typed at checkout, sent by the module. * **Newsletter consent** with its proof. ## Requirements [Section titled “Requirements”](#requirements) | What | Why | | ----------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------- | | Magento 2.4.4 or later, Adobe Commerce, or Mage-OS 1.0+; PHP 8.1+ with the curl extension | The versions the module runs on. | | Command-line access | To run `bin/magento setup:upgrade`, as for any module. | | Magento’s cron running | For retries. Without it, events still go out; only the retries wait for the next visit. | | The site on HTTPS | Keys are never sent unencrypted. | | Admin access | To create the integration and configure the module. | ## How it works [Section titled “How it works”](#how-it-works) * **Reading:** an integration with read-only API resources gives Azimea an access token for customers, orders, products and stores. It cannot change anything. * **Sending:** the module sends every cart change, the email typed at checkout, orders and their status changes, accounts and newsletter consent. Magento’s order states become the common statuses: new, pending payment, payment review and on hold as `pending`; processing as `paid`; complete as `completed`; canceled as `cancelled`; closed as `refunded`. * Events leave at the end of the request that produced them; Magento’s cron delivers the retries. ## Install and connect [Section titled “Install and connect”](#install-and-connect) ### 1. Create the integration [Section titled “1. Create the integration”](#1-create-the-integration) 1. In the admin, go to **System › Integrations**, press **Add New Integration** and name it `Azimea`. 2. Under **API**, tick Customers, Sales › Operations › Orders › Actions › View, Catalog › Inventory › Products and Stores › Settings › All Stores; optionally Categories and Sources. 3. Save, then press **Activate** and **Allow**, and copy the **Access Token**. 4. In **Stores › Configuration › Services › OAuth**, set “Allow OAuth Access Tokens to be used as standalone Bearer tokens” to **Yes**. ### 2. Connect in Azimea [Section titled “2. Connect in Azimea”](#2-connect-in-azimea) 1. Open **Integrations › Magento › Connect your store**. 2. Paste your store’s address and the access token, then press **Check and connect**. ### 3. Install the module [Section titled “3. Install the module”](#3-install-the-module) 1. Download it: [azimea-magento.zip](https://azimea.com/downloads/azimea-magento.zip) and unzip it in the Magento root; it lands in `app/code/Azimea/Connector`. 2. Run: ```bash bin/magento module:enable Azimea_Connector bin/magento setup:upgrade bin/magento setup:di:compile # production mode only bin/magento setup:static-content:deploy # production mode only bin/magento cache:flush ``` 3. In Azimea, on the Magento page, under **Store plugin**, press **Generate keys** and copy the store key and the secret (shown only once). 4. In the admin, open **Stores › Configuration › Azimea**, paste both and save. The **Status** line should say connected. 5. Add a product to the cart on your store: the plugin status in Azimea turns to **Connected**. The key and the secret can stay out of the database: set `AZIMEA_STORE_KEY` and `AZIMEA_SECRET` in the PHP environment, and the settings screen says they come from the server. ## Statuses [Section titled “Statuses”](#statuses) The store and plugin statuses are explained in the [overview](/use/connect-your-store/); order statuses in [carts and orders](/catalogs/carts-and-orders/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | ------------------------------------ | ------------------------------------------------------------------------------------ | ----------------------------------------------------------------- | | `stores.magento_token_format` | The token was not copied whole: it is 32 letters and digits. | Copy it again from the integration. | | `stores.magento_token_rejected` | Magento refused the token: wrong token, or standalone Bearer tokens are not allowed. | Check the token, and set the OAuth option to **Yes**. | | `stores.magento_permission_missing` | The integration cannot read one of the resources (named in the message). | Tick it in the integration’s API resources and save. | | `stores.magento_api_not_found` | The REST API did not answer at this address. | Check the store’s address. | | Orders sync but no carts arrive | The module is not installed, or its key and secret are missing. | Install it and paste the keys in Stores › Configuration › Azimea. | | Plugin status “… deliveries refused” | The store key or secret in the module is not the current one. | Generate new keys in Azimea and paste them again. | All codes: [error codes](/catalogs/error-codes/). ## Disconnect [Section titled “Disconnect”](#disconnect) In Azimea, press **Disconnect** next to the store. In Magento, deactivate the `Azimea` integration and disable the module. ## Related [Section titled “Related”](#related) * [Connect your store: overview](/use/connect-your-store/) * [Recover abandoned carts](/use/recover-abandoned-carts/) # OpenCart > Connect an OpenCart 4 shop with the free Azimea extension, which both reads and sends. ## What it brings [Section titled “What it brings”](#what-it-brings) * **Customers with an account** and **guest buyers** (taken from their orders), as contacts. * **Orders of the last 12 months** and **products** with price and stock. * **Carts as they happen**, anonymous ones included, with the email typed at checkout. * **Newsletter consent** with its proof. ## Requirements [Section titled “Requirements”](#requirements) | What | Why | | -------------------------------------------------------- | ---------------------------------------------------------------- | | OpenCart 4.0.2.x or 4.1.x, PHP 8.1+ | The versions the extension runs on. OpenCart 3 is not supported. | | The site on HTTPS | Keys are never sent unencrypted. | | An admin account that can use **Extensions › Installer** | To install the extension. | | Optional: a cron every 1 to 5 minutes | Quicker retries; its address is on the extension’s page. | ## How it works [Section titled “How it works”](#how-it-works) OpenCart has no API for customers, orders and products, so the Azimea extension does both halves of the connection: * **Reading:** it opens a read address that only Azimea can use, with signed requests and a read key and secret made in the extension. That address only reads. * **Sending:** with **Send events** switched on, it sends every cart change, the email typed at checkout, orders and their status changes, accounts, and newsletter consent. An order is announced when it gets its first real status (after payment or the merchant’s confirmation), not when the checkout writes it. * It works only through OpenCart’s event system, with no OCMOD patches: its events are registered on install and removed on uninstall. Retries go out on later visits, at most every 30 seconds, or from the cron. ## Install and connect [Section titled “Install and connect”](#install-and-connect) ### 1. Install the extension [Section titled “1. Install the extension”](#1-install-the-extension) 1. Download it: [azimea-opencart.ocmod.zip](https://azimea.com/downloads/azimea-opencart.ocmod.zip). 2. In the admin, upload it in **Extensions › Installer**. 3. In **Extensions › Extensions › Modules**, install **Azimea** and open it. ### 2. Connect in Azimea [Section titled “2. Connect in Azimea”](#2-connect-in-azimea) 1. In the extension, under **What Azimea reads from the shop**, copy the store address, the **Read key** (`azr_…`) and the **Read secret** (`azs_…`). 2. In Azimea, open **Integrations › OpenCart › Connect your store**, paste the three and press **Check and connect**. ### 3. Switch on the events [Section titled “3. Switch on the events”](#3-switch-on-the-events) 1. In Azimea, on the OpenCart page, under **Store plugin**, press **Generate keys** and copy the store key and the secret (shown only once). 2. Back in the extension, paste both, switch on **Send events**, save and press **Test connection**. 3. Add a product to the cart on your site: the plugin status in Azimea turns to **Connected**. The store key, the secret and the events address can also be fixed in `config.php` / `admin/config.php` or the environment (`AZIMEA_STORE_KEY`, `AZIMEA_SECRET`). ## Statuses [Section titled “Statuses”](#statuses) The store and plugin statuses are explained in the [overview](/use/connect-your-store/); order statuses in [carts and orders](/catalogs/carts-and-orders/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | -------------------------------------------------------------- | --------------------------------------------------------------------------------- | ------------------------------------------------------------------- | | `stores.opencart_extension_not_found` | The extension did not answer at this address. | Check the address, and that the extension is installed and enabled. | | `stores.opencart_key_format` / `stores.opencart_secret_format` | The read key or secret was not copied whole (`azr_…`, `azs_…`). | Copy them again from the extension. | | `stores.opencart_keys_rejected` | The shop refused the read keys, often because new ones were made since. | Copy both again: making new read keys stops the old ones. | | Orders sync but no carts arrive | **Send events** is off, or the store key and secret are missing in the extension. | Paste them and switch **Send events** on. | | Plugin status “… deliveries refused” | The store key or secret in the extension is not the current one. | Generate new keys in Azimea and paste them again. | All codes: [error codes](/catalogs/error-codes/). ## Disconnect [Section titled “Disconnect”](#disconnect) In Azimea, press **Disconnect** next to the store. In OpenCart, make new read keys in the extension (the old ones stop at once) or uninstall it; uninstalling removes its events and leaves nothing behind. ## Related [Section titled “Related”](#related) * [Connect your store: overview](/use/connect-your-store/) * [Recover abandoned carts](/use/recover-abandoned-carts/) # PrestaShop > Connect a PrestaShop 8 or 9 shop with a read-only webservice key and the free Azimea module. ## What it brings [Section titled “What it brings”](#what-it-brings) * **Customers with an account** and **guest buyers** (taken from their orders), as contacts. * **Orders of the last 12 months** and **products** with price and stock, read through the webservice. * **Carts as they happen**, anonymous ones included, with the email typed at checkout, sent by the module. * **Newsletter consent** with its proof: PrestaShop’s own newsletter box, or the Azimea checkbox if you choose it. ## Requirements [Section titled “Requirements”](#requirements) | What | Why | | ------------------------------- | ------------------------------------------------------- | | PrestaShop 8.0 to 9.x, PHP 8.1+ | The versions the module runs on. | | The webservice switched on | Azimea reads customers, orders and products through it. | | The site on HTTPS | Keys are never sent unencrypted. | | An admin account | To create the webservice key and install the module. | ## How it works [Section titled “How it works”](#how-it-works) * **Reading:** a webservice key that can only **view** lets Azimea read customers, orders, order states, currencies, settings, products, categories and stock. It cannot change anything. * **Sending:** the module sends every cart change, the email typed at checkout, orders and their status changes, new and changed accounts, and newsletter consent, the moment they happen. Order states are translated into the common statuses: cancelled, refunded and payment-error states as such, a shipped or delivered state as `completed`, a paid state as `paid`, anything else (cash on delivery waiting, for example) as `pending`. * Events are queued in the shop first and leave at the end of the request, after the visitor has the page where the server allows it. Failed deliveries are retried after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours. ## Install and connect [Section titled “Install and connect”](#install-and-connect) ### 1. Create the webservice key [Section titled “1. Create the webservice key”](#1-create-the-webservice-key) 1. In the back office, go to **Advanced Parameters › Webservice** and switch on **Enable PrestaShop’s webservice**. 2. Press **Add new webservice key**, then **Generate**, and set the description to `Azimea`. 3. Tick **View (GET)** for `customers`, `orders`, `order_states`, `currencies`, `configurations`, `products`, `categories` and `stock_availables`: nothing else. 4. Save and copy the key (32 letters and digits). ### 2. Connect in Azimea [Section titled “2. Connect in Azimea”](#2-connect-in-azimea) 1. Open **Integrations › PrestaShop › Connect your store**. 2. Paste your shop’s address and the webservice key, then press **Check and connect**. ### 3. Install the module [Section titled “3. Install the module”](#3-install-the-module) 1. Download the module: [azimea-prestashop.zip](https://azimea.com/downloads/azimea-prestashop.zip). 2. In the back office, go to **Modules › Module Manager › Upload a module** and upload the zip. 3. In Azimea, on the PrestaShop page, under **Store plugin**, press **Generate keys** and copy the store key and the secret (shown only once). 4. Open the module’s **Configure** page, paste both and press **Test connection**. The page also shows the last delivery, anything waiting and the consent method chosen in Azimea. 5. Recommended: call the cron address shown on the module’s page every 5 minutes. Without it, events still leave at the end of each request; only retries wait for later visits. The key, the secret and the events address can also come from the environment or from constants in `config/defines_custom.inc.php` (`AZIMEA_STORE_KEY`, `AZIMEA_SECRET`); the page then shows them locked. ## Statuses [Section titled “Statuses”](#statuses) The store and plugin statuses are explained in the [overview](/use/connect-your-store/); order statuses in [carts and orders](/catalogs/carts-and-orders/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | ------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------- | | `stores.prestashop_key_format` | The key was not copied whole: it is 32 letters and digits. | Copy it again. | | `stores.prestashop_key_rejected` | PrestaShop refused the key: copied wrong, or its status is off. | Check the key and switch its status on. | | `stores.prestashop_webservice_disabled` | The webservice is switched off. | Advanced Parameters › Webservice › switch it on. | | `stores.prestashop_webservice_not_found` | Nothing answered at this address. | Check the address and that the webservice is on. | | `stores.prestashop_permission_missing` | The key cannot read one of the resources (named in the message). | Tick **View (GET)** for it on the key and try again. | | Newsletter sign-ups from the footer block do not arrive | Only a signed-in customer’s own address, or one confirmed through the block’s email link, is sent: anyone can type any address in that form. | Turn on the newsletter block’s “verification email”. | | Plugin status “… deliveries refused” | The store key or secret in the module is not the current one. | Generate new keys in Azimea and paste them again. | All codes: [error codes](/catalogs/error-codes/). ## Disconnect [Section titled “Disconnect”](#disconnect) In Azimea, press **Disconnect** next to the store. In PrestaShop, switch off or delete the `Azimea` webservice key and uninstall the module; uninstalling removes its tables and settings. ## Related [Section titled “Related”](#related) * [Connect your store: overview](/use/connect-your-store/) * [Recover abandoned carts](/use/recover-abandoned-carts/) # Shopware > Connect a Shopware 6 shop with the free Azimea app and a link code. ## What it brings [Section titled “What it brings”](#what-it-brings) * **Customers with an account** and **guest buyers** (taken from their orders), as contacts. * **Orders of the last 12 months** and **products** with price and stock, read through the Shopware Admin API. * **New orders and their status changes**, accounts and confirmed newsletter subscriptions, through the app’s webhooks. * **Carts as they happen**, with the email typed at checkout, through the app’s storefront script. ## Requirements [Section titled “Requirements”](#requirements) | What | Why | | -------------------------------------------------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | Shopware 6.7, on your own server | The app is tested on self-hosted Shopware 6.7. Shopware’s cloud installs apps from the Shopware Store, where the Azimea app is not listed yet. | | An admin account that can upload and activate extensions | To install the app. | | The site on HTTPS | Keys are never sent unencrypted. | | The link code used within 30 days | After that it expires; reinstalling the app gives a new one. | ## How it works [Section titled “How it works”](#how-it-works) * It is an **app, not a plugin**: no PHP code of Azimea runs in your shop. On install you see the permissions it asks for: read access to customers, orders and products, plus its own settings. * **Reading:** when the app is installed, Shopware registers your shop with Azimea and gives it read-only Admin API keys. Azimea reads customers, orders and products with them. * **Sending:** new orders, status changes, accounts and confirmed newsletter subscriptions reach Azimea through the app’s webhooks; carts and the email typed at checkout through its storefront script, which is never cached and hashes the cart’s identifier in the browser before it leaves. * **Consent** comes from Shopware’s own newsletter, once confirmed. An email typed at checkout never counts as consent. * Shopware orders do not say which cart they came from, so Azimea closes the cart of the same email address when it orders within 7 days. ## Install and connect [Section titled “Install and connect”](#install-and-connect) 1. Download the app: [azimea-shopware.zip](https://azimea.com/downloads/azimea-shopware.zip). 2. In the Shopware admin, go to **Extensions › My extensions › Upload extension**, upload the zip, then install and activate the app. 3. Open **Extensions › My extensions › Azimea › Configure** and copy the **Link code**; it looks like `AZM-XXXX-XXXX-XXXX`. 4. In Azimea, open **Integrations › Shopware › Connect your store**, paste the link code with your shop’s address and press **Check and connect**. 5. That is all: Azimea writes the store key into the app itself and clears the code. Put a product in the cart on your shop and it appears in Azimea. There are no plugin keys to copy for Shopware: the app receives them from Azimea. ## Statuses [Section titled “Statuses”](#statuses) The store and plugin statuses are explained in the [overview](/use/connect-your-store/); order statuses in [carts and orders](/catalogs/carts-and-orders/). Deactivating the app in Shopware puts the store in `Error`; activating it again brings it back. ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | ------------------------------------ | --------------------------------------------------------------------------- | ----------------------------------------------------------------------- | | `stores.shopware_link_code_invalid` | The code does not match a Shopware shop at this address, or it expired. | Copy it again from the app’s settings; reinstall the app for a new one. | | `stores.shopware_not_confirmed` | The shop has not finished installing the app. | Install the app again in Shopware. | | `stores.shopware_app_deactivated` | The app is deactivated. | Activate it in Extensions › My extensions. | | `stores.shopware_keys_rejected` | Shopware no longer accepts the app’s keys. | Reinstall the app, then check the connection. | | `stores.shopware_permission_missing` | The app is not allowed to read one of the resources (named in the message). | Update the app in Shopware and accept its permissions. | | `stores.shopware_api_not_found` | The Admin API did not answer at this address. | Use your shop’s own address. | All codes: [error codes](/catalogs/error-codes/). ## Disconnect [Section titled “Disconnect”](#disconnect) Removing the app in Shopware disconnects the shop at once; nothing else is needed. You can also press **Disconnect** next to the store in Azimea. Contacts and orders brought in so far stay in your account. ## Related [Section titled “Related”](#related) * [Connect your store: overview](/use/connect-your-store/) * [Recover abandoned carts](/use/recover-abandoned-carts/) # WooCommerce > Connect a WooCommerce shop with two REST API keys and the free Azimea plugin. ## What it brings [Section titled “What it brings”](#what-it-brings) * **Registered customers** and **guest buyers** (taken from their orders), as contacts. * **Orders of the last 12 months**, with their products and value, then every new order and status change. * **Products**, with price and stock, for the recommendations in your emails. * **Carts as they happen**, anonymous ones included, with the email typed at checkout: this is where the abandoned-cart flow starts. Carts come only from the plugin. ## Requirements [Section titled “Requirements”](#requirements) | What | Why | | ----------------------------------------------------------------- | ------------------------------------------------------- | | WooCommerce with REST API v3 (WooCommerce 3.5 or later) | Azimea reads customers, orders and products through it. | | For the plugin: WordPress 6.4+, WooCommerce 8.2+, PHP 8.1+ | The plugin sends carts and events. | | The site on HTTPS | Keys are never sent unencrypted. | | Permalinks other than “Plain” (WordPress › Settings › Permalinks) | With “Plain”, the REST API has no address. | | A WordPress administrator account | Only an administrator can create REST API keys. | ## How it works [Section titled “How it works”](#how-it-works) * **Reading:** with the REST API keys, Azimea reads customers, orders and products, the same way on every sync. * **Sending:** the Azimea plugin sends the cart whenever it changes, the email typed at checkout (on the classic **and** the block checkout), orders and their status, accounts, and the consent your checkout collects. * Nothing is sent while a shopper’s page loads: events wait in the plugin’s own table and leave with Action Scheduler, in batches of up to 100. If Azimea cannot be reached, the plugin retries after 1 minute, 5 minutes, 30 minutes, 2 hours and 6 hours, then says so on its settings screen. * The plugin supports High-Performance Order Storage, and consent texts per language with WPML or Polylang. ## Install and connect [Section titled “Install and connect”](#install-and-connect) ### 1. Create the REST API keys [Section titled “1. Create the REST API keys”](#1-create-the-rest-api-keys) 1. In WordPress, go to **WooCommerce › Settings › Advanced › REST API**. 2. Press **Add key**, set the description to `Azimea`. 3. Under permissions choose **Read/Write**, as Azimea’s connect screen asks, and generate the key. 4. Keep the page open: you need the **Consumer key** (`ck_…`) and the **Consumer secret** (`cs_…`). ### 2. Connect in Azimea [Section titled “2. Connect in Azimea”](#2-connect-in-azimea) 1. In Azimea, open **Integrations › WooCommerce › Connect your store**. 2. Paste your shop’s address (`https://…`), the consumer key and the consumer secret, then press **Check and connect**. 3. Azimea checks the keys on your shop on the spot and starts bringing in customers and orders. You can leave the screen: it continues on the server. ### 3. Install the plugin [Section titled “3. Install the plugin”](#3-install-the-plugin) 1. Download the plugin: [azimea-for-woocommerce.zip](https://azimea.com/downloads/azimea-for-woocommerce.zip). 2. In WordPress, go to **Plugins › Add New Plugin › Upload Plugin**, upload the zip, then **Activate**. 3. In Azimea, on the WooCommerce page, under **Store plugin**, press **Generate keys**. Copy the store key (`str_…`) and the secret (`whsec_…`): the secret is shown only once. 4. In WordPress, open **WooCommerce › Settings › Azimea**, paste both and press **Test connection**. 5. Add a product to the cart on your site. In Azimea the plugin status turns to **Connected**. To keep the secret out of the database, define `AZIMEA_STORE_KEY` and `AZIMEA_SECRET` in `wp-config.php`; the settings screen then shows both as locked. ## Statuses [Section titled “Statuses”](#statuses) The store and plugin statuses are explained in the [overview](/use/connect-your-store/). Orders keep WooCommerce’s own states on the shop’s side; in Azimea they read as the common statuses in [carts and orders](/catalogs/carts-and-orders/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | -------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | `stores.consumer_key_format` / `stores.consumer_secret_format` | The key was not copied whole. A key starts with `ck_`, a secret with `cs_`, both 43 characters. | Copy them again from WooCommerce. | | `stores.credentials_rejected` | WooCommerce refused the keys: revoked, or for another site. | Create a new key pair and connect again. | | `stores.rest_api_not_found` | No REST API at this address: WooCommerce inactive, or permalinks set to “Plain”. | Check the address, activate WooCommerce, change the permalinks. | | `stores.not_a_woocommerce_api` | Something answered, but not WooCommerce: a security plugin or a firewall blocks the API. | Allow `/wp-json/wc/v3/` in the security plugin or the firewall. | | Plugin status “… deliveries refused” (`ingestion.signature_invalid`) | The store key or secret in the plugin is not the current one. | Generate new keys in Azimea and paste them again. | | Plugin status stays “Waiting for the first event” | Nobody has used the cart yet, or the site blocks its own background requests. | Add a product to the cart; the plugin then delivers on the next visit. | All codes: [error codes](/catalogs/error-codes/). ## Disconnect [Section titled “Disconnect”](#disconnect) In Azimea, press **Disconnect** next to the store: its keys are deleted and syncing stops; contacts and orders stay. In WooCommerce, revoke the `Azimea` key under **REST API** and deactivate the plugin. Deleting the plugin removes its table, its settings and the keys it added to orders. ## Related [Section titled “Related”](#related) * [Connect your store: overview](/use/connect-your-store/) * [Recover abandoned carts](/use/recover-abandoned-carts/) # Consent and GDPR > How you collect consent, why nothing goes out until you declare it, double opt-in, topics, unsubscribe and erasure. ## What it does and when to use it [Section titled “What it does and when to use it”](#what-it-does-and-when-to-use-it) Marketing email in the EU needs a legal basis for every person: they subscribed, or they are an existing customer who was told you may write to them. Azimea keeps that basis for each contact, with its proof, and checks it before every email. Use this page when you set up a store, when emails are not going out, or when someone asks what you hold about them or wants it gone. ## How it works [Section titled “How it works”](#how-it-works) ### 1. You choose how each store collects consent [Section titled “1. You choose how each store collects consent”](#1-you-choose-how-each-store-collects-consent) | Method | What happens at checkout | Who may receive marketing | | -------------------------------------- | -------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------- | | **Checkbox at checkout** (recommended) | An unticked box with your own wording | Whoever ticks it becomes a subscriber; whoever does not gets no marketing, only service emails and cart reminders | | **Notice at checkout** | No box, a visible notice that you may write about similar products and they can object at any time | Buyers become existing customers (soft opt-in): similar products, with an unsubscribe link in every email | | **I collect consent elsewhere** | Another plugin’s checkbox or your theme’s form | Consent reaches Azimea as an explicit opt-in; you declare that you hold the proof | | **No marketing** | — | No campaigns or flows; service emails and cart reminders remain | ### 2. You declare it [Section titled “2. You declare it”](#2-you-declare-it) Choosing a method is not enough: you **declare** that you collect consent that way and take responsibility for the legal basis. The declaration is recorded with your name and the date in the audit log. **Until a store’s method is declared, no marketing campaign or flow email goes out to anyone in that store**, not even to subscribers; it is stopped with the reason `gate_not_declared`. Changing the method clears the declaration: what you declared for one method does not cover another. Contacts that do not belong to a store (an imported file, your API) carry their own basis instead: the choice and the confirmation you made when importing them. ### 3. Optionally, people confirm by email (double opt-in) [Section titled “3. Optionally, people confirm by email (double opt-in)”](#3-optionally-people-confirm-by-email-double-opt-in) With the checkbox, or with consent collected elsewhere, you can ask people to confirm. After ticking the box they get an email and become subscribers only once they click the link (valid for 7 days). Until then they are `PendingConfirmation`: cart reminders still reach them, marketing waits, and the welcome email waits for the click. It means fewer fake addresses and a stronger proof, and is expected in practice in Germany and Austria. ### 4. Every change is kept, with its proof [Section titled “4. Every change is kept, with its proof”](#4-every-change-is-kept-with-its-proof) Each time a contact’s consent changes, Azimea keeps a row in their **consent history**: the new status, where it came from (checkout, a form, an import, the unsubscribe link…), the time, and the proof the shop sent: the text next to the checkbox, its version, the language and the IP address. The history cannot be edited. ### 5. Marketing or service [Section titled “5. Marketing or service”](#5-marketing-or-service) Every campaign and flow is either **marketing** (the default) or **service**. A service message is about the person’s own order or account, with no offer in it; it does not need marketing consent and still reaches someone who unsubscribed. Choosing service is a promise you confirm: that the email carries no offer. ### 6. Topics and the preference page [Section titled “6. Topics and the preference page”](#6-topics-and-the-preference-page) **Topics** (“Newsletter”, “Promotions”) let a person turn off part of your marketing without leaving all of it. Choose a topic on a campaign or a flow; whoever turned it off is left out of that marketing. Every marketing email links to a **preference page** where the person sees your topics and turns them on or off, or leaves all your marketing. ### 7. Unsubscribe is one click [Section titled “7. Unsubscribe is one click”](#7-unsubscribe-is-one-click) Every marketing email carries an unsubscribe link in its footer and the one-click unsubscribe header mail apps show next to the sender. One click and the person is `Unsubscribed`: marketing stops at once, service emails continue. ## What each status means and what to do [Section titled “What each status means and what to do”](#what-each-status-means-and-what-to-do) ### Consent statuses [Section titled “Consent statuses”](#consent-statuses) | Status or reason | What it means | What you do | | --------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `Unknown` | Azimea has no basis to send this person marketing: imported or added without a consent. | Only service emails reach them. To send more, collect consent (a form, a checkbox) and import it with its proof. | | `SoftOptIn` | An existing customer, in a shop whose checkout tells buyers it may write to them about similar products. | Campaigns and flows reach them once your consent method is declared as notice at checkout. Every email carries an unsubscribe link. | | `Subscribed` | They ticked a box or signed up to hear from you; the proof is kept in their consent history. | Everything reaches them, once you have declared how you collect consent (Settings › Consent). | | `CheckoutStarted` | They typed their address while placing an order and did nothing more: no purchase yet, no tick. | Only the reminder about that cart, and service emails. They become a subscriber if they tick the newsletter box. | | `PendingConfirmation` | They asked to subscribe and have not clicked the confirmation link yet (double opt-in). | Nothing to do: the confirmation email is on its way. Cart reminders still reach them; marketing waits for the click. | | `Unsubscribed` | They said they do not want your marketing emails. | Nothing: only service emails about their own orders or account still reach them. | | `Bounced` | Their address permanently refused emails: it no longer exists or no longer accepts them. | Nothing: Azimea stops writing to it. A new, working address can be added as a new contact. | | `Complained` | They marked one of your emails as spam. | Nothing: they never receive anything again, because each complaint hurts the reputation of every email you send. | ### When consent stops an email [Section titled “When consent stops an email”](#when-consent-stops-an-email) | Status or reason | What it means | What you do | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sending.gate_bounced` | The person's address does not work any more. | Nothing: the address is no longer used. | | `sending.gate_complained` | The person marked one of your emails as spam. | Nothing: they are never written to again. | | `sending.gate_no_basis` | The person has not subscribed and is not an existing customer, so marketing may not reach them. | Expected for people who only typed their address at checkout. To reach them, ask for consent; or make a message about their own order a service email. | | `sending.gate_not_declared` | You have not yet declared how your shop collects consent, so no marketing goes out, even to subscribers. | Settings › Consent: choose your method and declare it. | | `sending.gate_soft_opt_in_off` | The person is an existing customer, but your shop uses a consent checkbox, not a notice at checkout, so buying alone does not allow marketing. | Expected with a checkbox: only those who ticked it receive marketing. Switch to notice at checkout only if your checkout really shows that notice. | | `sending.gate_topic_opted_out` | The person turned off the topic this email belongs to; they still receive your other topics. | Nothing: respect their choice. | | `sending.gate_unsubscribed` | The person unsubscribed from your marketing emails. | Nothing: they said no. Service emails still reach them. | ## Events involved [Section titled “Events involved”](#events-involved) | Event | What it does to consent | | --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | `contact.identified` | Adds the person as `CheckoutStarted`, never as a subscriber, whatever the page claims | | `subscriber.created` | `Subscribed` when the person ticked a box, with the proof; otherwise only keeps the address | | `customer.upserted` | `Subscribed` only with an explicit tick; an account alone gives no marketing | | `order.placed` | Arrives with the tick given at checkout, if any (sent as `subscriber.created`); once paid, the buyer becomes an existing customer where the method is notice at checkout | | `subscriber.removed` | `Unsubscribed` | | `contact.erasure_requested` | Erases the person | See the [events catalog](/catalogs/events/). ## Set it up in the dashboard [Section titled “Set it up in the dashboard”](#set-it-up-in-the-dashboard) 1. **Settings › Consent**: choose the store, choose the method, optionally turn on double opt-in, **Save choice**. 2. Read the statement and **Declare**. From now on marketing can go out from this store. 3. Optional: in the same page, add **topics** with a name and a short description people read on their preference page. 4. On each campaign and flow, keep it marketing or mark it as service, and pick a topic if it has one. **Answering a person who asks:** * *What do you hold about me?* Contacts › the person › **Export data**. * *Delete me.* Contacts › the person › **Erase data**: their name, address and phone disappear, here and from their orders; orders stay as figures without a person; their consent history is reduced to one row with nothing personal; the next import will not bring them back. It cannot be undone. * *Stop writing to me.* The unsubscribe link does it; you do not need to do anything. ## Do it from your code [Section titled “Do it from your code”](#do-it-from-your-code) `POST /contacts` accepts the consent basis with its proof (the text, its version, the language, the IP, the time) and can unsubscribe or resubscribe someone with proof of their new consent. See [what you can build today](/build/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | ----------------------------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------- | | No campaign or flow email goes out | The store’s method is not declared (`gate_not_declared`). | Settings › Consent › Declare. | | Customers who bought receive no marketing | The store uses the checkbox; buying alone does not allow marketing (`gate_soft_opt_in_off`). | Expected. Switch to notice at checkout only if your checkout really shows that notice. | | New subscribers get no welcome email | Double opt-in is on and they have not confirmed. | Expected: it goes out after they click. | | Someone unsubscribed still got an email | It was a service email (about their order or account). | Expected; marketing stopped. | ## Related [Section titled “Related”](#related) * [Consent statuses](/catalogs/consent-statuses/) * [Why an email was not sent](/catalogs/why-an-email-was-not-sent/) * [Contacts and imports](/use/contacts-and-imports/) # Contacts and imports > Where contacts come from, what Azimea keeps about them, how to import a list, and how to erase or export someone. ## What it does and when to use it [Section titled “What it does and when to use it”](#what-it-does-and-when-to-use-it) **Contacts** is everyone Azimea knows: the people your emails go to. Each contact has an email address, their details, your own fields, tags, their orders and activity, and a [consent status](/catalogs/consent-statuses/) that decides what you may send them. Use this page when you bring an existing list into Azimea, when you want to know what Azimea keeps about a person, or when someone asks you to erase or hand over their data. ## How it works [Section titled “How it works”](#how-it-works) Contacts arrive on their own from several places; you rarely add them by hand. | Where they come from | What arrives | Consent it brings | | ------------------------------------- | --------------------------------------------------------------------------- | ------------------------------------------------------------------ | | Your shop’s customer list | Customers with an account, with their details | What the shop knows: subscribers stay subscribers | | Your shop’s orders | Buyers, including guests without an account | A paid order makes them a paying customer | | The shop plugin or app, as it happens | Someone typing their email at checkout, a newsletter sign-up, a new account | `CheckoutStarted` at checkout; `Subscribed` only with a ticked box | | A file you import | Any list from a spreadsheet | What you choose for the whole file, with your confirmation | | Your own systems, through the API | Contacts and their fields | What you send, with its proof | A contact is one email address per store. The same person arriving again from another source is **updated**, never duplicated: a newer name replaces an older one, an empty value never erases a known one, and consent only ever moves **up** (from `CheckoutStarted` to `Subscribed`, never back down by an import). **Your own fields** hold what you know beyond name and email: a plan, a customer level, a date. Create them in **Settings › Contact fields** (text, number, yes/no, date, date and time, list) or while importing. **Tags** are free labels, added by hand on a contact or by a flow’s Tag step; both can be used in [segments](/use/segments/). ### Importing a file [Section titled “Importing a file”](#importing-a-file) 1. **Contacts › Import contacts**, then choose a CSV or Excel file. The first row holds the column names. Up to **1,000,000 rows or 100 MB**; a CSV is much smaller than the same list in Excel. 2. **Map each column**: Email (required, exactly one column), First name, Last name, Phone, City, Country code, Language, Tags, one of your fields, a new field, or “Don’t import”. 3. **Say whether you may send them marketing**, for the whole file: * *Not yet* — kept as contacts, no campaigns until they subscribe or buy. * *They are our customers* — they bought from you or use your service; you confirm it. * *They subscribed* — they agreed explicitly, and you confirm you can show how and when each one did. 4. **Import.** Large files run in the background: you can leave the page, and contacts appear in the list as they are saved. You can stop an import; the rows saved before it stopped are kept. 5. **Read the result**: new contacts, updated, skipped, and the rows that were not imported, with the line and the problem, so you can fix them in the spreadsheet and import them again. Only an owner or an admin can import. ## What each status means and what to do [Section titled “What each status means and what to do”](#what-each-status-means-and-what-to-do) | Status or reason | What it means | What you do | | --------------------- | -------------------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------- | | `Unknown` | Azimea has no basis to send this person marketing: imported or added without a consent. | Only service emails reach them. To send more, collect consent (a form, a checkbox) and import it with its proof. | | `SoftOptIn` | An existing customer, in a shop whose checkout tells buyers it may write to them about similar products. | Campaigns and flows reach them once your consent method is declared as notice at checkout. Every email carries an unsubscribe link. | | `Subscribed` | They ticked a box or signed up to hear from you; the proof is kept in their consent history. | Everything reaches them, once you have declared how you collect consent (Settings › Consent). | | `CheckoutStarted` | They typed their address while placing an order and did nothing more: no purchase yet, no tick. | Only the reminder about that cart, and service emails. They become a subscriber if they tick the newsletter box. | | `PendingConfirmation` | They asked to subscribe and have not clicked the confirmation link yet (double opt-in). | Nothing to do: the confirmation email is on its way. Cart reminders still reach them; marketing waits for the click. | | `Unsubscribed` | They said they do not want your marketing emails. | Nothing: only service emails about their own orders or account still reach them. | | `Bounced` | Their address permanently refused emails: it no longer exists or no longer accepts them. | Nothing: Azimea stops writing to it. A new, working address can be added as a new contact. | | `Complained` | They marked one of your emails as spam. | Nothing: they never receive anything again, because each complaint hurts the reputation of every email you send. | ## Events involved [Section titled “Events involved”](#events-involved) | Event | When it happens | What it does to contacts | | ------------------------------- | --------------------------------------------- | --------------------------------------------------------------------------------- | | `contact.identified` | Someone types their email at checkout | Adds them as `CheckoutStarted` | | `subscriber.created` | A newsletter sign-up in your shop | Adds or raises them to `Subscribed` when they ticked a box | | `customer.upserted` | An account is created or changed in your shop | Adds or updates the contact | | `order.placed`, `order.updated` | Orders | Keep their orders and spending current; a paid order makes them a paying customer | | `subscriber.removed` | They leave the shop’s own newsletter | Moves them to `Unsubscribed` | | `contact.erasure_requested` | The shop forwards an erasure request | Erases them, as below | All of them are in the [events catalog](/catalogs/events/). ## Set it up in the dashboard [Section titled “Set it up in the dashboard”](#set-it-up-in-the-dashboard) * **See a contact:** Contacts, then a row. The page shows their details, tags, orders, timeline and **Consent basis**: the current status, where it came from and since when, with the full consent history. * **Find people:** the list filters by consent, tag and any field; a filter you want to keep becomes a [segment](/use/segments/). * **Export:** **Export CSV** on the list exports the contacts that pass your filters (or the ticked ones); **Export data** on a contact gives everything Azimea keeps about that person, for a data access request. * **Erase on request** (right to erasure, GDPR art. 17): on the contact, **Erase data**. The name, address and phone disappear, here and from their orders; the orders stay as figures without a person. It cannot be undone, and the next import will not bring them back. Their consent history is reduced to a single row with nothing personal in it. ## Do it from your code [Section titled “Do it from your code”](#do-it-from-your-code) `POST /contacts` and `POST /contacts/batch` add and update contacts with your fields and their consent and proof; `GET /contacts` reads them. See [what you can build today](/build/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | -------------------------------------------- | ------------------------------------------------------------------------ | -------------------------------------------------------------------------------- | | A row was not imported | The line shows the problem: usually an invalid email address. | Fix the line in the spreadsheet and import again; the other rows are already in. | | An imported contact receives no campaigns | The file was imported as *Not yet*, or the contact is `CheckoutStarted`. | Import again with the right choice only if you hold their consent. | | An import did not lower someone to *Not yet* | Imports only raise consent, never lower it. | Expected: a person’s “no” or a stronger basis is never overwritten by a file. | | An erased person came back | They signed up or ordered again afterwards. | That is a new, voluntary contact, not the erased data. | ## Related [Section titled “Related”](#related) * [Segments](/use/segments/) * [Consent and GDPR](/use/consent-and-gdpr/) * [Consent statuses](/catalogs/consent-statuses/) # How flows work > Triggers, steps, waits and conditions, testing, publishing and reading what happened to each person. ## What it does and when to use it [Section titled “What it does and when to use it”](#what-it-does-and-when-to-use-it) A flow sends the right email to one person at the right moment, on its own: when they leave a cart, subscribe, order, sign up in your product, or on a date you choose. You build it once from a trigger and a few steps; from then on every person who matches goes through it at their own pace. Use flows for anything that depends on what a person did. Use a [campaign](/use/campaigns/) for one email to many people at once. ## How it works [Section titled “How it works”](#how-it-works) Every flow starts with a **trigger** and ends with an **End workflow** step. In between, the steps run one after the other for each person. ### Triggers [Section titled “Triggers”](#triggers) | Trigger | Starts when | | --------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | | **Store events** | Something happens in your connected shop: **Order placed**, **Cart abandoned**, **New subscriber**, or **Customer created or updated** (someone subscribed while creating a shop account). | | **Something happens in your app** | Your product sends an event with that name to `POST /events` (for example `user.signed_up`). An optional filter keeps only some of them (`plan = pro`). | | **Schedule** | A recurring time (every day, every Monday at 9:00, or any cron expression) or a one-time date, in the time zone you choose. | | **Run now** | You press **Run now** on a published flow. | | **API / webhook** | Your own system calls the flow’s trigger URL with its token and the person’s email. The same call id twice starts it once. | A schedule or **Run now** reaches nobody on its own: add an **Audience & filters** step right after the trigger to say who enters (a segment, an uploaded list of up to 2,000 rows, or your contacts filtered by rules). The editor will not publish such a flow without it, so it can never email your whole list by accident. ### Steps [Section titled “Steps”](#steps) | Step | What it does | | ---------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | **Audience & filters** | Decides who enters; only right after the trigger. Whoever does not match ends there. | | **Wait** | Pauses one person’s path: for a while (60 minutes, 2 days), until a day and time (“Monday at 9:00”), or until they do something (`kyc.completed`) for at most a set time, with a **Yes** and a **No** path. | | **Condition** | Splits the path by rules on the contact (fields, tags, your custom attributes), on the trigger’s event, or on whether they opened or clicked an email from this flow. | | **Email** | Sends one of your email templates. The template is copied into the flow when you publish, so editing it later does not change what a live flow sends until you publish again. | | **Add tag** | Tags the contact, for later rules and segments. | | **Webhook** | Posts the contact (email, name, tags) to an HTTPS address you control, signed with the flow’s own key. | | **End workflow** | The path stops here. | ### Stop when… [Section titled “Stop when…”](#stop-when) On the trigger you can list up to 10 events that end a person’s run wherever it is (“stop the trial reminders when they subscribe”). Cart reminders also stop by themselves when the cart is ordered or emptied. ### Testing versions of an email [Section titled “Testing versions of an email”](#testing-versions-of-an-email) An **Email** step can test up to three versions (A, B, C): another template each, optionally under another sender name. Each person gets one at random. You choose what decides the best one (**Clicks**, **Orders** or **Revenue**) and whether a clearly better version is kept for everyone on its own. ### Draft, publish, history [Section titled “Draft, publish, history”](#draft-publish-history) You edit a **draft**; people go through the **live** version until you press **Publish**. **History** lists every published version: open one read-only, restore it as the draft, or discard your draft and go back to the live one. ### Test run [Section titled “Test run”](#test-run) **Test run** sends the draft through for one test contact, made up or a copy of a real one, and you follow every step in **Runs**. Nothing real happens: no email reaches the contact, no tag is added, no webhook is called, and waits pass instantly. You can choose what the test assumes (they opened, clicked, are in the segment, did the awaited event) and have each email also sent to your own inbox, marked **\[Test]**. ## What each status means and what to do [Section titled “What each status means and what to do”](#what-each-status-means-and-what-to-do) **Runs** shows every person a flow has taken in, and what happened to them. ### Run statuses [Section titled “Run statuses”](#run-statuses) | Status or reason | What it means | What you do | | ---------------- | ----------------------------------------------------------------------------- | ------------------------------------------------- | | `Running` | The run is executing its current step. | Nothing. | | `Waiting` | The run is at a wait step, or pausing before retrying a step that failed. | Nothing: it continues at the time shown. | | `Succeeded` | The run reached its end, or ended early for a reason recorded in its history. | Nothing. | | `Failed` | A step failed even after every retry. | Open the run's history to see which step and why. | | `Terminated` | Stopped from outside: the flow was unpublished or the contact was erased. | Nothing. | ### Steps in a run’s history [Section titled “Steps in a run’s history”](#steps-in-a-runs-history) | Step | What it means | | --------------- | ---------------------------------------------------------------------------------------------------- | | `Enrolled` | The contact entered the flow: its trigger happened for them. | | `Entered` | The run arrived at a step. | | `Evaluated` | A condition, an audience check or a wait-for-event was decided; the history shows which way it went. | | `Sent` | The email step queued its email for sending. | | `Tagged` | A tag was added to the contact. | | `WebhookCalled` | The flow called your webhook address. | | `Waited` | A wait step was reached; the run continues at the time it shows. | | `Retried` | A step failed for a passing reason and will be tried again. | | `Failed` | A step failed after every retry; the run stops there. | | `Completed` | The run ended, at the end of the flow or early for the reason it shows. | | `Skipped` | The email step did not send: the sending gate stopped it, and the history shows the reason. | | `Simulated` | In a test run: what the step would have done, without doing it. | ### Why a run ended early [Section titled “Why a run ended early”](#why-a-run-ended-early) | Status or reason | What it means | What you do | | ---------------------- | ------------------------------------------------------------------------------------------------------ | -------------------------------------------------- | | `audience_not_matched` | The contact did not fit the audience the flow is limited to, so the run ended right after the trigger. | Nothing, or widen the flow's audience. | | `cart_closed` | The cart the flow was about was ordered or emptied, so there was nothing left to remind anyone of. | Nothing: this is the reminder working as intended. | | `exit_event` | The contact did one of the things the flow stops on (for example they activated or subscribed). | Nothing. | An email step that did not send shows the [sending gate’s reason](/catalogs/why-an-email-was-not-sent/). ## Events involved [Section titled “Events involved”](#events-involved) Flows on **Store events** start on the facts Azimea records: | Event | What it means | When it happens | Who sends it | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | ------------ | | `order.placed` | Azimea recorded a new order on the contact's timeline. | When the order arrives, from any source. | Azimea | | `cart.abandoned` | Azimea's verdict that a cart was left: it starts the abandoned-cart flow. | 30 minutes without change, with products and an address, and no order. | Azimea | | `contact.subscribed` | A contact became a subscriber: a newsletter or form sign-up starts welcome flows; someone who ticked marketing while creating a shop account starts the flows on new customer accounts. | When a contact's consent becomes Subscribed. | Azimea | Flows on **Something happens in your app** start on your product’s events; these names make the ready-made product flows work without changes: | Event | What it means | When it happens | Who sends it | | ---------------------- | -------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------ | ---------------------------------- | | `user.signed_up` | A person created an account in your product. | Right after sign-up. | Your product, through POST /events | | `user.activated` | A person reached the moment your product counts as "they got it" (their first project, first payment, first report). | When you decide activation happened. | Your product, through POST /events | | `trial.ending` | A person's trial ends soon. | A few days before the end of the trial, as you choose. | Your product, through POST /events | | `subscription.started` | A person started paying. | On the first successful payment. | Your product, through POST /events | ## Set it up in the dashboard [Section titled “Set it up in the dashboard”](#set-it-up-in-the-dashboard) 1. **Flows › New workflow**: start from a template (abandoned cart, welcome series, post-purchase follow-up, win back) or from an empty flow. 2. Pick the trigger, then add steps from the library on the left; connect them on the canvas. 3. Choose **What it is for**: **Marketing**, or **Service message** for an email that only informs (see [After the order](/use/after-the-order/)). 4. Run a **Test run** and read it in **Runs**. 5. Press **Publish**. Problems that would stop it are shown on the canvas first. ## Do it from your code [Section titled “Do it from your code”](#do-it-from-your-code) Send events to `POST /events`, or call a flow’s trigger URL. See [What you can build today](/build/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | ------------------------------ | -------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------- | | **Publish** is refused | A step is incomplete (no template, no tag, no HTTPS address, a schedule without an Audience step). | Fix the problems marked on the canvas. | | Nobody enters | The flow is a draft, or the trigger never happens (no store connected, your app does not send that event yet). | Publish it; check **Runs** and the contact’s timeline. | | People enter but get no email | The sending gate stopped it; the run shows the reason. | See [why an email was not sent](/catalogs/why-an-email-was-not-sent/). | | An edited template is not used | Live flows keep the copy taken at publish. | Publish the flow again. | | A run is `Failed` | A step failed after every retry (often a webhook address that does not answer). | Open the run’s history for the step and the reason. | ## Related [Section titled “Related”](#related) * [Recover abandoned carts](/use/recover-abandoned-carts/) * [Welcome new subscribers](/use/welcome-and-onboarding/) * [Flow runs catalog](/catalogs/flow-runs/) # Recover abandoned carts > Remind people who left products in their cart, and stop the moment they order. ## What it does and when to use it [Section titled “What it does and when to use it”](#what-it-does-and-when-to-use-it) Most people who put products in a cart leave without paying. A short reminder, sent while they still remember what they wanted, brings a part of them back. Azimea notices when a cart has been left, sends the reminder with the products in it and a button that puts the cart back, and stops as soon as the person orders, so nobody is reminded of something they already bought. Use it in every shop: it is the flow that usually earns the most per email. ## How it works [Section titled “How it works”](#how-it-works) 1. **The cart is followed from the first product.** Your shop’s Azimea plugin (or the platform’s own data, on Shopify and MerchantPro) reports every change to the cart, even before the visitor is known. 2. **The person becomes known** when they type their email at checkout. From now on the cart has an address. 3. **The cart is declared abandoned** when it has not changed for **30 minutes**, has products, has an address and has not become an order. 4. **The flow starts.** The ready-made “Abandoned cart” flow waits **60 minutes** and sends the reminder. You can add a second reminder a day later. 5. **The order closes the cart.** When the person orders, the cart becomes *Recovered* and the flow stops before its next email. The order is matched by the cart itself, or by the same email address within 7 days on platforms that do not say which cart an order came from. The **“See my cart”** button rebuilds the same cart in any browser or device and lands on the checkout. The link is signed, expires after 30 days, and carries products only: never an address or a login. ## What each status means and what to do [Section titled “What each status means and what to do”](#what-each-status-means-and-what-to-do) ### Cart statuses [Section titled “Cart statuses”](#cart-statuses) | Status or reason | What it means | What you do | | ---------------- | ---------------------------------------------------------------------------------------------------------------- | ----------------------------------------------- | | `Open` | Someone is filling the cart, or left it less than 30 minutes ago. | Nothing. | | `Abandoned` | Left with products and an address for 30 minutes without becoming an order. The abandoned-cart flow starts here. | Make sure the Abandoned cart flow is published. | | `Recovered` | It became an order. It counts in your recovered revenue and stops the reminders. | Nothing. | | `Cleared` | The person emptied it. | Nothing: nobody is reminded of an empty cart. | ### When a reminder is not sent [Section titled “When a reminder is not sent”](#when-a-reminder-is-not-sent) The email is stopped by the [sending gate](/glossary/#sending-gate), and the flow’s history says why. For a cart reminder, only these reasons apply (all of them are in [why an email was not sent](/catalogs/why-an-email-was-not-sent/)): | Status or reason | What it means | What you do | | --------------------------- | --------------------------------------------------- | ------------------------------------------------------- | | `sending.gate_bounced` | The person's address does not work any more. | Nothing: the address is no longer used. | | `sending.gate_complained` | The person marked one of your emails as spam. | Nothing: they are never written to again. | | `sending.gate_unsubscribed` | The person unsubscribed from your marketing emails. | Nothing: they said no. Service emails still reach them. | When the cart is ordered before the reminder is due, the run ends with `cart_closed` (see [flow runs](/catalogs/flow-runs/)): not a refusal, the flow working as intended. A cart reminder does not need a newsletter subscription: someone who typed their address while ordering may be reminded of that order. It is the only marketing-like email they can receive until they subscribe. ## Events involved [Section titled “Events involved”](#events-involved) | Event | When it happens | Who sends it | What it does here | | -------------------- | -------------------------------------------------------- | ------------------------------------------------------------------------------------ | ---------------------------------------------------------------- | | `cart.updated` | A product is added, removed or changed. | Your shop’s plugin, the storefront script, or Azimea reading Shopify and MerchantPro | Keeps the cart and its products current. | | `contact.identified` | The visitor types their email at checkout. | Your shop’s plugin or storefront script | Gives the cart an address; adds the person as `CheckoutStarted`. | | `cart.cleared` | The cart is emptied. | Your shop’s plugin or storefront script | Marks the cart `Cleared`. | | `cart.abandoned` | 30 minutes without change, with products and an address. | Azimea itself | Starts the “Abandoned cart” flow. | | `order.placed` | An order is placed. | Your shop | Marks the cart `Recovered` and stops the flow. | ## Set it up in the dashboard [Section titled “Set it up in the dashboard”](#set-it-up-in-the-dashboard) 1. Connect your shop in **Integrations** and install the Azimea plugin or app (the page of each platform has the steps). 2. In **Flows**, open **Abandoned cart** and press **Publish**. Until it is published, carts are followed but nobody is reminded. 3. Optional: edit the email (subject, words, colours). The products and the “See my cart” button are filled in for each person. 4. Optional: add a second reminder after the first one, with a wait of a day. ## Do it from your code [Section titled “Do it from your code”](#do-it-from-your-code) Shops built on another platform can send the same events through the API. The event contract, the restore link and the signatures are described for developers in [Build with Azimea](/build/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | ------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------ | ----------------------------------------------- | | Carts appear but stay `Open` forever | They have no email address: the visitor never reached the email field. | Expected: an anonymous cart cannot be reminded. | | Carts become `Abandoned` but nobody gets an email | The flow is not published. | Flows › Abandoned cart › Publish. | | The flow history says “Email not sent” | The sending gate stopped it; the reason is shown next to it. | See the table above. | | A customer was reminded after ordering | The order was placed with a different email than the one typed in the cart, on a platform that does not report the cart. | Nothing to fix on your side; it is rare. | ## Related [Section titled “Related”](#related) * [Glossary: consent statuses](/glossary/#consent-status) * [Start here](/start-here/) # Segments > Groups of contacts defined by conditions, kept up to date on their own, and where you use them. ## What it does and when to use it [Section titled “What it does and when to use it”](#what-it-does-and-when-to-use-it) A **segment** is a group of contacts described by conditions (“bought twice or more, nothing in the last 60 days”), not a fixed list. Whoever matches the conditions is in it, today and from now on: it keeps itself up to date. Use segments to choose who receives a campaign, who may enter a flow, and who must be left out. ## How it works [Section titled “How it works”](#how-it-works) A segment is a set of **conditions**, joined by *all conditions* (everyone who matches every one) or *any condition* (everyone who matches at least one), up to 20 conditions. Each condition is a **field**, a **condition** on it and a **value**. Fields are what you know about a contact: | Group | Fields | | ----------------- | ---------------------------------------------------------------------- | | Details | Email, First name, Last name, Phone, City, Country, Language, Added on | | Consent | Has email consent, the topics they receive | | Tags | Tags | | Orders | Has bought, Orders, Spent, Last order, Last conversion | | Engagement | Last email opened, Last link clicked, Last email received | | Campaign activity | Received, opened, clicked or bought from a given campaign | | Your fields | Every field in Settings › Contact fields | | Events | Did an event, or when they last did it | Conditions depend on the field: equals, is not, contains, does not contain, greater or less than, before or after a date, and within or not within the last N days. Instead of building the conditions by hand you can **describe who you want** (“bought twice or more, but nothing in the last 60 days”) and let the AI write them; anything it cannot express with your fields is left out and you are told. ## What each status means and what to do [Section titled “What each status means and what to do”](#what-each-status-means-and-what-to-do) A segment has no status of its own. What decides whether its members actually **receive** an email is each one’s consent: a segment can include people who may not be written to, and the [sending gate](/glossary/#sending-gate) leaves them out at send time, with the reason recorded. See [why an email was not sent](/catalogs/why-an-email-was-not-sent/). ## Events involved [Section titled “Events involved”](#events-involved) Segments react to the same facts as everything else: a new order changes *Orders* and *Spent*, an open changes *Last email opened*, an event your product sends can be a condition. The list of events is in the [events catalog](/catalogs/events/). ## Set it up in the dashboard [Section titled “Set it up in the dashboard”](#set-it-up-in-the-dashboard) 1. **Contacts**, then build the filter: add a condition, choose the field, the condition and the value. The count of matching contacts updates as you go. 2. **Save as segment**, give it a name and a description of who is in it and what it is for. 3. Use it: * **In a campaign**: choose it as the audience; optionally leave out another segment, and people who received an email from you in the last N days. * **In a flow**: an Audience step lets only the segment’s members continue. * **From a campaign’s results**: save the people who opened, clicked or bought as a new segment. Editing a segment’s conditions changes who is in it from then on. Deleting it removes only the saved filter; the contacts stay. **When a campaign starts, its recipients are fixed.** The people in the segment at that moment are the ones who get the email, even if the segment changes while it is being sent; the campaign keeps the segment’s name as it was. ## Do it from your code [Section titled “Do it from your code”](#do-it-from-your-code) Your product decides who is in a segment through the data it sends: fields on `POST /contacts`, events on `POST /events`. See [what you can build today](/build/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------ | | The count is zero while you build it | A condition is incomplete, or the conditions are joined by *all* when you meant *any*. | Fill in every condition; check the join. | | A campaign reached fewer people than the segment holds | Some members may not receive marketing (no consent, unsubscribed, a topic turned off) or were excluded. | Check the campaign’s exclusions and [why an email was not sent](/catalogs/why-an-email-was-not-sent/). | | Someone left the segment | They no longer match: segments update on their own. | Expected. | ## Related [Section titled “Related”](#related) * [Contacts and imports](/use/contacts-and-imports/) * [Consent and GDPR](/use/consent-and-gdpr/) # Sending and deliverability > Your sending domain, your senders, the daily cap, bounces and complaints, and how to tell why an email did not arrive. ## What it does and when to use it [Section titled “What it does and when to use it”](#what-it-does-and-when-to-use-it) Whether an email lands in the inbox or in spam depends mostly on who sends it and how. Azimea sends your emails from **your own domain**, signed so mail servers can trust it, keeps sending within a daily cap that grows as your figures stay healthy, and stops writing to addresses that bounce or complain so they do not drag the rest of your list down. Use this page when you set up sending, when emails go to spam, or when an email did not arrive. ## How it works [Section titled “How it works”](#how-it-works) ### Your sending domain [Section titled “Your sending domain”](#your-sending-domain) Your emails go out from an address on a domain you own (`shop@yourshop.com`). To prove it is yours, you add a few DNS records with your DNS provider: | Record | What it does | Needed | | --------- | --------------------------------------------------- | ----------- | | **SPF** | Says who is allowed to send on your behalf | Yes | | **DKIM** | The signature that proves the email is really yours | Yes | | **DMARC** | Tells mail servers what to do with forged emails | Recommended | When SPF and DKIM are found, the domain is **Verified**. Azimea checks the records again regularly; if a record disappears, you see it on the domain. | Status or reason | What it means | What you do | | ---------------- | ------------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------- | | `Pending` | Added, but not all its DNS records are published yet. The dashboard shows it as "Being verified". | Publish the records shown at your DNS provider, then press Check again. | | `Verified` | All its records are in place: you can send from it. | Nothing; keep the records published. | | `Failed` | Verification failed for good. The dashboard shows it as "Stopped". | Check the records against the ones shown, then remove and add the domain again, or contact support. | ### Your senders [Section titled “Your senders”](#your-senders) A **sender** is a from address and name (“Maria’s Shop” ``), optionally with a reply-to address. Every sender must be on one of your verified domains. You keep a list of them, mark one as the default, and choose one on each campaign and on each flow’s email step. How many you can keep depends on your plan. ### The daily cap [Section titled “The daily cap”](#the-daily-cap) Each company has a number of emails it may send per day. The cap rises on its own as long as your figures look good (few bounces, few spam complaints); it reflects how you send, not what you pay. Emails over today’s cap are not lost: they wait and go out the next day. If the figures turn bad, the cap stops rising, and in serious cases sending stops until your list is cleaned up; prepared emails wait in the queue and go out once it restarts. ### Addresses that no longer receive [Section titled “Addresses that no longer receive”](#addresses-that-no-longer-receive) Azimea stops writing to an address on its own when it **bounces** permanently, when the person **marks an email as spam**, or when they **unsubscribe**. These addresses are listed in Settings › Sending status. Only a bounced address can be put back, and only once you are sure the problem is fixed; an unsubscribe or a complaint is the person’s decision and cannot be undone by the store. ### The sending gate [Section titled “The sending gate”](#the-sending-gate) Before an email is queued, Azimea checks the person’s consent against the kind of message. If it does not allow it, the email is stopped and the reason kept. See [Consent and GDPR](/use/consent-and-gdpr/). ### Testing without reaching anyone [Section titled “Testing without reaching anyone”](#testing-without-reaching-anyone) A **sandbox** is a copy of your company where flows, campaigns and the API work as usual but emails stay inside it and never reach your contacts. To see an email as a customer would, add **test addresses** to the sandbox (your own Gmail or Outlook): each one receives a confirmation link and counts only once it is confirmed. ## What each status means and what to do [Section titled “What each status means and what to do”](#what-each-status-means-and-what-to-do) ### Email statuses [Section titled “Email statuses”](#email-statuses) | Status or reason | What it means | What you do | | ---------------- | -------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------------- | | `Queued` | Accepted by Azimea and waiting its turn to go out. | Nothing. | | `Sent` | The sending server took it from Azimea. | Nothing: Delivered or Bounced follows. | | `Failed` | It could not be sent and Azimea is not retrying; the reason is written on the message. | Read the reason on the message; contact support if it repeats. | | `Suppressed` | Stopped before sending by the sending gate; the reason is kept on the message. | See "Why an email was not sent" for the reason and what to do. | | `Delivered` | The recipient's mail server received it. | Nothing. | | `Bounced` | The recipient's mail server refused it after it was sent. | A permanent refusal stops future emails to that address; see the contact's status. | | `HandingOff` | Being handed to the sending server right now. | Nothing: it moves on within seconds. | | `Accepted` | Azimea's sending server confirmed it holds the email in its queue. | Nothing: Delivered or Bounced follows. | | `Unknown` | It left Azimea, but no confirmation came back in the expected time. | Nothing: a late confirmation still moves it to its real status. | ### Why an email was stopped [Section titled “Why an email was stopped”](#why-an-email-was-stopped) | Status or reason | What it means | What you do | | ------------------------------ | ---------------------------------------------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------ | | `sending.gate_bounced` | The person's address does not work any more. | Nothing: the address is no longer used. | | `sending.gate_complained` | The person marked one of your emails as spam. | Nothing: they are never written to again. | | `sending.gate_no_basis` | The person has not subscribed and is not an existing customer, so marketing may not reach them. | Expected for people who only typed their address at checkout. To reach them, ask for consent; or make a message about their own order a service email. | | `sending.gate_not_declared` | You have not yet declared how your shop collects consent, so no marketing goes out, even to subscribers. | Settings › Consent: choose your method and declare it. | | `sending.gate_soft_opt_in_off` | The person is an existing customer, but your shop uses a consent checkbox, not a notice at checkout, so buying alone does not allow marketing. | Expected with a checkbox: only those who ticked it receive marketing. Switch to notice at checkout only if your checkout really shows that notice. | | `sending.gate_topic_opted_out` | The person turned off the topic this email belongs to; they still receive your other topics. | Nothing: respect their choice. | | `sending.gate_unsubscribed` | The person unsubscribed from your marketing emails. | Nothing: they said no. Service emails still reach them. | ## Events involved [Section titled “Events involved”](#events-involved) Sending produces its own events about each email (delivered, opened, clicked, bounced, complained, unsubscribed). They show on the contact’s timeline, feed reports and segments (*Last email opened*, *Last link clicked*), and can be sent to your systems through webhooks. Opens and clicks made by machines (mail privacy features, link scanners) are told apart from people’s and never counted as a person’s. ## Set it up in the dashboard [Section titled “Set it up in the dashboard”](#set-it-up-in-the-dashboard) 1. **Settings › Sending domains › Add domain**: type your domain (usually your shop’s). 2. Add each record shown (type, name, value) with your DNS provider, then **Check again** until the domain is Verified. 3. **Settings › Sending status**: add your senders on that domain and mark the default. 4. On each campaign and flow email step, choose the sender. 5. Watch **Settings › Sending status**: today’s cap, what was sent, how the last days look (bounces, spam complaints) and the addresses that no longer receive. ## Do it from your code [Section titled “Do it from your code”](#do-it-from-your-code) `POST /sending/emails` sends a transactional email from your product; webhooks tell your systems what happened to each email. See [what you can build today](/build/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | --------------------------------- | ------------------------------------------------------------------------------------------------ | ---------------------------------------------------------------------- | | The domain stays “Being verified” | A required record is missing or has a different value; the domain page shows what is in DNS now. | Correct the record with your DNS provider and **Check again**. | | A sender cannot be saved | Its address is not on a verified domain of yours, or your plan’s sender limit is reached. | Verify the domain first; or remove a sender you no longer use. | | An email shows `Suppressed` | The sending gate stopped it. | See [why an email was not sent](/catalogs/why-an-email-was-not-sent/). | | Emails arrive late | Today’s cap was reached; the rest goes out the next day. | Expected; the cap rises as your figures stay healthy. | | Emails land in spam | Missing DMARC, a sudden large send to an old list, or many complaints. | Add DMARC, send to people who engaged recently, keep your list clean. | ## Related [Section titled “Related”](#related) * [Email statuses](/catalogs/email-statuses/) * [Why an email was not sent](/catalogs/why-an-email-was-not-sent/) * [Consent and GDPR](/use/consent-and-gdpr/) # Welcome new subscribers > Greet everyone who subscribes, at the moment they are most interested, and only once they have confirmed. ## What it does and when to use it [Section titled “What it does and when to use it”](#what-it-does-and-when-to-use-it) Someone who has just subscribed is more interested in your shop than they will ever be again. A welcome email sent at that moment thanks them, tells them what they will receive, and invites them back. Azimea sends it on its own, the moment a person becomes a subscriber, and never to someone who has not confirmed yet. Use it in every shop that collects newsletter sign-ups. Products with user accounts use the same idea from their own sign-up event (see [How flows work](/use/flows/)). ## How it works [Section titled “How it works”](#how-it-works) 1. **Someone subscribes**: a newsletter form, a popup, the newsletter box at checkout, or the shop’s own newsletter (MerchantPro, Shopware). Their consent and its proof are kept. 2. **If you use double opt-in**, they first receive a confirmation email and stay `PendingConfirmation`. The welcome waits: it starts only when they click the confirmation link. Someone who never confirms is never welcomed. 3. **They become `Subscribed`**, and Azimea records [`contact.subscribed`](/catalogs/events/) on their timeline. 4. **The welcome flow starts.** A flow whose trigger is **Store events › New subscriber** begins for them; the ready-made **Welcome** flow sends its email right away. 5. **A person who ticked “subscribe” while creating an account in your shop** starts the flows whose trigger is **Customer created or updated** instead, so you can greet new account holders differently. An account created without the tick starts nothing: it gives no right to marketing. The ready-made flow sends one email. The **Welcome series** template in **Flows › New workflow** sends three: on the day, two days later and three days after that. ## What each status means and what to do [Section titled “What each status means and what to do”](#what-each-status-means-and-what-to-do) Only these consent statuses lead to a welcome; the full list is in [consent statuses](/catalogs/consent-statuses/): | Status or reason | What it means | What you do | | --------------------- | -------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------- | | `Subscribed` | They ticked a box or signed up to hear from you; the proof is kept in their consent history. | Everything reaches them, once you have declared how you collect consent (Settings › Consent). | | `PendingConfirmation` | They asked to subscribe and have not clicked the confirmation link yet (double opt-in). | Nothing to do: the confirmation email is on its way. Cart reminders still reach them; marketing waits for the click. | A welcome is a marketing email, so the [sending gate](/glossary/#sending-gate) applies. It can be stopped for: | Status or reason | What it means | What you do | | ------------------------------ | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------- | | `sending.gate_bounced` | The person's address does not work any more. | Nothing: the address is no longer used. | | `sending.gate_complained` | The person marked one of your emails as spam. | Nothing: they are never written to again. | | `sending.gate_not_declared` | You have not yet declared how your shop collects consent, so no marketing goes out, even to subscribers. | Settings › Consent: choose your method and declare it. | | `sending.gate_topic_opted_out` | The person turned off the topic this email belongs to; they still receive your other topics. | Nothing: respect their choice. | | `sending.gate_unsubscribed` | The person unsubscribed from your marketing emails. | Nothing: they said no. Service emails still reach them. | ## Events involved [Section titled “Events involved”](#events-involved) | Event | What it means | When it happens | Who sends it | | -------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -------------------------------------------- | ------------ | | `contact.subscribed` | A contact became a subscriber: a newsletter or form sign-up starts welcome flows; someone who ticked marketing while creating a shop account starts the flows on new customer accounts. | When a contact's consent becomes Subscribed. | Azimea | The shop sends the sign-up itself as `subscriber.created` (or `customer.upserted` for an account); both are explained in the [events catalog](/catalogs/events/). ## Set it up in the dashboard [Section titled “Set it up in the dashboard”](#set-it-up-in-the-dashboard) 1. **Settings › Consent**: choose how your shop collects consent and declare it. Until you do, no marketing email goes out, the welcome included. 2. Optional, in the same place: turn on double opt-in. 3. **Flows › Welcome**: check the email and press **Publish**. The flow was created as a draft when you set up your first emails; until it is published, nobody is welcomed. 4. Optional: use the **Welcome series** template for more than one email. ## Do it from your code [Section titled “Do it from your code”](#do-it-from-your-code) A product without a shop starts welcome flows from its own sign-up: send `user.signed_up` to `POST /events` and build the flow on **Something happens in your app**. See [What you can build today](/build/). ## Troubleshooting [Section titled “Troubleshooting”](#troubleshooting) | Symptom | Likely cause | Fix | | -------------------------------------- | -------------------------------------------------------------- | ---------------------------------------------------------------------- | | Subscribers appear but get no welcome | The Welcome flow is not published, or consent is not declared. | Publish the flow; declare consent in Settings › Consent. | | Some subscribers get it, others not | Double opt-in is on and they have not confirmed. | Nothing: they are welcomed when they click the confirmation link. | | The flow history says “Email not sent” | The sending gate stopped it; the reason is shown next to it. | See [why an email was not sent](/catalogs/why-an-email-was-not-sent/). | | A new shop account got no welcome | The account was created without the newsletter tick. | Expected: an account alone gives no right to marketing. | ## Related [Section titled “Related”](#related) * [How flows work](/use/flows/) * [Consent statuses](/catalogs/consent-statuses/) * [Recover abandoned carts](/use/recover-abandoned-carts/)