Skip to docs content
Public docsEnglish firstVerified B2B contract

Moji developer workspace

Build media intelligence into your product.

Integrate Moji text, voice, SSE responses, credits, webhooks, and invoices using customer API keys from the MojitoFilms platform.

sse-json-v1
POST/api/v3/assistant/

{

"prompt": {

"params": {

"b2b": true,

"session_id": "website-001",

"user_message": "Recommend...",

"stream_protocol": "sse-json-v1"

}

}

}

Text

SSE JSON v1

Voice

WebSocket

Billing

Credits-key auth

1

Key

Portal

2

Request

B2B contract

3

Moji

SSE or voice

Need a key?

Log in to create API keys, inspect usage, and configure webhooks for your organization.

Log in to portal

Integration model

Overview

1

Portal key

Create and rotate customer keys inside the MojitoFilms portal.

2

Clean request

Send b2b: true with a customer user_id or website session_id.

3

Choose output

Pick HTTP text for cards and captions, or WebSocket voice when you need playable speech.

Customer user

Use a stable customer end-user id when your product has accounts.

Website bot

Use only session_id when Moji runs as a no-user website bot.

Clean contract

Send b2b: true with a scoped customer identity.

The B2B contract has two public integration paths. HTTP text requests return structured SSE events for text, cards, trailers, cinemas, and showtimes. WebSocket voice requests accept either text input or audio input and return voice-capable output. HTTP text authenticates with X-Mojito-Credits-Key. Browser WebSocket clients send the same customer key in the init frame as payment_api_key.

First request

Quickstart

  1. 1

    Create an API key in the portal. The plaintext key is shown once.

  2. 2

    For text and card UI, start with POST /api/v3/assistant/ and X-Mojito-Credits-Key.

  3. 3

    Set b2b: true, provide either session_id or user_id, and send the customer message.

  4. 4

    Set stream_protocol: sse-json-v1 so every response event has the same JSON envelope.

SSE website bot requestbash
export MOJITO_AI_BASE_URL="https://ai-dev.mojitofilms.de"
export MOJITO_CREDITS_KEY="mfk_live_replace_with_your_key"

curl -N "$MOJITO_AI_BASE_URL/api/v3/assistant/" \
  -H "Content-Type: application/json" \
  -H "X-Mojito-Credits-Key: $MOJITO_CREDITS_KEY" \
  -d '{
    "prompt": {
      "id": "mojito_assistant",
      "params": {
        "b2b": true,
        "session_id": "website-session-001",
        "action": "assistant",
        "user_message": "Recommend one warm science fiction movie for Friday night.",
        "user_details": {},
        "stream_protocol": "sse-json-v1"
      }
    },
    "system": {},
    "model": {},
    "module": "mojito"
  }'

Keys and secrets

Authentication

HTTP

Send X-Mojito-Credits-Key for text requests.

WebSocket

Browser clients cannot set arbitrary WebSocket headers, so send payment_api_key in the init JSON frame.

  • Keep customer API keys server-side whenever possible. Do not put keys in query strings or logs.
  • Customer requests do not use Authorization: Bearer for the AI server.
  • user_details is optional. If present, it must be a JSON object. null, arrays, and strings are rejected.

Fields

Payload reference

Use this section as a field dictionary after you choose an integration path. It covers the HTTP request envelope, prompt.params for text requests, and the shared user_details object. For a voice integration, use the Voice API section for the connection sequence and then come back here only for shared fields such as user_id, session_id, user_language, and user_details.

Provide at least one of user_id or session_id. If your product has customer accounts, send user_id; add session_id when you want separate conversation threads for the same customer. For a no-account website bot, send only session_id.

