MailcrateDocs

Triggering Journeys

Start a journey for a contact from your own code or from any tool that can send an HTTP request, pass it data, and take contacts back out.

Journeys are built in the app and driven from outside it. Besides recording an event that a journey listens for, there are two direct ways to put a contact into a journey:

Journey hook URLEnroll endpoint
CallPOST /hooks/journeys/{token}POST /public/journey/{id}/enroll
AuthenticationNone: the URL is the secretAPI key with journey:write
Unknown contactCreated from email404; the contact must exist
Result{ "accepted": true }, enrollment happens moments laterThe enrollment, or an error saying why not
Safe to retryNoYes, with idempotencyKey
Best forForm builders, Zapier, a partner's systemYour own backend

Building the journey itself is covered in the user guides: Journey Builder and Triggers.

Journey hook URLs

A journey whose trigger is a Webhook node gets a URL of its own. Anything that can send JSON can start the journey with it, with no API key.

Get the URL

In the journey builder, add a Webhook trigger and select it. The panel shows its URL, https://api.mailcrate.io/hooks/journeys/<token>, with a copy button. Each Webhook trigger node has its own URL, and it is created as soon as the draft is saved.

Publish the journey

Requests are only turned into enrollments while the journey is active and the published version still contains that Webhook node. Until then, calls are accepted and dropped.

Send a request

POST/hooks/journeys/{token}
Terminal
curl -X POST https://api.mailcrate.io/hooks/journeys/$JOURNEY_HOOK_TOKEN \
  -H "Content-Type: application/json" \
  -d '{
    "email": "ada@example.com",
    "externalIdentifier": "user_8231",
    "properties": {
      "plan": "pro",
      "order": { "id": "5512", "total": 129.5 }
    }
  }'
200 OK
{ "accepted": true }

The builder shows the last payload the URL received and when, which is the quickest way to check that a tool is sending what you expect. Send the Content-Type: application/json header with a JSON body. Without it the body is not read as JSON, and the request fails with 400 for having no identifier.

The request body

FieldWhat it is
contactIdMailcrate's contact id (a UUID).
externalIdentifierYour own id for the person.
emailTheir email address.
propertiesOptional. Any JSON object: the journey's trigger data, see below.

At least one of contactId, externalIdentifier and email is required. Mailcrate tries them in that order and uses the first that finds a contact. When none does and an email was sent, it creates the contact, with the externalIdentifier on it if you sent one. That creation is subject to the consent gate. Without an email, an unknown contact is a 404 Contact not found.

What properties does

The object does two things once the contact is enrolled:

  • It becomes the trigger data. Every key is available to the journey's steps as {{ trigger.<key> }}, nested objects included: {{ trigger.order.total }}. Use it, for instance, to fill a Send Email step's template variables. A payload over 16 KB of JSON is dropped from the trigger data, and the run goes ahead without it.
  • Its top-level text, number and boolean values are written to the contact as properties, before the first step runs. Nested objects and arrays are not. Unknown keys become new text properties, as with the contact API. If a value does not fit a typed property, none of the properties from that request are written, and the run still goes ahead.

Accepted is not enrolled

{ "accepted": true } means Mailcrate recorded the request. The enrollment is decided a few seconds later, and the hook never tells you how that went, so a URL cannot be used to probe a journey's state. A request leads to no enrollment when:

  • the journey is a draft, paused or archived, or the Webhook node is not in its published version;
  • the journey has re-entry turned off and the contact has been through it before;
  • the contact is in the journey right now (a contact has at most one live run per journey);
  • the journey is at its active-contact limit.

The last three are recorded in the journey's activity as a skipped enrollment. To learn the outcome in your own system, subscribe a webhook to journey.enrolled.

Every request is a new trigger. A tool that retries a request it already delivered can enroll the contact twice, once the first run has finished, on a journey that allows re-entry.

Errors

StatusWhen
400The body failed validation, for example no identifier at all, or contactId is not a UUID.
403A new contact would be created and the workspace has not confirmed the consent statement yet.
404Journey hook not found for an unknown token, or Contact not found.
429More than 600 requests in 5 minutes from one IP address. Wait and retry.

