Vocal Journaling API
  • Documentation
  • API Reference
Vocal Journaling API Gateway
powered by Zuplo
Documentation

Vocal Journaling API Gateway

This workspace publishes the public API contract for vocal-journaling-api.

Use this API as an authenticated JSON/multipart interface for:

  1. Voice upload and analysis
  2. Text chat and coaching endpoints
  3. Aggregated insights
  4. Recent history retrieval

/health remains public.

Authentication and onboarding

This portal now has two distinct layers:

  1. Developer portal sign-in
  2. App user authentication

Developer portal sign-in

Developers sign in to this portal with Supabase credentials. Once signed in, Zuplo can manage developer-facing API keys from the portal UI.

This is for:

  1. Access to the portal as a known developer
  2. API key lifecycle management in Zuplo
  3. Future developer-key-protected gateway routes

App user authentication

The application routes in this API are still user-scoped. Protected routes currently require a valid Supabase user access token in the request:

Code
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR...

This is for:

  1. Identifying the end user whose data is being read or written
  2. Voice uploads, history, insights, chat, and coaching
  3. Provisioning and storing per-user analysis data

So today the flow is:

  1. A developer signs into the portal
  2. A developer can manage Zuplo API keys
  3. App requests still send a Supabase user JWT when calling user-scoped endpoints

Developer API keys do not replace app user identity for the current endpoint set.

Error contract

All documented errors use:

Code
{ "error": "Human-readable message", "code": "MACHINE_READABLE_CODE", "requestId": "optional-trace-id" }

Error code matrix