HTTP JSON envelope
ParameterTypeRequirementHow to use it
promptobjectRequiredRequired. Container for the Moji prompt id and customer-facing params.
prompt.idstringRequiredRequired. Use mojito_assistant for the B2B assistant contract.
prompt.paramsobjectRequiredRequired. Holds auth mode, conversation identity, message, and optional personalization context.
systemobjectRequiredRequired for compatibility. Send an empty object for the current B2B contract.
modelobjectRequiredRequired for compatibility. Send an empty object unless MojitoFilms documents a model override.
modulestringRequiredRequired. Use mojito so the request routes to the Moji assistant module.
prompt.params for text requests
ParameterTypeRequirementHow to use it
b2bbooleanRequiredRequired. Must be true for customer integrations. Enables credits-key auth and customer-safe tool behavior.
user_idstringOptionalOptional when session_id is present. Use your stable end-user id when your product has accounts.
session_idstringRequiredRequired when user_id is omitted. Creates an isolated conversation thread; use a new value to reset no-user bot memory.
actionstringOptionalOptional, defaults to assistant. B2B currently supports only assistant.
user_messagestringRequiredRequired for HTTP text requests. The customer's natural-language prompt.
user_languagestringOptionalOptional, defaults to en. Supported values include en, de, fr, and es. Used to steer response language.
user_detailsobjectOptionalOptional personalization context. Must be a JSON object if sent; null, arrays, and strings are rejected.
stream_protocolstringRequiredRequired for public B2B text UI integrations that use the documented SSE contract. Set sse-json-v1.
user_details personalization object
ParameterTypeRequirementHow to use it
namestringOptionalOptional. Lets Moji address the user naturally when appropriate.
languagestringOptionalOptional. Personalization hint for response language. Prefer user_language for the API-level language setting.
user_languagestringOptionalOptional inside user_details for personalization. Keep it aligned with params.user_language if you use it.
favourite_genresstring[]OptionalOptional. Supported spelling for favorite-genre preferences.
favorite_moviesstring[]OptionalOptional. Supported persona context for titles the user already loves.
favorite_tv_showsstring[]OptionalOptional. Supported persona context for series and shows the user already loves.
genderstringOptionalOptional. Use only when it is relevant to your product experience and consented.
agenumberOptionalOptional. Supported persona context. Send only when it is relevant and consented.
custom fieldsJSON valuesOptionalOptional. You can include customer-specific context, but keep it minimal, consented, and non-secret.

Example user_details

{
  "name": "Sheldon",
  "gender": "male",
  "age": 35,
  "favourite_genres": [
    "Comedy",
    "Fantasy",
    "Science Fiction"
  ],
  "favorite_movies": [
    "Harry Potter and the Sorcerer's Stone",
    "Lord of the Rings: The Fellowship of the Ring",
    "The Shawshank Redemption",
    "The Dark Knight",
    "Star Wars: A New Hope"
  ],
  "favorite_tv_shows": [
    "Star Trek: The Next Generation",
    "Game of Thrones",
    "The Big Bang Theory"
  ]
}

Treat this object as prompt context, not storage. Include only data your product is allowed to share with Moji and never include passwords, API keys, tokens, payment data, or private notes.

Text

Text API

Text requests use POST /api/v3/assistant/. B2B customer UIs should send stream_protocol: "sse-json-v1" for stable structured SSE output. Use this path when your product wants text, movie cards, trailer links, cinema options, or showtimes rather than playable speech.

  1. 1

    Send POST /api/v3/assistant/ with Content-Type: application/json and X-Mojito-Credits-Key.

  2. 2

    Body must include prompt.id: mojito_assistant, module: mojito, b2b: true, one of session_id or user_id, and user_message.

  3. 3

    Set stream_protocol: sse-json-v1, parse event: moji.event frames, and stop loading at lifecycle / response.completed.

TypeScript structured SSE requestts
const response = await fetch(`${baseUrl}/api/v3/assistant/`, {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Mojito-Credits-Key": process.env.MOJITO_CREDITS_KEY
  },
  body: JSON.stringify({
    prompt: {
      id: "mojito_assistant",
      params: {
        b2b: true,
        user_id: "customer-user-123",
        session_id: "customer-user-123-thread-a",
        action: "assistant",
        user_message: "Give me two movies similar to Arrival.",
        stream_protocol: "sse-json-v1"
      }
    },
    system: {},
    model: {},
    module: "mojito"
  })
});

Structured SSE

SSE JSON v1

Structured mode

Required for B2B text UIs that use this contract. Set stream_protocol to sse-json-v1.

One event name

Every frame is event: moji.event with one JSON data object. Route by type and subtype.

Clear lifecycle

Handle response.completed once, then close UI loading state.

