This recipe sends a thank-you email after every order. You create a journey that starts
on the Order Completed event, activate it, track an order for a contact, and check that
the contact entered the journey.
Every POST here carries an Idempotency-Key, so running a step twice is safe: the
second run returns the first response. If you change a request’s body, change its key
too, or the API returns 409 IDEMPOTENCY_KEY_REUSED.
Before you start
- An API key with
journeys:write,journeys:activate,contacts:write,events:write,contacts:readandmessages:read. See Authentication. - Room in the plan’s event allowance. Free takes 1,000 events a month, enough to try
this recipe; past the allowance, tracking returns 403
PLAN_LIMIT_REACHED. - What marketing email needs: a
fromaddress on a verified sending domain with a DMARC record, and the workspace’s postal address. Activation checks both. - cURL and jq, and
KIRORA_API_URLandKIRORA_API_KEYset as in the Quickstart.
Replace mail.juniperkitchen.com with your verified domain in step 1.
1 Create the journey
The journey starts on Order Completed (names are case-sensitive), lets a contact enter
again with every order, and sends one email that names the order from the event’s
properties:
curl "$KIRORA_API_URL/v1/journeys" \
--fail-with-body --silent --show-error -o journey.json \
-H "Authorization: Bearer $KIRORA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: recipe-order-journey-create-001" \
-d '{
"name": "Order thank-you",
"trigger": {
"type": "event",
"name": "Order Completed",
"reentry": { "mode": "repeatable", "waitAfterEntry": { "value": 0, "unit": "days" } }
},
"steps": [
{
"id": "thank-you-email",
"type": "send",
"channel": "email",
"content": {
"subject": "Thanks for your order",
"from": "Juniper Kitchen <[email protected]>",
"templateSnapshot": {
"schemaVersion": 1,
"blocks": [
{ "id": "title", "type": "heading", "text": "Thanks for your order" },
{ "id": "body", "type": "text", "text": "We have order {{ event.properties.order_id | default: \"your order\" }} and are packing it now." },
{ "id": "footer", "type": "footer", "text": "{{ org.name }}\n{{ org.address }}", "locked": true }
],
"theme": {}
}
},
"nextStep": null
}
]
}'
JOURNEY_ID=$(jq -er '.data.id' journey.json)The response is 201 with the journey in status: "draft".
2 Activate it
curl "$KIRORA_API_URL/v1/journeys/$JOURNEY_ID/activate" \
--fail-with-body --silent --show-error \
-H "Authorization: Bearer $KIRORA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: recipe-order-journey-activate-001" \
-d '{}'The response is 200 with the journey active. Only events tracked from now on start it.
A 409 DRAFT_INCOMPLETE lists what is missing in errors; the other refusals are
in Campaigns and journeys.
3 Create the customer
Marketing email goes only to contacts who consented. This creates the contact with your
own id and records how they agreed. [email protected] is a
test address: the email goes through every check, is
reported as delivered, and reaches no inbox.
curl "$KIRORA_API_URL/v1/contacts/identify" \
--fail-with-body --silent --show-error \
-H "Authorization: Bearer $KIRORA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: recipe-order-journey-contact-001" \
-d '{
"externalId": "customer-4821",
"email": "[email protected]",
"firstName": "Amaka",
"consent": {
"email": true,
"statement": { "basis": "opted_in", "evidence": "Checkout opt-in on juniperkitchen.com" }
}
}'The response is 201 when the contact is new, 200 when it already existed.
4 Track the order
Track Order Completed with the order’s properties, in major units (see
E-commerce events). Use the order id in the Idempotency-Key, so
a retried order is recorded once:
curl "$KIRORA_API_URL/v1/events/track" \
--fail-with-body --silent --show-error \
-H "Authorization: Bearer $KIRORA_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1001-completed" \
-d '{
"externalId": "customer-4821",
"name": "Order Completed",
"properties": {
"order_id": "1001",
"total": 45.00,
"currency": "USD",
"products": [
{ "product_id": "sku-paprika", "name": "Smoked paprika", "price": 15.00, "quantity": 3 }
]
}
}'The response is 201: the event is stored. The journey picks it up in the background
afterwards, not within this request. A contact that does not exist returns 404; tracking
never creates one.
5 Check the enrollment
List the contact’s journeys, by your own id:
curl "$KIRORA_API_URL/v1/contacts/customer-4821/journeys?idType=externalId" \
--fail-with-body --silent --show-error \
-H "Authorization: Bearer $KIRORA_API_KEY"An entry whose journey.id is your journey’s shows the contact entered: status is active while it
moves through the steps and completed once it has. If the list is empty, read it again
shortly. Then read the message it sent:
curl "$KIRORA_API_URL/v1/messages?journeyId=$JOURNEY_ID" \
--fail-with-body --silent --show-error \
-H "Authorization: Bearer $KIRORA_API_KEY"For the test address the message ends delivered
with simulated: true. To learn of each enrollment as it happens, subscribe to the
journey.enrolled webhook instead of reading the list.
If nothing happens
- No enrollment: the event name differs from the trigger’s
name(case included), or the journey was notactivewhen the event was tracked. - An enrollment, but the message is
skipped:skip.reasonsays why, for exampleconsent_missingwhen the contact has no email consent.