Integration Cookbook
Integration Cookbook
Complete, copy-pasteable journeys — not per-endpoint fragments. Every recipe
assumes BASE=https://ghostmind.optdmsa.com and a bearer key in
$GHOSTMIND_API_KEY. Errors use the canonical envelope
({ "error": { "code", "message", "request_id" } }) — see
Handling Errors.
Recipe 1 — Project-scoped chat with file retrieval
The flagship flow: a project with instructions + source files, then conversations that actually retrieve file content upstream.
# 1) Create the upstream project (instructions included)PROJECT=$(curl -sS -X POST "$BASE/v1/projects" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "name": "RetentionOS Engine", "instructions": "You are the reply engine. Always answer in JSON.", "memory_policy": "project_only" }')SLUG=$(echo "$PROJECT" | jq -r .slug) # e.g. "retentionos-engine"PROJ_ID=$(echo "$PROJECT" | jq -r .id) # GhostMind UUID
# 2) Upload a source file (multipart, max 10 MB)FILE=$(curl -sS -X POST "$BASE/v1/projects/$PROJ_ID/files" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -F "file=@brand_voice.txt;type=text/plain")FILE_ID=$(echo "$FILE" | jq -r .id)
# 3) Poll until the file is indexed ("ready" — retrieval needs indexing,# not just upload)while :; do ST=$(curl -sS "$BASE/v1/projects/$PROJ_ID" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ | jq -r '.sources[] | select(.id=="'"$FILE_ID"'") | .status') [ "$ST" = "ready" ] && break [ "$ST" = "failed" ] && { echo "file indexing failed"; exit 1; } sleep 2done
# 4) Chat inside the project — the SLUG goes in X-GhostMind-Upstream-Projectcurl -sS -X POST "$BASE/v1/chat/completions" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -H "X-GhostMind-Upstream-Project: $SLUG" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o","messages":[{"role":"user","content":"Summarize the brand voice file in one line."}]}'Gotchas that bite real integrators:
X-GhostMind-Upstream-Projecttakes the slug, not the UUID. Passing a UUID (or a wrong slug) returns404 upstream_project_not_found. Passing a slug viaX-GhostMind-Project(the internal header) returns422 wrong_project_header.- A file reporting
status: "uploading"/"processing"is not retrievable yet — chat before"ready"silently ignores the source. - Project file upload can return 403
UPSTREAM_ACCOUNT_NOT_AUTHORIZED: the connected ChatGPT account’s tier does not permit project file attachment. Checkcapabilities.project_files.enabledbefore building a flow around project files, and fall back to Recipe 5 conversation attachments (a different capability that works on all account tiers). memory_policy: "project_only"keeps project memory from leaking into the account’s general memory — recommended for multi-tenant workloads.
Recipe 2 — Durable conversation (send now, continue anytime)
# First message — capture the continuity header from the RESPONSERESP=$(curl -siS -X POST "$BASE/v1/chat/completions" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o","messages":[{"role":"user","content":"Remember: the invoice prefix is RT-."}]}')CONV=$(echo "$RESP" | grep -i '^x-ghostmind-conversation-id:' | tr -d '\r' | awk '{print $2}')
# Continue later (minutes or days) — same upstream conversation, same# project binding. Continuity is sticky to the original account.curl -sS -X POST "$BASE/v1/chat/completions" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -H "X-GhostMind-Conversation-Id: $CONV" \ -H "Content-Type: application/json" \ -d '{"model":"gpt-4o","messages":[{"role":"user","content":"What is the invoice prefix?"}]}'Continuity failure codes are explicit: 410 conversation_deleted_upstream,
419 conversation_session_expired, 409 conversation_continuity_lost,
503 conversation_upstream_unavailable. None silently start a new chat.
Recipe 3 — Async speech generation (queue + poll + download)
Speech generation takes ~30–120 s upstream — always use the async job path, not the bounded sync endpoint, for anything non-trivial.
Voice names are provider-specific Arabic names (e.g. يوسف, مالك, ليلى) —
there is no savio_default. List them first:
curl -sS "$BASE/v1/audio/providers" -H "Authorization: Bearer $GHOSTMIND_API_KEY"curl -sS "$BASE/v1/audio/providers/google_ai_studio_savio_tts/voices" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY"# Enqueue — returns 202 immediatelyJOB=$(curl -sS -X POST "$BASE/v1/audio/generations" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "input": "مرحباً بك في منصتنا", "voice": "يوسف", "response_format": "wav" }')JOB_ID=$(echo "$JOB" | jq -r .id)
# Poll — status: created → queued → processing → completed | failedwhile :; do ST=$(curl -sS "$BASE/v1/audio/generations/$JOB_ID" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" | jq -r .status) case "$ST" in completed) break ;; failed|cancelled) echo "job $ST"; exit 1 ;; esac sleep 5done
# Download — content expires after 24h by default# (operator-configurable via GHOSTMIND_AUDIO_JOB_TTL_HOURS)curl -sS "$BASE/v1/audio/generations/$JOB_ID/content" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" -o clip.wavRecipe 4 — Transcription
curl -sS -X POST "$BASE/v1/audio/transcriptions" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -F "file=@clip.wav" -F "model=whisper-1"# → {"text": "..."} (usually < 2 s for short clips)Recipe 5 — Conversation attachments (send files/images to chat)
Two steps, and both are required. Uploading stores the file and returns a
file_id, but the file is only visible to the model when you reference that
id in attachment_file_ids on the chat request. Uploading alone does not
attach it to the next message.
# 1) Upload to an existing conversation — capture file_idATT=$(curl -sS -X POST "$BASE/v1/conversations/$CONV/attachments" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -F "file=@invoice.pdf")FILE_ID=$(echo "$ATT" | jq -r .file_id) # e.g. file_00000000...
# 2) Reference it in the chat request bodycurl -sS -X POST "$BASE/v1/chat/completions" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -H "X-GhostMind-Conversation-Id: $CONV" \ -H "Content-Type: application/json" \ -d "{\"model\":\"gpt-4o\",\"attachment_file_ids\":[\"$FILE_ID\"], \"messages\":[{\"role\":\"user\",\"content\":\"Summarize the attached invoice.\"}]}"Notes:
file_ids are per upstream account — an id uploaded under one account cannot be used in a conversation bound to another (400 invalid_attachment).- Image attachments must have stored dimensions or are rejected
422. - Conversations created via
POST /v1/conversationsalready carry an upstream account binding, so attachments work immediately. A409 Conversation has no upstream account bindingonly occurs for records created through other paths that left the binding null.
Recipe 6 — Usage reconciliation
curl -sS "$BASE/v1/usage?period=7d" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY"# → totals + per-model + per-day buckets, scoped to THIS key only.Use it for quota dashboards and billing reconciliation — it never exposes other tenants or provider internals.
Recipe 7 — Cleanup discipline
GhostMind deletes are soft deletes (conversations restorable via
POST /v1/conversations/{id}/restore). For task-scoped conversations:
curl -sS -X DELETE "$BASE/v1/conversations/$CONV" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY"Hard purges are an operator decision, not an API self-service — see Deletion and Retention.
Recipe 8 — Run a UAC Action with a caller-supplied file
Published Actions whose inputs include a file field take an
asset_id — never inline bytes. Upload once, reference it in the run,
poll for the verdict.
# 1) Upload the file against the action (multipart `file` field,# actions:run scope required; default cap 20 MiB, TTL 24h)ASSET=$(curl -sS -X POST "$BASE/v1/actions/$ACTION_SLUG/assets" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -F "file=@photo.png;type=image/png")ASSET_ID=$(echo "$ASSET" | jq -r .asset_id)
# 2) Create the run — the file input is {"asset_id": "..."}RUN=$(curl -sS -X POST "$BASE/v1/actions/$ACTION_SLUG/runs" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY" \ -H "Content-Type: application/json" \ -d '{"input": {"photo": {"asset_id": "'"$ASSET_ID"'"}, "caption": "launch day"}}')RUN_ID=$(echo "$RUN" | jq -r .id)
# 3) Poll — 202 means ACCEPTED, not succeeded. The verdict is the truth.while :; do R=$(curl -sS "$BASE/v1/actions/$ACTION_SLUG/runs/$RUN_ID" \ -H "Authorization: Bearer $GHOSTMIND_API_KEY") S=$(echo "$R" | jq -r .status) [ "$S" = "queued" ] || [ "$S" = "running" ] || break sleep 2doneecho "$R" | jq '{status, verdict, error_code}'Semantics that matter:
- An asset is single-use — a second run with the same
asset_idfails validation. Upload again for each run. SUCCEEDEDmeans the outcome oracle confirmed the effect;needs_review/UNKNOWNare not success — inspect the run’s oracle evidence before treating it as done.- Assets are tenant- and action-scoped: an
asset_iduploaded under a different action or tenant is rejected at run creation.
Choosing the right header — the 30-second table
| Header | Takes | When |
|---|---|---|
X-GhostMind-Conversation-Id | conversation UUID | continue a conversation |
X-GhostMind-Upstream-Project | project slug | chat inside an upstream project |
X-GhostMind-Upstream-Account | account label | pin a specific connected account |
X-GhostMind-Route-Profile | profile slug | named routing policy |
X-GhostMind-Project | internal project | platform-internal bindings — most integrators never need it |
X-Request-Id | any string | correlation through errors/logs |