sse-json-v1 returns one self-contained JSON object per SSE event. Each event can be parsed independently and rendered from its type, subtype, source, provider, and data fields. Unsupported protocol values are rejected before payment preflight or AI work begins.

SSE frame shapesse
event: moji.event
id: 3
data: {"protocol":"sse-json-v1","sequence":3,"turn_id":"turn_123","type":"entity","subtype":"movie","source":"moji","provider":"tmdb","collection":"movies","index":0,"data":{"item":{"id":152601,"title":"Her","year":2013},"display_text":"Her (2013): warm, thoughtful science fiction with a human center."}}

Event envelope
FieldTypePresenceMeaning
protocolstringAlways presentAlways present. Always sse-json-v1 for this SSE response.
sequencenumberAlways presentAlways present. Monotonic integer starting at 1 for each response.
turn_idstringAlways presentAlways present. Stable correlation id for all events in this request or turn. Do not treat it as a database message_id.
typestringAlways presentAlways present. High-level category: lifecycle, text, entity, audio, or error.
subtypestringAlways presentAlways present. Specific meaning such as assistant_message.delta, movie, trailer, or response.completed.
sourcestringAlways presentAlways present. Logical emitter: user, moji, tool, or system.
providerstringWhen applicableWhen applicable. External data provider for entity events, such as tmdb, youtube, movieglu, or mojito_internal.
collectionstringWhen applicableWhen applicable. Ordered entity collection such as movies, cinemas, showings, or trailers.
indexnumberWhen applicableWhen applicable. Zero-based position inside the collection.
dataobjectAlways presentAlways present. Renderable payload as a JSON object, never a stringified JSON blob.
text / transcriptsource: user

data.content

Render or store the user turn when the response includes transcript text.

text / assistant_message.deltasource: moji

data.content

Append data.content to the assistant message incrementally.

text / assistant_message.completedsource: moji

data.content

Optional final text block. Reconcile or replace accumulated deltas if present.

entity / moviesource: mojiprovider: tmdb

data.item, data.display_text

Render a movie card from data.item and use data.display_text as fallback narration.

entity / mentioned_moviesource: mojiprovider: tmdb

data.item

Render movies mentioned inside a movie-information answer.

entity / cinemasource: mojiprovider: mojito_internal

data.item, data.display_text

Render a cinema option with name, address, and display copy.

entity / showtimesource: mojiprovider: movieglu or mojito_internal

data.item, data.display_text

Render cinema, movie, and screening-time listings.

entity / trailersource: mojiprovider: youtube

data.item

Render or link to a trailer using video id, URL, title, and thumbnail.

lifecycle / response.completedsource: system

data.finish_reason

Stop loading and finalize the answer. finish_reason is complete or error.

error / <code>source: system

data.code, data.message, data.recoverable

Show an SSE-safe error after headers were sent, then wait for response.completed.

The happy path ends with exactly one response.completed event. The separate error-path example shows what Moji sends when a recoverable SSE error happens after headers are already open.

Text, entity, and lifecycle eventssse
event: moji.event
id: 1
data: {"protocol":"sse-json-v1","sequence":1,"turn_id":"turn_123","type":"text","subtype":"transcript","source":"user","data":{"content":"Recommend one warm science fiction movie for Friday night."}}

event: moji.event
id: 2
data: {"protocol":"sse-json-v1","sequence":2,"turn_id":"turn_123","type":"text","subtype":"assistant_message.delta","source":"moji","data":{"content":"If you want a softer Friday-night mood, start here."}}

event: moji.event
id: 3
data: {"protocol":"sse-json-v1","sequence":3,"turn_id":"turn_123","type":"text","subtype":"assistant_message.completed","source":"moji","data":{"content":"If you want a softer Friday-night mood, start here."}}

event: moji.event
id: 4
data: {"protocol":"sse-json-v1","sequence":4,"turn_id":"turn_123","type":"entity","subtype":"movie","source":"moji","provider":"tmdb","collection":"movies","index":0,"data":{"item":{"id":152601,"title":"Her","media_type":"movie"},"display_text":"Her (2013): warm, thoughtful science fiction with a human center."}}