Keep the URL secret

Whoever has the URL can create contacts in your workspace and put them into the journey, so treat it like an API key: keep it out of client-side code and public repositories. A URL cannot be rotated. To retire one, delete its Webhook node, add a new one (which gets a new URL) and publish. The old URL then stops enrolling anyone, but it still answers { "accepted": true } and still creates contacts from unknown email addresses.

The enroll endpoint

When your own backend decides who enters a journey, call it with an API key. You get the outcome in the response, and retries are safe.

POST/public/journey/{id}/enrollscope · journey:write

Find the journey's id in the app's URL, or list journeys with GET /public/journey; each one carries its status, its reEnrollment setting and a summary of its triggers.

Terminal
curl -X POST https://api.mailcrate.io/public/journey/0c5e2b7a-4d1f-4a8e-9c3b-6e2f1a7d8b90/enroll \
  -H "Authorization: Api-Key $MAILCRATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "contactId": "user_8231",
    "data": { "trialEndsAt": "2026-08-17", "seats": 5 },
    "idempotencyKey": "trial-onboarding-user_8231"
  }'
200 OK
{
  "enrollment": {
    "id": "e41c9a0d-7b2e-4f6a-8d35-0a9b1c2d3e4f",
    "journeyId": "0c5e2b7a-4d1f-4a8e-9c3b-6e2f1a7d8b90",
    "status": "active",
    "contact": { "id": "3f0d8a52-1c4e-4b8e-9a51-7f3c2b6d9e10", "email": "ada@example.com" },
    "goalReachedAt": null,
    "enteredAt": "2026-08-03T11:20:05.311Z",
    "finishedAt": null
  },
  "idempotentReplay": false
}
FieldWhat it is
contactIdA contact id, external identifier or email address.
emailAn email address. Use either field; at least one is required.
dataOptional trigger data, available as {{ trigger.<key> }}, dropped above 16 KB. Unlike a hook's properties, it is not written to the contact.
idempotencyKeyOptional, up to 200 characters of letters, digits, ., _, - and :.

The endpoint never creates a contact. Create or sync it first.

Retrying safely

An idempotencyKey is scoped to the journey and the contact. Repeating a request with the same key returns the original enrollment with "idempotentReplay": true, even when a fresh enrollment would now be refused, so a retry after a timeout can never enroll twice or turn into a confusing 409.

When enrollment is refused

StatusMessageWhat it means
404Journey with id: "…" not found, Contact "…" not foundCheck the id; the contact must exist.
409Contact is currently in this journey — wait for the run to finishOne live run per contact per journey.
409Contact has already been in this journey and re-enrollment is disabled for itThe journey's re-entry setting is off.
422Journey must be active to enroll contactsDraft, paused or archived.
422Journey is at its active-contact limitTry again once runs finish, or raise the limit.
422Journey is not currently accepting enrollmentsIt changed under the request, for example a new version was published.

Mailcrate does not check suppression here. A suppressed or unsubscribed contact is enrolled, and the journey's send steps skip mail they may not receive, the same as for every other way in.

Take a contact out

POST/public/journey/{id}/exitscope · journey:write
Terminal
curl -X POST https://api.mailcrate.io/public/journey/0c5e2b7a-4d1f-4a8e-9c3b-6e2f1a7d8b90/exit \
  -H "Authorization: Api-Key $MAILCRATE_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "contactId": "user_8231" }'

The response is { "exited": 1, "enrollments": [ … ] }. It is safe to repeat: a contact with no live run answers "exited": 0, not an error. It also works on paused and archived journeys. A typical use is stopping a trial-nurture journey the moment the customer upgrades. If the journey has a goal and is set to exit when the goal is reached, recording the goal's event does the same without a call per journey.

Follow what happens

  • GET /public/journey/{id}/enrollments lists runs, optionally for one contact with ?contactId= (a contact id, external identifier or email).
  • GET /public/journey/{id}/stats returns the numbers the builder shows, counting real runs only.
  • The journey.enrolled and journey.exited webhook events report every run as it starts and ends, whichever way it was triggered.

Next steps

On this page