A little help. A clear record.
dyna.ai - Developer guide
Build a bot. Agree its boundaries. Follow the evidence.
Your organisation. Your operating rules. Build lending and servicing workflows with reviewed policies, controlled access and a record of the decisions behind the work.
Developer guide
Build against the tenant's own hostname and inspect the deployed contract. JSON method results are inside message. Binary audio endpoints return media directly.
Authentication and the API contract
Named Frappe API users send Authorization: token API_KEY:API_SECRET. Their keys carry their user permissions and do not bypass tenant access, capability checks or reviewed policies. Keep keys in server-side secret storage.
Authenticated browser sessions use the sid cookie. Session mutations also send X-Frappe-CSRF-Token from the authenticated page.
bash
curl 'https://passer.dynaai.us/api/method/dyna_control.openapi.spec' \
-H "Authorization: token $PASSER_API_KEY:$PASSER_API_SECRET"
The public voice experience uses a different credential: an opaque, short-lived session_token issued for an explicit requested experience. Its endpoints accept JSON POST requests without a Frappe API key. Keep this token in memory and send it only in the request body. Never put it in a URL, analytics event, screenshot, browser storage or log. It grants access to one visitor experience, not organisation records.
Public voice experience
The endpoint prefix is /api/method/dyna_control.public_demonstrations.. The supported personas are milo and casey for outgoing calls, and fern and clover for incoming calls. Availability is operational. Read capabilities before offering an experience. An enabled interface does not mean all destinations or jurisdictions are admitted.
The following JavaScript examples use values from your form and responses. They contain no telephone number or credential. Run each mutation only after the indicated visitor action.
javascript
const base = 'https://passer.dynaai.us/api/method/dyna_control.public_demonstrations.';
async function post(method, body = {}) {
const response = await fetch(base + method, {
method: 'POST', credentials: 'omit', cache: 'no-store',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(body)
});
const envelope = await response.json();
if (!response.ok) {
// Retain the operation ID and reconcile before any new mutation.
throw {status: response.status,
retryAfter: response.headers.get('Retry-After'),
message: envelope.public_error?.message || 'Request not confirmed'};
}
return envelope.message;
}
const capabilities = await post('capabilities');
1. Retain the visitor's contact request
Display capabilities.reason, the available voices and languages, and each persona's incoming_available. Show permission_notices.contact.text and record its version. The current flow does not request a verification call, SMS or OTP.
javascript
// The visitor accepts the displayed contact request notice.
const experienceRequestId = crypto.randomUUID();
const experience = await post('start_experience', {
phone: form.phone,
agent: form.agent,
phone_permission: {
granted: form.phone_permission,
version: capabilities.permission_notices.contact.version
},
request_id: experienceRequestId
});
const sessionToken = experience.session_token;
// Continue only when experience.status === 'Ready'.
The service retains an explicit requested-contact receipt. It checks provider-confirmed US eligibility for public numbers. This is a visitor declaration. It does not prove phone possession, customer identity or authority over an account. No verified timestamp is created.
Preserve the exact request body and identifier if the response is lost. Exact replay recovers the existing session without repeating a paid eligibility check. A provider result that remains unknown requires reconciliation. The retired start_verification and verify_phone endpoints return HTTP 410 and perform no verification request.
2. Retain separate call and recording choices
Display permission_notices.call and permission_notices.recording. The call requires explicit permission. Recording is a separate choice. Use only voice and language values returned by capabilities.
javascript
const launchRequestId = crypto.randomUUID();
const launched = await post('start_call', {
session_token: sessionToken,
request_id: launchRequestId,
call_permission: {
granted: form.call_permission,
version: capabilities.permission_notices.call.version
},
recording_permission: {
granted: form.recording_permission,
version: capabilities.permission_notices.recording.version
},
configuration: {
agent: form.agent,
visitor_name: form.visitor_name,
actual_state: form.actual_state,
actual_timezone: form.actual_timezone,
voice: form.voice,
language: form.language,
instructions: form.instructions,
business: form.business,
options: form.options,
scenario: form.scenario
}
});
The deployed OpenAPI definition describes persona-specific fields. actual_state and actual_timezone must be a supported pair returned by capabilities.state_timezones. The national fictional demonstration publication does not assert statutory coverage in every state. Do not infer a person's present location from their telephone area code. For an explicitly requested public fictional demonstration, scenario.local_time supplies the calling-hour policy input and defaults to 12:00. The service binds this value to the retained configuration, scenario audit, mission, bot and call. It records the actual local time and UTC time separately. Actual contact history, permission expiry, identity checks and budgets remain authoritative. Customer campaigns and calls outside this bound demonstration path use the actual clock.
A new session permits one call and expires 24 hours after creation. The contact request and phone eligibility evidence each remain valid for up to 24 hours from their original timestamps. The earliest deadline applies. Existing sessions retain their stored expiry. Genuine legacy possession evidence keeps its original meaning and expiry. Use the returned expires_at. A launch timeout is an uncertain result. Query status using the same token before deciding what happened. Do not create a fresh session to bypass a pending or uncertain dispatch.
3. Incoming number and one-use access code
For Fern or Clover, wait for status: "Awaiting incoming call". Display incoming_number and incoming_challenge only while returned in that state. The visitor calls from the supplied number and enters the eight-digit challenge using the telephone keypad. The service checks the session before accessing its scenario context.
Keep the challenge private and out of logs. Remove it after connection, cancellation or expiry. Retain expires_at from the experience response. Caller ID alone is insufficient. A lost launch response can be reconciled through status, which returns the current incoming details when still valid.
4. Follow retained events and review the result
javascript
const update = await post('status', {
session_token: sessionToken,
after: lastCursor
});
lastCursor = update.cursor;
// Deduplicate update.events by event.id before displaying them.
Poll at a reasonable interval, such as every three seconds. The endpoint returns up to 100 events per page. Continue from the returned cursor while a page is full, including after terminal becomes true, so the last page is not omitted. Stop after the terminal result and its remaining event pages are read.
| Field | Interpretation |
|---|---|
kind: transcript |
Recognised visitor speech or generated agent text. Check state and speaker. |
kind: speech |
Retained runtime event, including audio emission or interruption. |
kind: policy, scope: actual_call |
An actual runtime policy decision. A denied action is not proof that a violation occurred. |
kind: scenario_policy, scope: example_scenario |
An example policy result using the retained launch configuration. It never authorised the real call. |
scenario_assumptions, scenario_policy |
The example assumptions and retained version, source, schema and snapshot hashes. |
summary.findings |
Scoped evidence findings with requirement, reason, status and evidence_refs. |
Keep actual decisions and example scenarios in different panels. Render finding statuses as supplied: evidence, incomplete, review, denied, not evaluated, not recorded or example only. No single finding or carrier completion certifies all applicable laws, the words a person heard, a payment, an accepted claim or a connected human transfer.
5. Withdraw permission and request termination
javascript
const stopRequestId = crypto.randomUUID();
const stopped = await post('cancel', {
session_token: sessionToken,
request_id: stopRequestId
});
Cancellation withdraws permission and requests termination. Inspect stop_acknowledged, then reconcile through status. A pending request is not a confirmed carrier stop. Preserve its request identifier when recovering an uncertain response.
Requests, limits and uncertain outcomes
Use a stable operation identifier for the same payload. Never reuse it for a different payload. HTTP 403 indicates denied access. HTTP 417 commonly indicates Frappe validation failure. HTTP 429 indicates a rate limit. Honour Retry-After when present and use bounded backoff. Do not switch channels or identifiers to evade a limit.
The shared request limiter uses fixed 60-second windows. The default is 600 API requests per authenticated user and 120 per anonymous request IP. Credentials for the same user share a counter within the site. An administrator can configure an explicit service-user limit. Allow up to 60 seconds for site configuration changes to reach each web worker. Public endpoint limits and call budgets apply separately.
| Response header | Meaning |
|---|---|
X-Passer-RateLimit-Limit |
Allowed requests in this window |
X-Passer-RateLimit-Remaining |
Requests remaining in this window |
X-Passer-RateLimit-Reset |
Seconds until the next window |
Retry-After |
Delay before another request after a limit response |
Requests around a window boundary count in different windows. Use the returned delay rather than guessing the reset time. A limiter outage returns HTTP 503 before the operation executes. If a response is lost or its origin is uncertain, reconcile the operation before retrying a mutation.
Experience creation is limited to 50 requests per IP and 10 per number per hour. Public calls allow up to 24 actual endpoint attempts per rolling day, subject to the active policy and remaining allocation. Other endpoints have their own limits. Server budgets and destination eligibility apply independently. Read the deployed contract and capability response rather than assuming that public traffic has an allocation.
A timeout or HTTP 500 does not prove that a mutation failed before storage or provider dispatch. Retain the original identifiers, inspect the session or operation and ask an authorised operator to reconcile unresolved provider evidence. An unanswered call, missing recording or missing transcript does not authorise a repeat call.
Organisation API groups
| Group | Module | Responsibility |
|---|---|---|
| Bots and missions | dyna_control.passer_workspace |
Drafts, tested publication, mission pins and call admission |
| Conversation graphs | dyna_control.conversation_guards |
Metadata, validation, draft tests and scoped session evidence |
| Campaigns | dyna_control.passer_campaigns |
Preparation, preflight, release and target results |
| Policies | dyna_control.cedar_editor |
Cedar source, schema, evaluation and reviewed versions |
| Contact history | dyna_control.contact_ledger |
Authoritative contact and restriction evidence |
| Durable workflows | dyna_control.passer_workflows |
Templates, source events, state changes and evidence |
| Reports | dyna_control.passer_reports |
Persisted results and evidence exports |
| Call review | dyna_control.call_review |
Permission-checked recordings and transcript review |
Use domain action endpoints for controlled transitions. Generic Frappe record writes do not replace publication, admission or audit operations.
Draft graph tests
Read conversation_guards.builder_metadata for permitted tools, published knowledge, active conversation policies, voices, languages and models. Validate through validate_graph and save the bot before testing. Every graph variable needs an explicit protected boolean. A public prompt cannot use a protected variable to bypass disclosure controls.
javascript
// authenticatedPost uses your named session and CSRF token or server API user.
const session = await authenticatedPost('conversation_guards.create_test_session', {
bot: savedBotId, channel: 'chat', values: testVariables
});
const answer = await authenticatedPost('conversation_guards.test_chat', {
session: session.name, text: visitorText, operation_id: stableTurnId
});
Draft sessions pin the saved graph and expire after 15 minutes. Unsaved edits require another save and test session. Draft testing cannot dispatch PSTN calls or execute production business tools. A response includes text, node, generation, terminal and outcome. Retain the same operation_id when recovering one chat turn.
Browser audio uses create_test_session with channel: "browser", then mint_browser_grant(session, origin). The authorised page receives a WebSocket URL and a short-lived, one-use grant. Send the grant in the first JSON frame, never the URL. The workspace handles microphone capture and 8kHz mono G.711 μ-law transport. Browser tests require microphone access and an allowed origin.
Runtime service endpoints use a separate configured identity. A browser author cannot manufacture verified identity, consent, tool approval or protected-action grants.
Audio, evidence and integration checks
Use the call-review reference for authenticated range requests and transcript alignment. Read the conversation ontology for entities, actions, trusted facts and disclosure gates.
Before enabling an integration, test a permitted account and a denied account, wrong-tenant access, expired credentials, duplicate operations and lost responses. Bind provider callbacks to the expected tenant and operation, validate signatures and retain late events without reopening completed work. A business outcome requires its authorised source receipt, not only a transcript or carrier state.
Fixed opening notices
A graph can pin an approved organisation identity notice and fixed closure templates. The server retains one notice operation per session and bounds retransmission after confirmed interruption. Ordinary output requires a completed notice and current conversation authority. A model response cannot substitute for that declaration.
The dyna_control.conversation_opening methods are private runtime APIs. They reserve attempts, retain channel receipts, authorise ordinary or fixed speech, and enforce session, graph, generation, channel and time bindings. An exact duplicate does not grant another transmission. Uncertain delivery blocks further ordinary output. The runtime alone supplies channel observations. The user-facing session API retains server-authored notice transcript segments separately from model replies.
A sent receipt describes transport or response construction. It does not prove playback or human hearing. Draft session evidence is readable by its owner. Production call evidence follows the call and Mission permissions.
Launch feedback and recovery
A refused launch returns public_error with a stable code, a public message and optional remediation and clock. A calling-hours refusal includes the selected demo time and the evaluated window when available. Polling returns the retained explanation in launch_denial. Do not infer a calling-hours refusal from a generic policy denial.
Keep the same request ID when recovering a request. If dispatch is unconfirmed, poll the existing session or request cancellation. Do not submit a new call until the existing request has an authoritative outcome. A timeout does not prove that no call was placed. The public interface retains minimal recovery state in the current browser tab and restores status after a reload.
Share a completed demonstration
After a call ends, preview and save a read-only experience for colleagues. Choose whether to include the transcript and recording. Anyone with the link can view the selected content without signing in. Links expire within 30 days and can be revoked from the original experience.