event: moji.event
id: 5
data: {"protocol":"sse-json-v1","sequence":5,"turn_id":"turn_123","type":"entity","subtype":"mentioned_movie","source":"moji","provider":"tmdb","collection":"mentioned_movies","index":0,"data":{"item":{"id":329865,"title":"Arrival","media_type":"movie"}}}

event: moji.event
id: 6
data: {"protocol":"sse-json-v1","sequence":6,"turn_id":"turn_123","type":"entity","subtype":"cinema","source":"moji","provider":"mojito_internal","collection":"cinemas","index":0,"data":{"item":{"id":"cinema-6","name":"Cinema 6","address":"Example Street 1"},"display_text":"Cinema 6: a reliable nearby option."}}

event: moji.event
id: 7
data: {"protocol":"sse-json-v1","sequence":7,"turn_id":"turn_123","type":"entity","subtype":"showtime","source":"moji","provider":"movieglu","collection":"showings","index":0,"data":{"item":{"cinema_name":"Cinema 6","title":"Raiders of the Lost Ark","showtimes":[{"start_time":"18:00","end_time":"20:00"}]},"display_text":"Cinema 6: Raiders of the Lost Ark at 18:00."}}

event: moji.event
id: 8
data: {"protocol":"sse-json-v1","sequence":8,"turn_id":"turn_123","type":"entity","subtype":"trailer","source":"moji","provider":"youtube","collection":"trailers","index":0,"data":{"item":{"video_id":"abc123","title":"Her Official Trailer","url":"https://www.youtube.com/watch?v=abc123","thumbnail_url":"https://..."}}}

event: moji.event
id: 9
data: {"protocol":"sse-json-v1","sequence":9,"turn_id":"turn_123","type":"lifecycle","subtype":"response.completed","source":"system","data":{"finish_reason":"complete"}}
Post-header error pathsse
event: moji.event
id: 1
data: {"protocol":"sse-json-v1","sequence":1,"turn_id":"turn_456","type":"error","subtype":"upstream_unavailable","source":"system","data":{"code":"upstream_unavailable","message":"The upstream model failed.","recoverable":true,"details":{}}}

event: moji.event
id: 2
data: {"protocol":"sse-json-v1","sequence":2,"turn_id":"turn_456","type":"lifecycle","subtype":"response.completed","source":"system","data":{"finish_reason":"error"}}

Use an SSE parser that can read a POST response body. Browser EventSource only opens GET requests, so it cannot send the JSON request body shown in the quickstart.

Frontend routing examplets
import { createParser } from "eventsource-parser";

type MojiEvent = {
  type: string;
  subtype: string;
  data: Record<string, unknown>;
};

const reader = response.body?.getReader();
if (!reader) throw new Error("Missing SSE response body.");

let completed = false;
const decoder = new TextDecoder();
const parser = createParser({
  onEvent(message) {
    if (message.event !== "moji.event" || !message.data) return;
    completed = routeMojiEvent(JSON.parse(message.data)) || completed;
  }
});

while (!completed) {
  const { value, done } = await reader.read();
  if (done) break;
  parser.feed(decoder.decode(value));
}

await reader.cancel();

function routeMojiEvent(event: MojiEvent) {
  if (event.type === "error") {
    showSseError(event.data.message, {
      code: event.data.code,
      recoverable: event.data.recoverable
    });
    return false;
  }

  switch (`${event.type}:${event.subtype}`) {
    case "text:assistant_message.delta":
      appendAssistantText(event.data.content);
      break;
    case "entity:movie":
      renderMovieCard(event.data.item, event.data.display_text);
      break;
    case "entity:cinema":
      renderCinemaCard(event.data.item, event.data.display_text);
      break;
    case "entity:showtime":
      renderShowtimeList(event.data.item);
      break;
    case "entity:trailer":
      renderTrailer(event.data.item);
      break;
    case "lifecycle:response.completed":
      stopLoading(event.data.finish_reason);
      return true;
    default:
      logUnknownMojiEvent(event);
  }

  return false;
}
Unsupported stream_protocol errorjson
{
  "success": false,
  "error": {
    "code": "invalid_field",
    "message": "Unsupported stream_protocol.",
    "recoverable": false,
    "details": {
      "field": "stream_protocol",
      "supported": ["sse-json-v1"]
    }
  }
}

