A hands-on walkthrough: create a flow, publish it, submit data with curl, read it back.
Time: ~10 minutes · You'll use: curl only (no frontend required)
A simple onboarding intake: name + email, saved when someone submits. Optional: webhook notification.
flowchart LR
dev[Developer_API_key] --> createFlow[Create_flow]
createFlow --> addVersion[Add_draft_version]
addVersion --> publish[Publish_v1]
publish --> user[User_embed_key]
user --> submit[Submit_intake]
submit --> db[(Intake_Relay)]
submit --> webhook[Webhook_optional]
| Term | Plain English |
|---|---|
| Organization | Your account |
API key (mfk_...) |
Admin key — create flows, read all submissions |
| Flow | One intake process (e.g. "SaaS onboarding") |
| Version | A snapshot of fields + steps (v1, v2, …) |
| Publish | Lock a version so embeds can use it |
Embed key (ifk_...) |
Public key — submit data, read published schema |
| Intake | One person's submitted answers |
Rule: API key in your backend only. Embed key in your frontend.
Create a free account and verify your email.
curl -s -X POST https://willowriverautomation.com/relay/v1/auth/api-key \
-H 'Content-Type: application/json' \
-d '{"email":"you@example.com","password":"your-password"}'
Copy the mfk_... value from the response. It is shown once.
export API_KEY="mfk_..." # paste yours
export BASE="https://willowriverautomation.com/relay"
All API requests in this tutorial use $BASE/v1/....
What: Confirm the API is reachable.
curl -s "$BASE/health"
Expected: {"status":"ok"}
What: Create a named intake process. Intake Relay returns an embed key for this flow.
Why: A flow is the container — versions and submissions live under it.
curl -s -X POST "$BASE/v1/flows" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"name":"My Onboarding","slug":"onboarding"}'
Save from the response:
export FLOW_ID="<id from response>"
export EMBED_KEY="<embed_key from response>"
What: Add fields and steps as JSON. Status starts as draft.
Why: Draft lets you edit before going live. Published versions are immutable.
curl -s -X POST "$BASE/v1/flows/$FLOW_ID/versions" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"schema": {
"fields": [
{"id":"name","type":"text","label":"Your name","validation":{"required":true}},
{"id":"email","type":"email","label":"Work email","validation":{"required":true}}
],
"steps": [
{"id":"about","title":"About you","fieldIds":["name","email"]}
]
}
}'
Expected: "version_number": 1, "status": "draft"
What: Make version 1 live. Embeds and intake submissions can now target it.
curl -s -X POST "$BASE/v1/flows/$FLOW_ID/versions/1/publish" \
-H "Authorization: Bearer $API_KEY"
Expected: "status": "published"
What: Send answers like a user filling the form.
Why: Uses embed key, not API key — same as your frontend would.
curl -s -X POST "$BASE/v1/flows/$FLOW_ID/intake?version=1" \
-H "X-Embed-Key: $EMBED_KEY" \
-H "Content-Type: application/json" \
-d '{"data":{"name":"Alex","email":"alex@example.com"}}'
Save:
export SUBMISSION_ID="<id from response>"
Expected: "status": "accepted", HTTP 201
What: Fetch the saved submission with your API key.
curl -s "$BASE/v1/intake/$SUBMISSION_ID" \
-H "Authorization: Bearer $API_KEY"
Expected: "data": {"name": "Alex", "email": "alex@example.com"}
Same key = same submission, no duplicate:
curl -s -X POST "$BASE/v1/flows/$FLOW_ID/intake?version=1" \
-H "X-Embed-Key: $EMBED_KEY" \
-H "Idempotency-Key: signup-alex-001" \
-H "Content-Type: application/json" \
-d '{"data":{"name":"Alex","email":"alex@example.com"}}'
Run twice — second response has "idempotent_replay": true.
Get notified on every submission:
curl -s -X POST "$BASE/v1/flows/$FLOW_ID/webhooks" \
-H "Authorization: Bearer $API_KEY" \
-H "Content-Type: application/json" \
-d '{"url":"https://example.com/webhook","events":["intake.submitted"]}'
See Webhooks for signature verification.
<div id="onboarding"></div>
<script
src="https://willowriverautomation.com/relay/v1/intake.js"
data-flow-id="YOUR_FLOW_ID"
data-version="1"
data-embed-key="YOUR_EMBED_KEY"
data-target="#onboarding">
</script>
See Embedding for React and callbacks.
You just:
Go deeper:
| Problem | Fix |
|---|---|
401 unauthorized |
Check Authorization: Bearer $API_KEY or X-Embed-Key: $EMBED_KEY |
400 version_not_published |
Run the publish step first |
400 validation |
Missing required field — check schema |
429 quota_exceeded |
Free tier limits — see billing or upgrade |
| Connection errors | Confirm $BASE is https://willowriverautomation.com/relay and your network allows HTTPS |