Concepts

Environments

Test and live keys, and what sandbox mode changes.

Every app has two publishable keys:

One app holds two publishable keys. The test key opens sandbox conversations and reads the draft config; the live key is billed and reads the published config.One appactions, knowledge, persona, retentionrd_pk_test_…Sandbox conversationsnever billed, capped per monthReads the draft configyour build is the preview deviceMarked “sandbox” in the consolerd_pk_live_…Billable conversationsone thread, any number of messagesReads the published configwhat your users are looking atCounts against the monthly cap
Two keys, one app, one set of actions and knowledge. The environment decides whether a conversation is billed and which config channel the device reads, so your own build on a test key is the preview device.
  • rd_pk_test_... puts the whole pipeline into sandbox: conversations are flagged as test and never billed. Sandbox has its own monthly conversation cap, separate from your plan's, and the same per-device and daily message limits. Model behavior matches live except for a one-line sandbox notice in the system prompt.
  • rd_pk_live_... serves production traffic and counts toward your plan.

Sandbox specifics

  • Conversations show a Sandbox badge in the console and a subtle "Test mode" banner in the thread.
  • Handlers still execute for real. To short-circuit one with canned data in test builds, pass sandboxResult on the action.
  • Sandbox conversations follow the app's own retention window, 365 days by default and changeable per app under Settings → Data retention. A nightly job deletes conversations past it, along with the encrypted values behind any redacted placeholders in them.

Billing definition

A conversation becomes billable at its first user message. Everything after, in that thread, is free: follow-ups, actions, confirmations. A thread is closed 24 hours after its last message, so the next thing the user says opens a new one and is billable again. The SDK does not need to do anything: it keeps sending the same conversation_id, the server declines to reuse a closed thread, and turn.start returns the new one.