Record contact events
/api/v1/contact-eventsTells Mailyte that something happened to a person in your own system — a checkout started, an order completed, an application submitted. Automations that start on that event, wait for it or leave on it react within seconds. Read Sending contact events before you integrate.
Send up to 500 events in events, or post a single event's fields at the top level.
Each event is checked on its own: a bad one is listed in rejected with its position and
the reason, and the others are recorded. The request fails as a whole only when its shape
is wrong — no events, more than 500, an item that is not an object — or the key lacks
contact_events:write.
Four things to know before you send the first one:
- An event is not consent. An
emailwe have never seen creates a contact with the statusunconfirmed. It can enter automations, but no automated email reaches it until the contact is subscribed. Record consent through the contacts endpoints, not here. - Send
unique_id. Repeating an event with the sameunique_idis counted underduplicatesand changes nothing, which is what makes a retry safe. Without one, a key is derived from the event's name, person, time and properties — which only catches an exact repeat. - Old events never start anything. An event whose
occurred_atis more than 7 days before we receive it is stored as historical and never starts, moves or ends a journey. Events are kept for 90 days fromoccurred_at, so anything older is removed overnight. - The limit is 600 requests a minute per organization, across all its keys. Batch rather than sending one request per event.
Request body
eventsarray<object>requiredThe events to record. To send one event you may instead put its fields at the top level of the body.
eventstringrequiredWhat happened, in lowercase snake_case starting with a letter, up to 64 characters: `checkout_started`, `order_completed`. This is the name the automation builder offers.
emailstringWho it happened to. Matched case-insensitively; an address we have not seen creates an `unconfirmed` contact. Send this or `contact_id`.
contact_idstringThe contact's Mailyte id, instead of `email`. Wins when both are sent. Must belong to your organization.
namestringThe person's name. Used only when this event creates the contact; never changes an existing one.
occurred_atstringWhen it happened, ISO 8601 with an offset. Defaults to now. At most five minutes in the future. More than 7 days in the past makes it historical: stored, and never starts or moves a journey.
unique_idstringYour own id for this occurrence — an order number, a checkout id. Unique per organization: a repeat is reported as a duplicate and ignored.
propertiesobjectAnything about it, up to 16 KB as JSON. Available to the journey as `{{ event.<key> }}` in emails and to its checks, e.g. `event.total`.
Request
/api/v1/contact-eventscurl -X POST 'https://app.mailyte.com/api/v1/contact-events' \
-H 'Authorization: Bearer mk_live_YOUR_API_KEY' \
-H 'Content-Type: application/json' \
-d '{
"events": [
{
"event": "checkout_started",
"email": "ada@example.com",
"occurred_at": "2026-09-23T10:04:11+01:00",
"unique_id": "checkout-81234",
"properties": {
"cart_value": 45000,
"currency": "NGN",
"items": 3
}
},
{
"event": "order_completed",
"email": "tunde@example.com",
"unique_id": "order-50917",
"properties": {
"total": 12500,
"category": "consumable"
}
}
]
}'const response = await fetch('https://app.mailyte.com/api/v1/contact-events', {
method: 'POST',
headers: {
Authorization: 'Bearer mk_live_YOUR_API_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
"events": [
{
"event": "checkout_started",
"email": "ada@example.com",
"occurred_at": "2026-09-23T10:04:11+01:00",
"unique_id": "checkout-81234",
"properties": {
"cart_value": 45000,
"currency": "NGN",
"items": 3
}
},
{
"event": "order_completed",
"email": "tunde@example.com",
"unique_id": "order-50917",
"properties": {
"total": 12500,
"category": "consumable"
}
}
]
}),
});
const { data } = await response.json();import requests
response = requests.post(
"https://app.mailyte.com/api/v1/contact-events",
headers={"Authorization": "Bearer mk_live_YOUR_API_KEY"},
json={
"events": [
{
"event": "checkout_started",
"email": "ada@example.com",
"occurred_at": "2026-09-23T10:04:11+01:00",
"unique_id": "checkout-81234",
"properties": {
"cart_value": 45000,
"currency": "NGN",
"items": 3
}
},
{
"event": "order_completed",
"email": "tunde@example.com",
"unique_id": "order-50917",
"properties": {
"total": 12500,
"category": "consumable"
}
}
]
},
)
data = response.json()["data"]<?php
$response = Http::withToken('mk_live_YOUR_API_KEY')
->post('https://app.mailyte.com/api/v1/contact-events', [
'events' => [
[
'event' => 'checkout_started',
'email' => 'ada@example.com',
'occurred_at' => '2026-09-23T10:04:11+01:00',
'unique_id' => 'checkout-81234',
'properties' => [
'cart_value' => 45000,
'currency' => 'NGN',
'items' => 3,
],
],
[
'event' => 'order_completed',
'email' => 'tunde@example.com',
'unique_id' => 'order-50917',
'properties' => [
'total' => 12500,
'category' => 'consumable',
],
],
],
]);
$data = $response->json('data');require "net/http"
require "json"
uri = URI("https://app.mailyte.com/api/v1/contact-events")
request = Net::HTTP::Post.new(uri)
request["Authorization"] = "Bearer mk_live_YOUR_API_KEY"
request["Content-Type"] = "application/json"
request.body = {
"events": [
{
"event": "checkout_started",
"email": "ada@example.com",
"occurred_at": "2026-09-23T10:04:11+01:00",
"unique_id": "checkout-81234",
"properties": {
"cart_value": 45000,
"currency": "NGN",
"items": 3
}
},
{
"event": "order_completed",
"email": "tunde@example.com",
"unique_id": "order-50917",
"properties": {
"total": 12500,
"category": "consumable"
}
}
]
}.to_json
response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: true) { |http| http.request(request) }Response
Accepted. Read `rejected` — a 202 does not mean every event was recorded.
dataobjectacceptedintegerEvents recorded.
duplicatesintegerEvents already recorded under the same unique_id; nothing happened twice.
rejectedarray<object>indexintegerPosition in the events array you sent.
reasonstring
Returned inside the standard envelope.
Errors
| Status | When |
|---|---|
401 | The API key is missing, unknown, revoked or expired. All four answer identically, on purpose: distinguishing them would confirm which keys exist. |
403 | The key is valid but may not do this: it lacks the required scope, its IP allowlist does not include you, or this endpoint does not accept API keys. |
404 | No such resource in this organization. |
422 | The request was understood but the values were not acceptable. |
429 | Too many requests, or the organization has spent its sending allowance. `Retry-After` says how long to wait. |
Every status, with what causes it and what to do, is on the error reference.