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

# Quickstart: your first Voiceflow API conversation turn

> Run a real conversation turn against your agent in three curl calls: start a session, launch the conversation, and send a message.

Every conversation with a Voiceflow agent runs in two phases: you start a
session once to get a session key, then send actions against that key for as
long as the conversation lasts. The session key, not your API key, is what
authorizes each turn.

This recipe runs three requests: one to open the session, one to launch the
conversation, and one to send a user message. It needs nothing but `curl` and
`jq`.

<Info>Last verified 2026-08-13 by executing the recipe end to end.</Info>

## The recipe

```bash main.sh theme={null}
#!/usr/bin/env bash
# Recipe: first-conversation-turn
#
# Three calls: start a session (API key), launch the conversation (session
# key), send a user message (session key). Asserts its own behaviour with
# jq -e, so a zero exit IS the verification - the page's trust line comes
# from this script running, never from a claim.
#
# Required environment:
#   VF_API_KEY     project API key (Settings -> API keys), VF.DM....
#   VF_PROJECT_ID  the project ID (Settings -> General -> Metadata)
set -euo pipefail

: "${VF_API_KEY:?set VF_API_KEY to a project API key}"
: "${VF_PROJECT_ID:?set VF_PROJECT_ID to the project ID}"

BASE="https://general-runtime.voiceflow.com"
USER_ID="cookbook-$(date +%s)"

# The runtime can emit raw control characters inside trace payload strings,
# which strict JSON parsers reject. Discovered by running this recipe; strip
# them before jq. Compact JSON carries no raw newlines, so this is lossless
# for the assertions below.
json() { tr -d '\000-\037'; }

fail() { echo "FAIL at $1" >&2; exit 1; }

# 1. Start a session. Authenticates with the PROJECT API KEY.
SESSION=$(curl -sf -X POST "$BASE/v4/project/$VF_PROJECT_ID/session" \
  -H "Authorization: $VF_API_KEY" \
  -H "Content-Type: application/json" \
  -d "{ \"userID\": \"$USER_ID\" }") || fail "start session (a 401 here means the API key is wrong)"
SESSION_KEY=$(printf '%s' "$SESSION" | json | jq -er '.sessionKey') || fail "no sessionKey in the response"

# 2. Launch the conversation. Authenticates with the SESSION KEY.
LAUNCH=$(curl -sf -X POST "$BASE/v4/interact" \
  -H "Authorization: $SESSION_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": { "type": "launch" } }') || fail "launch (a 401 here means the API key was sent where the session key belongs)"
printf '%s' "$LAUNCH" | json | jq -e '.traces | length > 0' > /dev/null || fail "launch returned no traces"

# 3. Send a user message and expect traces back.
REPLY=$(curl -sf -X POST "$BASE/v4/interact" \
  -H "Authorization: $SESSION_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "action": { "type": "text", "payload": "What can you help me with?" } }') || fail "send message"
printf '%s' "$REPLY" | json | jq -e '.traces | length > 0' > /dev/null || fail "reply returned no traces"

echo "ok: launch returned $(printf '%s' "$LAUNCH" | json | jq '.traces | length') trace(s), reply returned $(printf '%s' "$REPLY" | json | jq '.traces | length') trace(s), types: $(printf '%s' "$REPLY" | json | jq -c '[.traces[].type] | unique')"
```

## How it works

* The session key is scoped to one user and one conversation. Starting a new
  session for the same `userID` ends the previous one.
* `launch` runs everything your agent does before the first user turn, which
  is why the first response can carry many traces.
* Every response is an array of [traces](/api-reference/trace-types). Your
  application decides how to render each type; most carry a `text` trace with
  the agent's reply.

## When it fails

| Symptom                                   | Cause                                                                                  | Fix                                                                      |
| ----------------------------------------- | -------------------------------------------------------------------------------------- | ------------------------------------------------------------------------ |
| `401` on the session request              | The `Authorization` header is missing, or carries a `Bearer` prefix it should not have | See [Authentication](/api-reference/authentication)                      |
| `401` on an interact request              | You passed the API key instead of the session key                                      | Pass the `sessionKey` from step 1                                        |
| Your JSON parser rejects a `200` response | Trace payload strings can carry raw control characters, which strict parsers reject    | Strip `\000`-`\037` before parsing, as the recipe's `json()` helper does |