EndpointCodeMeaning
POST /api/v1/voiceMISSING_FILEaudio file was not included
POST /api/v1/voiceINVALID_FILE_TYPEFile MIME type is not allowed
POST /api/v1/voiceINVALID_FILE_SIZEUploaded file exceeds VOICE_UPLOAD_MAX_BYTES
POST /api/v1/chatINVALID_REQUESTMissing text, non-string text, or length > 10,000
POST /api/v1/coachingINVALID_REQUESTtext is present but invalid
GET /api/v1/insightsINVALID_REQUESTperiod is not day, week, or month
GET /api/v1/historyINVALID_REQUESTlimit or days outside documented bounds
*/api/v1/*UNAUTHORIZEDMissing or invalid bearer token
*INTERNAL_ERRORUnexpected server failure

Endpoint Reference

POST /api/v1/voice — Upload and analyze a recording

  • Auth: required (Bearer token)
  • Content type: multipart/form-data
  • Form field:
    • audio (required): audio bytes only
  • Validation:
    • audio required
    • MIME must be audio-compatible
    • VOICE_UPLOAD_MAX_BYTES default: 25 MB

Success response

Code
{ "id": "analysis-1", "transcript": "Hello, this is the transcript.", "sentiment": { "overall": "neutral", "score": 0.1 }, "voiceType": "steady", "dynamics": {}, "duration": 12.4, "features": {}, "classifications": {}, "nlpAnalysis": {}, "createdAt": "2026-03-07T00:00:00.000Z", "processingTime": 123, "notification": null }

Examples

TerminalCode
curl -X POST "http://localhost:9000/api/v1/voice" \ -H "Authorization: Bearer $SUPABASE_JWT" \ -F "audio=@./samples/recording.wav"
Code
const form = new FormData(); form.append("audio", fileInput.files[0]); const response = await fetch("http://localhost:9000/api/v1/voice", { method: "POST", headers: { Authorization: `Bearer ${token}` }, body: form, }); const result = await response.json();
Code
import requests with open("recording.wav", "rb") as f: resp = requests.post( "http://localhost:9000/api/v1/voice", headers={"Authorization": f"Bearer {token}"}, files={"audio": ("recording.wav", f, "audio/wav")}, ) print(resp.status_code, resp.json())

Error example

Code
{ "error": "Audio file is required", "code": "MISSING_FILE" }

POST /api/v1/chat — Chat text analysis

  • Auth: required
  • Content type: application/json
  • Body:
    • text (required string, maxLength 10000)
Code
{ "response": "Keep breathing steady while speaking." }
Code
{ "error": "text is required", "code": "INVALID_REQUEST" }

POST /api/v1/coaching — Generate coaching guidance

  • Auth: required
  • Content type: application/json
  • Body:
    • text (optional string, maxLength 10000)
    • omitted text uses fallback coaching prompt
Code
{ "advice": "Try pacing your sentences.", "question": "How can I improve my voice?" }
Code
{ "error": "text must be a string", "code": "INVALID_REQUEST" }

GET /api/v1/insights — Aggregate summary

  • Auth: required
  • Query:
    • period optional: day | week | month (default month)

Success response

Code
{ "summary": { "period": "month", "totalRecordings": 18, "trend": "up" }, "milestone": "You maintained a 3-day streak this month", "tips": [ "Record consistently for short intervals.", "Pause between long phrases." ] }

Query examples

TerminalCode
curl -H "Authorization: Bearer $SUPABASE_JWT" \ "http://localhost:9000/api/v1/insights?period=day"
Code
const response = await fetch("http://localhost:9000/api/v1/insights?period=week", { headers: { Authorization: `Bearer ${token}` }, }); const result = await response.json();
Code
import requests resp = requests.get( "http://localhost:9000/api/v1/insights?period=month", headers={"Authorization": f"Bearer {token}"} ) print(resp.status_code, resp.json())
Code
{ "error": "Invalid period. Allowed values are day, week, or month.", "code": "INVALID_REQUEST" }

GET /api/v1/history — Recent voice history

  • Auth: required
  • Query:
    • limit optional integer 1..100 (default 10)
    • days optional integer 1..365 (default 30)

Success response

Code
{ "history": [ { "id": "analysis-1", "createdAt": "2026-03-07T00:00:00.000Z", "transcript": "Short spoken segment.", "duration": 11.2, "sentiment": "neutral" } ] }
TerminalCode
curl -H "Authorization: Bearer $SUPABASE_JWT" \ "http://localhost:9000/api/v1/history?limit=25&days=7"
Code
const response = await fetch("http://localhost:9000/api/v1/history?limit=10&days=30", { headers: { Authorization: `Bearer ${token}` }, }); const result = await response.json();
Code
import requests resp = requests.get( "http://localhost:9000/api/v1/history?limit=10&days=30", headers={"Authorization": f"Bearer {token}"} ) print(resp.status_code, resp.json())
Code
{ "error": "limit must be an integer between 1 and 100", "code": "INVALID_REQUEST" }

GET /health — Public health check

  • Auth: none
Code
{ "status": "ok", "version": "1.0.0" }

Rate limits and environment

Policies and limits are defined in config/routes.oas.json and gateway policy files.

TerminalCode
pnpm run zuplo:dev pnpm run zuplo:docs

For endpoint-level API behavior tests:

TerminalCode
pnpm run test:api:routes

For gateway/contract checks:

TerminalCode
pnpm run zuplo:test

Local requestId assertions and error shape tests are included in the API route suite.

Last modified on March 10, 2026
On this page
  • Authentication and onboarding
    • Developer portal sign-in
    • App user authentication
  • Error contract
    • Error code matrix
  • Endpoint Reference
    • POST /api/v1/voice — Upload and analyze a recording
    • POST /api/v1/chat — Chat text analysis
    • POST /api/v1/coaching — Generate coaching guidance
    • GET /api/v1/insights — Aggregate summary
    • GET /api/v1/history — Recent voice history
    • GET /health — Public health check
  • Rate limits and environment
JSON
JSON
Javascript
JSON
JSON
JSON
JSON
JSON
JSON
Javascript
JSON
JSON
Javascript
JSON
JSON