Audio

Voice API

Endpoint

Use /api/v3/assistant/ws/voice when your product needs Moji to answer with playable speech.

Protocol

Set stream_protocol to ws-json-v1 in the first JSON frame for the documented browser contract.

Input modes

Send either already-transcribed text or user audio bytes. Do not mix both in one turn.

A WebSocket session has three phases: send the init frame, send one user turn, then read events until response.completed. Text input means your frontend already has the user transcript; it does not mean text-only output. Audio input means you send user audio bytes and finish with {"type":"end_of_audio"}. In both cases, /ws/voice returns voice-capable output: text frames carry JSON events, and assistant speech arrives in binary audio frames.

  1. 1

    Open /api/v3/assistant/ws/voice and send the init JSON frame with b2b: true, payment_api_key, session_id or user_id, and stream_protocol: ws-json-v1.

  2. 2

    Optionally send {type:prewarm} before the user turn if you want the assistant state ready earlier.

  3. 3

    Send exactly one input mode: either {type:text,text:...} or binary audio bytes followed by {type:end_of_audio}.

  4. 4

    Read JSON and binary frames until lifecycle / response.completed. Unpack binary frames before playing assistant audio.

WebSocket voice init frame
ParameterTypeRequirementHow to use it
b2bbooleanRequiredRequired. Set true in the first JSON init frame.
stream_protocolstringRequiredRequired for the documented WebSocket contract. Set ws-json-v1 in the first JSON init frame.
payment_api_keystringRequiredRequired for browser clients. Same value as X-Mojito-Credits-Key, sent in the init frame because browser WebSockets cannot set custom headers.
session_id or user_idstringRequiredRequired. Defines the B2B memory scope together with organization and action.
user_languagestringOptionalOptional, defaults to en. Normalized by the server before response generation.
actionstringOptionalOptional, defaults to assistant. B2B supports assistant only.
user_detailsobjectOptionalOptional. Same object rules and personalization behavior as HTTP text.
WebSocket voice turn input
ParameterTypeRequirementHow to use it
type: prewarmstringOptionalOptional before turn input. Send {type:prewarm} to initialize assistant state before the user turn.
type: textstringText inputText input required. Send {type:text,text:...} after init or prewarm when your app already has the user transcript.
textstringText inputText input required. Contains the already-transcribed user turn.
binary audio framesbytesAudio inputAudio input required. Send raw bytes for one complete recorded audio file or Blob, split across one or more binary frames.
type: end_of_audiostringAudio inputAudio input required. Send {type:end_of_audio} after the final binary audio frame.

Treat audio input as one recorded user turn. The server concatenates every binary frame it receives before {"type":"end_of_audio"}, then transcribes the result as a single audio file. Send raw file or Blob bytes directly; do not base64-encode them and do not wrap them in JSON. Every binary frame for that turn should be a slice of the same recording, such as a browser-recorded Blob or a WAV file read as an ArrayBuffer.

Browser WebSocket requestts
const ws = new WebSocket("wss://ai-dev.mojitofilms.de/api/v3/assistant/ws/voice");
ws.binaryType = "arraybuffer";

ws.onopen = () => {
  ws.send(JSON.stringify({
    b2b: true,
    session_id: "website-voice-session-001",
    payment_api_key: mojitoCreditsKey,
    stream_protocol: "ws-json-v1",
    user_language: "en",
    action: "assistant",
    user_details: {}
  }));

  // Text input: client-side STT already produced the user turn.
  ws.send(JSON.stringify({
    type: "text",
    text: "Recommend one tense but fun movie."
  }));

  // Audio input instead:
  // ws.send(audioArrayBuffer);
  // ws.send(JSON.stringify({ type: "end_of_audio" }));
};

ws.onmessage = (event) => {
  if (event.data instanceof ArrayBuffer) {
    const { metadata, pcm } = unpackMojiAudioFrame(event.data);
    routeMojiVoiceEvent(metadata);
    playAssistantVoice(pcm, metadata.data);
    return;
  }

  routeMojiVoiceEvent(JSON.parse(event.data));
};

