> ## Documentation Index
> Fetch the complete documentation index at: https://docs.minimo.it/llms.txt
> Use this file to discover all available pages before exploring further.

# Automatic messages: rules, events and sendAt

> Send events, Minimo sends the emails at the right time: confirmation now, reminder 2 hours before, review request after, cancelled when the booking is

## Overview

If you run a SaaS on top of Minimo (one workspace, your customers as **tenants**, see [How embedded Minimo works](https://docs.minimo.it/guides/embedded-overview.md)), your backend does not need a scheduler. It sends Minimo an **event** when something happens (`booking.created`, `booking.updated`, `booking.cancelled`); Minimo keeps the **rules** and the **clock**:

* a rule says which email goes out for which event, **immediately** or at an **offset** from a date in the event ("2 hours before `booking_at`");
* a `booking.cancelled` with the same `cancellationKey` cancels what is still pending, `booking.updated` moves it;
* each tenant can switch a rule off from the [automatic messages widget](https://docs.minimo.it/guides/embedded-widget-automatic-messages.md) (or you do it by API);
* the email is the tenant's own version of the template when they personalized it.

No cron, no queue, no deduplication on your side.

## Before you start

* An API key with the **Events** permission (`events`) to send events, and **Widgets** (`widgets`) to manage the rules. Keep it on your server.
* The email templates (masters) your tenants use, e.g. confirmation, reminder, review request.

## 1. Define the rules once (for every tenant)

```bash theme={null}
curl -X PUT https://api.minimo.it/public/v1/automation-rules \
  -H "Authorization: Bearer YOUR_MINIMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "rules": [
      { "key": "confirmation", "name": "Booking confirmation", "event": "booking.created", "template": 891 },
      { "key": "reminder", "name": "Reminder 2 hours before", "event": "booking.created", "template": 892,
        "offset": "-PT2H", "dateField": "booking_at" },
      { "key": "review", "name": "Review request", "event": "booking.created", "template": 889,
        "offset": "PT2H", "dateField": "booking_at" }
    ]
  }'
```

| Field | Notes |
| - | - |
| `key` | Stable name of the rule. A tenant override uses the same key. |
| `event` | The event that schedules the send (compared ignoring case). |
| `template` | Template id or uid. Use the **master**: a tenant that personalized it gets its own copy at send time. |
| `offset` | Signed ISO 8601 duration from `dateField`: `-PT2H` 2 hours before, `PT2H` 2 hours after, `-P1D` a day before. Omitted: immediately. |
| `dateField` | Path of the date in the event payload (`booking_at`, `booking.date`). |
| `enabled` | Default `true`. |
| `cancelOn` | Events that cancel the pending sends with the same `cancellationKey`. Default: `<prefix>.cancelled` and `<prefix>.canceled` (`booking.created` → `booking.cancelled`). |
| `rescheduleOn` | Events that move a pending timed send to the new date. Default: `<prefix>.updated`. |

`PUT` adds or updates the rules you send (by key) and returns every rule with a readable `timing` ("Immediately", "2 hours before"); `GET /public/v1/automation-rules` lists them, `DELETE /public/v1/automation-rules/{key}` removes one (`204`). A rule with an `offset` and no `dateField` is refused (`422 INVALID_AUTOMATION_RULE`).

## 2. Send events (your backend)

```bash theme={null}
curl -X POST https://api.minimo.it/public/v1/events \
  -H "Authorization: Bearer YOUR_MINIMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "event": "booking.created",
    "tenant": "restaurant_4812",
    "email": "anna@example.com",
    "firstName": "Anna",
    "idempotencyKey": "booking-9921-created",
    "cancellationKey": "booking-9921",
    "from": "Trattoria da Mario <prenotazioni@trattoriadamario.yoursaas.example>",
    "replyTo": "owner@trattoriadamario.example",
    "marketingConsent": true,
    "payload": {
      "booking_at": "2026-11-20T20:30:00+01:00",
      "GUEST_NAME": "Anna",
      "BOOKING_DATE": "venerdì 20 novembre",
      "BOOKING_TIME": "20:30",
      "PARTY_SIZE": 4
    }
  }'
```

When the booking changes or is cancelled, send the same `cancellationKey`:

```json theme={null}
{ "event": "booking.updated", "tenant": "restaurant_4812", "email": "anna@example.com",
  "idempotencyKey": "booking-9921-updated-2", "cancellationKey": "booking-9921",
  "payload": { "booking_at": "2026-11-21T21:00:00+01:00", "BOOKING_TIME": "21:00", "PARTY_SIZE": 6 } }
```

```json theme={null}
{ "event": "booking.cancelled", "tenant": "restaurant_4812", "email": "anna@example.com",
  "idempotencyKey": "booking-9921-cancelled", "cancellationKey": "booking-9921" }
```

| Field | Notes |
| - | - |
| `tenant` | Your customer. Its own rules apply on top of the defaults; the guest is linked to it. |
| `idempotencyKey` | Your unique id of this event. Sent again (a retry), it is recognised: nothing is sent or scheduled twice, the response says `"status": "duplicate"`. |
| `cancellationKey` | Groups the sends of one object, e.g. the booking id. Even without an `idempotencyKey`, one rule never schedules twice for the same key. |
| `payload` | Any JSON. Every value is a merge variable of the email, by its path in capitals (`GUEST_NAME`, `booking.time` → `BOOKING_TIME`), over the [tenant profile](https://docs.minimo.it/guides/embedded-widget-brand.md#tenant-profile) (`RESTAURANT_NAME`, `LOGO_URL`, `RESTAURANT_ADDRESS`). |
| dates | **Always with the UTC offset** (`2026-11-20T20:30:00+01:00` or `…Z`). A date-time without offset is refused (`422 INVALID_EVENT_DATE`): Minimo cannot guess the restaurant's timezone. Send display values (`BOOKING_TIME`) next to the machine one. |
| `from`, `replyTo` | Sender and Reply-To of the emails of this event, checked now and again at send time (same rules as the [per-send sender](https://docs.minimo.it/guides/embedded-senders-and-sending.md)). |
| `marketingConsent` | Recorded only when the event creates the guest. An existing contact is never changed by an event. |

The response (`202`) tells you what was planned:

```json theme={null}
{
  "data": {
    "event": "booking.created", "status": "no_automation", "automations": 0, "contactId": 4521,
    "tenantAutomations": {
      "scheduled": [
        { "id": 41, "rule": "confirmation", "status": "pending", "reason": null, "sendAt": "2026-11-08T18:00:01.000Z" },
        { "id": 42, "rule": "reminder", "status": "pending", "reason": null, "sendAt": "2026-11-20T17:30:00.000Z" },
        { "id": 43, "rule": "review", "status": "pending", "reason": null, "sendAt": "2026-11-20T21:30:00.000Z" }
      ],
      "cancelled": 0, "rescheduled": 0
    }
  }
}
```

`status` / `automations` are about workspace automations with an "Event received" trigger, which keep working as before on the same events.

## What happens at send time

Nothing decided when the event arrived is trusted when the email is due. Minimo checks again that the rule still exists and is on for the tenant, that the guest still exists and did not hard-bounce, that the template is there, that the sender is still allowed and, for a marketing template such as the review request, that the guest has marketing consent and the workspace is not on a marketing hold. The unsubscribe footer and `List-Unsubscribe` header apply as for every send.

A send that does not go out is never an error to you: it is recorded with a reason.

| Status | Reason |
| - | - |
| `pending` | Waiting for its time. |
| `sent` | Delivered to the email provider (see the [send history](https://docs.minimo.it/guides/embedded-widget-history.md)). |
| `skipped` | `disabled_by_tenant`, `disabled`, `past_due` (its time passed by more than 5 minutes, e.g. a booking made 1 hour ahead with a 2-hour reminder), `rule_deleted`, `contact_deleted`, `suppressed`, `template_missing`, `sender_not_allowed`; for marketing templates also `no_marketing_consent`, `no_tenant_subscription` ([consent per tenant](https://docs.minimo.it/guides/embedded-consent.md#consent-per-tenant)), `marketing_send_on_hold`, `tenant_on_hold`, `tenant_daily_cap_reached` ([protections](https://docs.minimo.it/guides/embedded-protections.md)). |
| `cancelled` | `cancelled_by_event:booking.cancelled`, `cancelled_by_api`, `rescheduled`. |
| `failed` | The provider refused it. |

```bash theme={null}
# What is planned or happened for one booking
curl "https://api.minimo.it/public/v1/scheduled-sends?cancellationKey=booking-9921" \
  -H "Authorization: Bearer YOUR_MINIMO_KEY"

# Cancel everything still pending for it (same as the cancelling event)
curl -X DELETE "https://api.minimo.it/public/v1/scheduled-sends?cancellationKey=booking-9921&tenant=restaurant_4812" \
  -H "Authorization: Bearer YOUR_MINIMO_KEY"
```

## Per tenant: switch off or change a rule

The [automatic messages widget](https://docs.minimo.it/guides/embedded-widget-automatic-messages.md) shows each tenant an **Automatic sends** list with an on/off switch per rule. The choice is saved by Minimo for that tenant and your page receives `automation.toggled` with `{ tenant, rule, enabled }`. A send of a rule that is off is recorded as `skipped` / `disabled_by_tenant`.

By API (any field of a default can be overridden for one tenant, using the default's `key`; a key without a default is the tenant's own rule and needs `event`, `template` and the timing, with `"offset": null` for an immediate one, otherwise `422 INVALID_AUTOMATION_RULE`):

```bash theme={null}
curl -X PUT https://api.minimo.it/public/v1/tenants/restaurant_4812/automations \
  -H "Authorization: Bearer YOUR_MINIMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "rules": [ { "key": "reminder", "offset": "-P1D" }, { "key": "review", "enabled": false } ] }'
```

`GET /public/v1/tenants/{tenant}/automations` returns the tenant's effective rules (`source`: `default`, `override` or `tenant`); `DELETE /public/v1/tenants/{tenant}/automations/{key}` goes back to the default (`204`, `404` when the tenant has no override for that key).

## Or schedule one email yourself: `sendAt`

If your backend already knows when an email must go out, you can skip the rules and schedule a single send. Add `sendAt` to `POST /public/v1/transactionals` or `POST /public/v1/templates/email/send` (scope `transactional`):

```bash theme={null}
curl -X POST https://api.minimo.it/public/v1/transactionals \
  -H "Authorization: Bearer YOUR_MINIMO_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "transactional": "TRF4MS9",
    "recipient": "guest@example.com",
    "tenant": "restaurant_4812",
    "sendAt": "2026-11-19T20:30:00+01:00",
    "idempotencyKey": "booking-4812-reminder",
    "cancellationKey": "booking-4812",
    "customFields": { "GUEST_NAME": "Anna", "BOOKING_TIME": "20:30" }
  }'
```

The answer is HTTP `201` with `status: 202` inside (scheduled, not sent):

```json theme={null}
{
  "data": {
    "status": 202,
    "sent": 0,
    "scheduled": {
      "id": 1042, "status": "pending", "sendAt": "2026-11-19T19:30:00.000Z", "recipient": "guest@example.com",
      "transactional": "TRF4MS9", "tenant": "restaurant_4812", "cancellationKey": "booking-4812",
      "idempotencyKey": "booking-4812-reminder", "duplicate": false
    }
  }
}
```

* `transactional`: the template uid (7 characters). Send the master: the tenant's copy is picked when the email goes out.
* `sendAt`: ISO 8601 **with its UTC offset**, in the future, at most 90 days ahead (`422 INVALID_SEND_AT` otherwise, with a `userMessage` that says what is wrong). Without `sendAt` nothing changes: the email goes now and the response is the usual one.
* Checked now: the template exists, `tenant`, `replyTo`, the sender (`from`/`senderId` under a verified domain). Checked again when it goes out, like any send: the tenant's version of the template (picked at that moment), the sender, bounces, marketing consent (marketing templates only: a utility email is never blocked by consent) and the tenant's sending limits.
* `idempotencyKey`: the same key again returns the send already scheduled (`duplicate: true`), nothing new is planned. On `/templates/email/send` it is a UUID.
* Goes out **once**, at most a minute late; more than 5 minutes late (e.g. an outage) it is skipped, never sent late.
* Never for sign-in, verification or system emails and never with a one-time code or sign-in link in the fields (`422 SEND_AT_NOT_ALLOWED`). On `/templates/email/send` it schedules the template content: no `html`, `subject`, `text` or `attachments`.

```bash theme={null}
# Status of one scheduled send, or the list (filters: tenant, cancellationKey, status)
GET    /public/v1/transactionals/scheduled/1042
GET    /public/v1/transactionals/scheduled?tenant=restaurant_4812&cancellationKey=booking-4812
# Cancel one (only while pending; once sending or sent: 409 SCHEDULED_SEND_NOT_CANCELLABLE;
# an already cancelled one answers 200 with its row)
DELETE /public/v1/transactionals/scheduled/1042
# Cancel everything still pending for a booking of one tenant -> { "data": { "cancelled": 1 } }
DELETE /public/v1/transactionals/scheduled?cancellationKey=booking-4812&tenant=restaurant_4812
```

Send the `DELETE` calls without a body and without a `Content-Type: application/json` header (an empty JSON body is refused with `400`).

These sends are separate from the rules: a `booking.cancelled` event does not cancel them and `/public/v1/scheduled-sends` does not list them.

## Limits

* Email only; one event → one email per rule. No multi-step flows, waits or conditions (use the workspace automations for those).
* Changing a rule's offset does not move sends already planned: send the update event.
* Offsets up to one year.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.