function routeMojiVoiceEvent(event) {
  if (event.type === "error") {
    showVoiceError(event.data.message, {
      code: event.data.code,
      recoverable: event.data.recoverable
    });
    return;
  }

  switch (`${event.type}:${event.subtype}`) {
    case "lifecycle:prewarm.completed":
      markVoiceSocketReady(event.data.duration_ms);
      break;
    case "text:transcript":
      renderUserTranscript(event.data.content);
      break;
    case "text:assistant_message.delta":
      appendAssistantCaption(event.data.content);
      break;
    case "audio:assistant_phrase":
      updateMojiExpression(event.data.picture_id);
      break;
    case "entity:movie":
      renderMovieCard(event.data.item, event.data.display_text);
      break;
    case "entity:cinema":
      renderCinemaCard(event.data.item, event.data.display_text);
      break;
    case "entity:showtime":
      renderShowtimeList(event.data.item);
      break;
    case "entity:trailer":
      renderTrailer(event.data.item);
      break;
    case "lifecycle:response.completed":
      markConversationComplete(event.data.finish_reason);
      break;
    default:
      logUnknownMojiVoiceEvent(event);
      break;
  }
}

function unpackMojiAudioFrame(buffer) {
  const view = new DataView(buffer);
  const metadataLength = view.getUint32(0, true);
  const metadataBytes = new Uint8Array(buffer, 4, metadataLength);
  const metadata = JSON.parse(new TextDecoder().decode(metadataBytes));
  const pcm = buffer.slice(4 + metadataLength);
  return { metadata, pcm };
}
WebSocket ws-json-v1 event envelope
FieldTypePresenceMeaning
protocolstringAlways presentAlways present. Always ws-json-v1 for the documented WebSocket contract.
sequencenumberAlways presentAlways present. Monotonic integer starting at 1 for each WebSocket turn.
turn_idstringAlways presentAlways present. Stable correlation id shared by all events in one turn.
typestringAlways presentAlways present. High-level category: lifecycle, text, entity, audio, or error.
subtypestringAlways presentAlways present. Specific meaning such as transcript, assistant_message.delta, assistant_phrase, or response.completed.
sourcestringAlways presentAlways present. Logical emitter: user, moji, or system.
providerstringWhen applicableWhen applicable. External data provider for entity events, such as tmdb, youtube, movieglu, or mojito_internal.
collectionstringWhen applicableWhen applicable. Ordered entity collection such as movies, cinemas, showings, or trailers.
indexnumberWhen applicableWhen applicable. Zero-based position inside the collection.
dataobjectAlways presentAlways present. Renderable payload as a JSON object, never a stringified JSON blob.

Text WebSocket messages are JSON envelopes. Binary assistant audio messages are packed as 4-byte uint32 little-endian metadata length header, then the UTF-8 JSON envelope, then raw pcm_s16le_22050 bytes. The audio payload is mono signed 16-bit little-endian PCM at 22,050 Hz. The audio envelope uses type: "audio", subtype: "assistant_phrase", and data.phrase, data.picture_id, and data.audio_format.

WebSocket ws-json-v1 event types

lifecycle / prewarm.completedsource: system

data.status, data.duration_ms

Only sent when the client sends type: prewarm before turn input. Wait for this before sending the user turn.

text / transcriptsource: user

data.content

Render or store the normalized user turn. Text input and server-side audio transcription both produce this event.

text / assistant_message.deltasource: moji

data.content

Supplemental assistant text. Voice clients may append it as captions, but playable speech arrives as audio / assistant_phrase.

audio / assistant_phrasesource: moji

data.phrase, data.picture_id, data.audio_format

Assistant voice payload. This event is the JSON metadata inside a binary frame; PCM bytes follow that metadata in the same frame.

entity / moviesource: mojiprovider: tmdb

data.item, data.display_text

Render a movie card from data.item and use data.display_text as fallback narration.

entity / mentioned_moviesource: mojiprovider: tmdb

data.item

Render movies mentioned inside a movie-information answer.

entity / cinemasource: mojiprovider: mojito_internal

data.item, data.display_text

Render a cinema option with name, address, and display copy.

entity / showtimesource: mojiprovider: movieglu or mojito_internal

data.item, data.display_text

Render cinema, movie, and screening-time listings.

entity / trailersource: mojiprovider: youtube

data.item

Render or link to a trailer using video id, URL, title, and thumbnail.

lifecycle / response.completedsource: system

data.finish_reason

Stop loading and finalize the turn. finish_reason is complete, error, or empty_transcript.

error / <code>source: system

data.code, data.message, data.recoverable

Show the error, then wait for lifecycle / response.completed if the socket remains open.

Billing behavior

Credits and usage

WorkflowUnitsPortal display
Text HTTP request1001 credit
WebSocket text turn1001 credit
WebSocket audio turn1501.5 credits

Validation and preflight failures do not charge. Successful AI work and disconnects after AI work starts are finalized with the applicable credit cost.

Failure modes

Errors and rate limits

Stable HTTP error envelopejson
{
  "success": false,
  "error": {
    "code": "missing_field",
    "message": "B2B requests require either user_id or session_id.",
    "recoverable": false,
    "details": {}
  }
}

400 missing_field

A required field is absent.

400 invalid_field

A field has the wrong type or value.

400 invalid_field stream_protocol

Unsupported stream_protocol values are rejected before payment preflight or AI work.

400 unsupported_action

B2B currently supports action=assistant.

401 invalid_api_key

The credits key is missing or invalid.

402 insufficient_credits

The organization cannot cover the request.

429 rate_limited

The key exceeded its request-per-minute limit.

429 overage_limit_exceeded

Billing overage policy blocked the request.

503 upstream_unavailable

Payment validation is temporarily unavailable.

Outbound events

Webhooks

Webhook endpoints are managed through the portal API and return a signing secret once. Store that secret and verify the live x-mojito-signature header on every delivery. Reject signatures outside the 5-minute replay tolerance. Delivery detail redacts historical signature values by design.

low_credit.threshold_25low_credit.threshold_10low_credit.threshold_0payment.succeededpayment.failedsubscription.canceledorg.suspended
Create a webhook endpointbash
curl "https://pay-dev.mojitofilms.com/v1/webhooks/endpoints" \
  -H "Authorization: Bearer $PORTAL_ACCESS_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/webhooks/mojito",
    "event_subscriptions": ["payment.succeeded", "org.suspended"],
    "description": "Production webhook receiver"
  }'
Verify x-mojito-signaturets
import crypto from "node:crypto";

export function verifyMojitoSignature({
  rawBody,
  header,
  signingSecret,
  toleranceSeconds = 300
}: {
  rawBody: Buffer;
  header: string;
  signingSecret: string;
  toleranceSeconds?: number;
}) {
  const parts = Object.fromEntries(
    header
      .split(",")
      .map((part) => part.split("=", 2))
      .filter(([key, value]) => key && value)
      .map(([key, value]) => [key.trim(), value.trim()])
  );

  const timestamp = Number(parts.t);
  const signature = parts.v1;

  if (
    !Number.isInteger(timestamp) ||
    typeof signature !== "string" ||
    !/^[a-f0-9]{64}$/i.test(signature)
  ) {
    return false;
  }

  const ageSeconds = Math.abs(Date.now() / 1000 - timestamp);
  if (ageSeconds > toleranceSeconds) {
    return false;
  }

  const signedPayload = Buffer.concat([
    Buffer.from(String(timestamp)),
    Buffer.from("."),
    rawBody
  ]);
  const expected = crypto
    .createHmac("sha256", signingSecret)
    .update(signedPayload)
    .digest("hex");

  const received = Buffer.from(signature, "hex");
  const computed = Buffer.from(expected, "hex");

  return (
    received.length === computed.length &&
    crypto.timingSafeEqual(received, computed)
  );
}

Billing records

Invoices

The public portal API lists issued invoices for the authenticated organization. Supported status filters are issued, paid, partially_refunded, refunded, and storno. PDF downloads redirect to a short-lived signed URL.

List paid invoicesbash
curl "https://pay-dev.mojitofilms.com/v1/invoices?status=paid" \
  -H "Authorization: Bearer $PORTAL_ACCESS_TOKEN"

Release notes

Changelog

2026-06-12: Documented the current B2B text, SSE JSON v1, customer memory, credits-key authentication, and WebSocket voice contract.

Public signup, generated OpenAPI reference pages, German docs, and long-term memory controls remain future docs work.