API Reference
Every endpoint documented with request and response schemas.
Base URL
All API requests are made to https://engine.debatetalk.ai. The debate streaming endpoint lives at the root path with no versioning prefix. All account management endpoints use the /v1/ prefix.
https://engine.debatetalk.ai/debate # Debate streaming
https://engine.debatetalk.ai/v1/user/profile # Management endpoints/debate endpoint intentionally has no version prefix. Its response shape is the SSE event stream, which is versioned implicitly by the type field on each event. All other endpoints are under /v1/.Authentication
DebateTalk supports two authentication methods depending on your integration context. Both are passed as a standard HTTP Authorization bearer header.
Session Token (Supabase)
When building on top of the DebateTalk dashboard or integrating server-side with Supabase Auth, pass your Supabase session JWT directly. The token is issued after the user signs in via the dashboard or via the Supabase auth client library.
Authorization: Bearer YOUR_SUPABASE_JWTSession tokens are short-lived and automatically refreshed by the Supabase client. All management endpoints (/v1/user/*, /v1/models/*) require a session token.
API Key
For programmatic, server-to-server integrations you can use a DebateTalk API key. API keys are created in the dashboard under Settings. Pass the key as the bearer token:
Authorization: Bearer dt_YOUR_API_KEYAPI keys are scoped to specific operations and can carry an optional expiration date. The only operations an API key authorises are running debates (POST /debate,GET /debate) and requesting model recommendations (POST /v1/models/recommend). Management endpoints always require a session token and will reject API key auth with 403 Forbidden.
Free accounts cannot create API keys. Pro accounts can hold up to 2 active keys. Enterprise accounts have no key limit.
dt_ so they are easy to identify and can be detected by secret-scanning tools in your CI pipeline.Unauthenticated Requests
The debate endpoint accepts requests with no Authorization header at all. Unauthenticated debates are rate-limited at the IP level. They are not saved to debate history and no token usage is attributed to any account.
Rate Limits
Rate limit violations always return HTTP 429 with a standard error body. Theerror.code is rate_limited.
Unauthenticated requests are limited to 3 debates per hour per IP address. Free accounts are limited to 5 debates per day total. The dashboard shows a CAPTCHA prompt after 3 debates in a single day on the Free plan. Pro and Enterprise accounts have no debate rate limit.
The POST /v1/auth/check-provider endpoint is separately rate-limited to 10 requests per minute per IP address, regardless of authentication status.
Error Format
All non-streaming endpoints return errors as JSON with HTTP 4xx or 5xx status codes. Every error body has the same shape:
{
"error": {
"code": "error_code_string",
"message": "Human-readable description",
"details": {}
}
}The details object is optional and may carry field-level validation errors or additional context. It is always an object, never null.
Common error codes:
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
unauthorized | string | No | No valid credential was provided or the credential has expired. |
forbidden | string | No | The credential is valid but does not have permission for this operation. API keys will receive this on management endpoints. |
not_found | string | No | The requested resource does not exist or is not accessible to the caller. |
rate_limited | string | No | The caller has exceeded their allowed request rate. See the Rate Limits section. |
validation_error | string | No | The request body or query parameters failed schema validation. Check details for field-level messages. |
tier_limit_exceeded | string | No | The operation requires a higher plan tier than the account currently has. |
insufficient_credits | string | No | The Pro account does not have enough credit balance to run the requested debate. |
debate_not_found | string | No | No debate with the given ID exists for this account. |
key_not_found | string | No | No API key with the given ID exists for this account. |
errorSSE event rather than an HTTP error status. See the SSE Event Reference section for the event shape.Debate Streaming
The debate endpoint is the core of the DebateTalk API. It streams a full multi-model debate as a sequence of Server-Sent Events (SSE). The connection stays open until the debate completes or an error occurs. The final event is always of type final.
Two HTTP methods are available depending on question length. Both return the same SSE stream.
https://engine.debatetalk.ai/debateAuth optionalStream a debate for questions up to 2,000 characters. Pass the question as a URL query parameter.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
question | string | Yes | The debate question. Maximum 2,000 characters. |
https://engine.debatetalk.ai/debateAuth optionalStream a debate for longer questions up to 100,000 characters. Pass the question in the JSON body.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
question | string | Yes | The debate question. Must be between 1 and 100,000 characters. |
{ "question": "string (1 to 100,000 characters)" }The response Content-Type is text/event-stream. Each event is a UTF-8 line in the format data: {...}\n\n. The following example shows how to consume the stream in a browser or Node.js environment:
const response = await fetch("https://engine.debatetalk.ai/debate", {
method: "POST",
headers: {
"Content-Type": "application/json",
"Authorization": "Bearer YOUR_TOKEN"
},
body: JSON.stringify({ question: "Should remote work be the default for knowledge workers?" })
})
const reader = response.body.getReader()
const decoder = new TextDecoder()
while (true) {
const { done, value } = await reader.read()
if (done) break
const text = decoder.decode(value)
// Each SSE line is: "data: {...}\n\n"
for (const line of text.split("\n")) {
if (line.startsWith("data: ")) {
const event = JSON.parse(line.slice(6))
console.log(event.type, event.data)
}
}
}Authorization header is omitted, the debate runs without attribution. Authenticated debates are saved to history and billed against the account's credit balance (Pro) or counted toward the daily limit (Free).SSE Event Reference
Every SSE message is a JSON object with a type field and an optionaldata object. Events arrive in a deterministic order within a debate. The sequence is: debate_created => classification => debate_start => one or more round groups (round_start,model_response per model, consensus_check, consensus) => synthesis_start => synthesis => accuracy => final.
debate_created
The first event emitted. Contains the server-assigned debate ID and the generated title derived from the question. Store the debate_id if you need to retrieve the full record from the history API after the stream closes.
{
"type": "debate_created",
"data": {
"debate_id": "uuid",
"title": "Should remote work be the default for knowledge workers?"
}
}classification
Emitted after the question has been classified. The question_type is one of the DebateTalk question domains (Normative, Factual, Predictive, Business, etc.). The termination_trigger determines the consensus condition that, when met, ends the debate early. See the How Debates Work page for the full list of triggers.
{
"type": "classification",
"data": {
"question_type": "Normative",
"termination_trigger": "Value Alignment"
}
}debate_start
Emitted once before the first round. Lists the model display names selected for this debate and the maximum number of rounds the debate can run before synthesis is forced.
{
"type": "debate_start",
"data": {
"models": ["claude-opus-4-6", "gpt-5-4", "gemini-3-1-pro"],
"max_rounds": 4
}
}round_start
Emitted at the beginning of each round. The phase is "blind" for round 1, in which models respond without seeing each other's answers. All subsequent rounds have phase "deliberation".
{
"type": "round_start",
"data": {
"round": 1,
"phase": "blind"
}
}model_response
Emitted once per model that completes a round. The structure of thedata payload differs between the blind round and deliberation rounds.
Round 1 (blind phase) response shape:
{
"type": "model_response",
"data": {
"round": 1,
"name": "Model A",
"answer": "Remote work should be the default for knowledge workers because...",
"key_claims": [
{ "claim": "Productivity increases for focused work", "confidence": 0.85 },
{ "claim": "Collaboration suffers for complex team projects", "confidence": 0.72 }
],
"confidence": 0.78
}
}Deliberation round response shape (round 2 and beyond):
{
"type": "model_response",
"data": {
"round": 2,
"name": "Model A",
"answer": "I maintain that remote work should be the default, though I now weight...",
"key_claims": [
{ "claim": "Productivity increases for focused work", "confidence": 0.88, "changed": false },
{ "claim": "Hybrid models outperform pure remote for mentorship", "confidence": 0.80, "changed": true, "reason": "Model B raised compelling evidence about junior developer outcomes" }
],
"confidence": 0.82,
"what_i_learned": "Model B's point about onboarding and mentorship is well-supported",
"what_i_corrected": "I understated the cost of remote work for early-career employees",
"still_disagree_on": "Whether in-office mandates are necessary for culture"
}
}In deliberation rounds, each entry in key_claims gains achanged boolean. When changed is true, areason string is also present explaining what prompted the update. The top-level fields what_i_learned, what_i_corrected, andstill_disagree_on are present only in deliberation rounds.
model_skipped
Emitted when a model is dropped from a round because its latency is consistently slower than the other models in the panel. The debate continues without that model for the remainder of the round. If the model recovers in a later round it may be reintroduced.
{
"type": "model_skipped",
"data": {
"name": "Model C",
"reason": "latency"
}
}consensus_check
Emitted when the adjudicator begins evaluating whether consensus has been reached after a round. This event carries no data payload.
{ "type": "consensus_check" }consensus
Emitted after each round with the result of the consensus evaluation.reached is true when the termination trigger fires and the debate ends early. The scores object contains the four sub-scores (0 to 25 each) used to compute overall consensus. A total above roughly 70 typically triggers termination depending on the question type.
{
"type": "consensus",
"data": {
"round": 1,
"reached": false,
"scores": {
"stance_alignment": 18,
"empirical_overlap": 15,
"framework_agreement": 14,
"confidence_convergence": 16
}
}
}synthesis_start
Emitted when the synthesizer model begins producing the final answer. No data payload is present.
{ "type": "synthesis_start" }synthesis
Contains the synthesized final answer produced by the synthesizer model. Thesynthesis string is a prose summary of what the panel collectively concluded, including areas of remaining disagreement.
{
"type": "synthesis",
"data": {
"synthesis": "The panel reached consensus that remote work should be the default for knowledge workers performing focused individual tasks, with structured in-person time reserved for onboarding, mentorship, and complex collaborative work. Pure remote-first policies were found to disadvantage early-career employees disproportionately."
}
}accuracy
Contains accuracy scores for each debater as evaluated by the adjudicator.factual_accuracy measures claim correctness. logical_consistency measures internal argument coherence. evidence_quality measures how well claims were supported. trajectory_score measures how productively the model updated its position across rounds.
{
"type": "accuracy",
"data": {
"scores": [
{
"name": "Model A",
"factual_accuracy": 82,
"logical_consistency": 88,
"evidence_quality": 79,
"trajectory_score": 85
},
{
"name": "Model B",
"factual_accuracy": 91,
"logical_consistency": 84,
"evidence_quality": 90,
"trajectory_score": 78
}
]
}
}final
The last event in every debate, always present regardless of how the debate ended. Contains a complete summary including the full token usage breakdown and cost. Once you receive this event the stream will close.
{
"type": "final",
"data": {
"debate_id": "uuid",
"question": "Should remote work be the default for knowledge workers?",
"rounds_completed": 2,
"max_rounds": 4,
"consensus_reached": true,
"synthesis": "The panel reached consensus that...",
"models": ["Model A", "Model B", "Model C"],
"token_usage": {
"Model A": { "prompt_tokens": 2400, "completion_tokens": 800 },
"Model B": { "prompt_tokens": 2350, "completion_tokens": 920 }
},
"cost_usd": 0.042
}
}error
Emitted when a fatal error occurs during the stream. After this event the connection closes. The code field matches the standard error codes documented in the Error Format section.
{
"type": "error",
"data": {
"code": "rate_limited",
"message": "You have reached your daily debate limit."
}
}User Profile
/v1/user/profileAuth requiredRetrieve the authenticated user's profile.
{
"id": "uuid",
"email": "user@example.com",
"display_name": "Alice",
"tier": "pro",
"created_at": "2026-01-15T10:00:00Z",
"updated_at": "2026-03-20T14:30:00Z"
}/v1/user/profileAuth requiredUpdate the user's display name.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
display_name | string | Yes | The new display name for the account. |
The response has the same shape as the GET /v1/user/profile response, reflecting the updated values.
Model Configuration
/v1/user/model-configAuth requiredGet the user's current model configuration and the full list of available models.
The response contains three top-level keys. config is the user's saved debate configuration. tier_limits describes what the account's plan permits. available_models is the full catalogue with pricing and accessibility for this account.
{
"config": {
"auto_mode": true,
"num_debaters": 3,
"debaters": ["model-id-1", "model-id-2", "model-id-3"],
"adjudicator": "model-id",
"synthesizer": "model-id",
"max_rounds": 4
},
"tier_limits": {
"max_debaters": 5,
"max_rounds": 4,
"adjudicator_is_algorithmic_only": false
},
"available_models": [
{
"id": "model-id",
"display_name": "Claude Opus",
"provider": "Anthropic",
"context_window": 200000,
"tier_required": "free",
"is_accessible": true,
"cost_per_million_input_tokens": 15.0,
"cost_per_million_output_tokens": 75.0
}
]
}The is_accessible field on each model is true when the model is available to the caller's current plan tier. Models withtier_required: "pro" will have is_accessible: false for Free accounts.
/v1/user/model-configAuth requiredSave a new model configuration. All fields are required when not using auto mode.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
auto_mode | boolean | Yes | When true, the smart router selects models automatically and the other fields are ignored. |
num_debaters | integer | Yes | Number of debater models. Must be between 2 and the plan's max_debaters limit. |
debaters | string[] | Yes | Array of model IDs to use as debaters. Length must match num_debaters. |
adjudicator | string | Yes | Model ID to use as the adjudicator. |
synthesizer | string | Yes | Model ID to use as the synthesizer. |
max_rounds | integer | Yes | Maximum number of debate rounds before synthesis is forced. Must not exceed the plan's max_rounds limit. |
{
"auto_mode": false,
"num_debaters": 3,
"debaters": ["model-id-1", "model-id-2", "model-id-3"],
"adjudicator": "model-id",
"synthesizer": "model-id",
"max_rounds": 3
}The response returns the saved configuration in the same shape as theconfig field from GET /v1/user/model-config.
Cost Estimation
Use this endpoint to preview the expected cost of a debate before running it. This is useful for Pro accounts that want to verify they have sufficient credits before initiating a long debate with expensive models.
/v1/user/estimate-costAuth requiredEstimate the cost of a debate with a given configuration before running it.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
auto_mode | boolean | Yes | When true, the estimate is based on the smart router's typical model selection for an average question. |
debaters | string[] | No | Model IDs for the debaters. Required when auto_mode is false. |
adjudicator | string | No | Model ID for the adjudicator. Required when auto_mode is false. |
synthesizer | string | No | Model ID for the synthesizer. Required when auto_mode is false. |
rounds | integer | Yes | Number of rounds to estimate for. |
{
"auto_mode": false,
"debaters": ["model-id-1", "model-id-2"],
"adjudicator": "model-id",
"synthesizer": "model-id",
"rounds": 3
}Response:
{
"estimated_cost_usd": 0.085,
"breakdown": {
"debaters": 0.062,
"adjudicator": 0.012,
"synthesizer": 0.011
},
"monthly_projection": {
"debates_per_month": 100,
"estimated_monthly_cost_usd": 8.50
}
}Debate History
/v1/user/historyAuth requiredList past debates with optional filters. Returns a paginated list in reverse chronological order.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
page | integer | No | Page number. Defaults to 1. |
limit | integer | No | Results per page. Defaults to 20, maximum 100. |
from | ISO datetime | No | Return only debates created at or after this timestamp. |
to | ISO datetime | No | Return only debates created at or before this timestamp. |
status | string | No | Filter by debate status. One of: completed, failed, aborted. |
search | string | No | Full-text search across the debate question content. |
{
"debates": [
{
"id": "uuid",
"title": "Should remote work be the default?",
"question": "Should remote work be the default for knowledge workers?",
"status": "completed",
"rounds_completed": 2,
"consensus_reached": true,
"models_used": ["Model A", "Model B", "Model C"],
"cost_usd": 0.042,
"created_at": "2026-03-20T10:00:00Z"
}
],
"pagination": {
"page": 1,
"limit": 20,
"total": 147,
"pages": 8
}
}/v1/user/history/:debate_idAuth requiredGet the full record for a single debate, including the complete SSE event transcript.
The response extends the list item shape with four additional fields:events (the full ordered array of SSE event objects as they were streamed), synthesis (the final synthesized answer as a string),accuracy_scores (the accuracy evaluation array), andtoken_usage (the per-model token breakdown).
{
"id": "uuid",
"title": "Should remote work be the default?",
"question": "Should remote work be the default for knowledge workers?",
"status": "completed",
"rounds_completed": 2,
"consensus_reached": true,
"models_used": ["Model A", "Model B", "Model C"],
"cost_usd": 0.042,
"created_at": "2026-03-20T10:00:00Z",
"events": [...],
"synthesis": "The panel concluded that...",
"accuracy_scores": [...],
"token_usage": {...}
}Usage Statistics
/v1/user/usage?period=:periodAuth requiredGet aggregated usage statistics for a given time period.
Valid values for period: current_month, last_month, last_7d, last_30d, all_time.
{
"period": "current_month",
"debates_run": 34,
"debates_today": 2,
"total_tokens": 1840000,
"total_cost_usd": 3.42,
"avg_cost_per_debate_usd": 0.101,
"avg_rounds": 2.1,
"consensus_rate": 0.82,
"credit_balance_usd": 22.50
}The credit_balance_usd field is only present in responses for Pro tier accounts.
/v1/user/usage/detailed?period=:periodAuth requiredGet a day-by-day usage breakdown for a given time period.
Valid values for period: today, week, month, lastMonth.
{
"period": "month",
"daily": [
{
"date": "2026-03-01",
"debates": 3,
"tokens_in": 48000,
"tokens_out": 12000,
"cost_usd": 0.31
}
]
}Billing
/v1/user/billingAuth requiredGet current billing information and available upgrade options.
The response shape varies by plan tier. Free accounts receive an upgrade URL. Pro accounts receive credit balance and auto-refill settings. Enterprise accounts receive a support contact.
Free tier response:
{
"tier": "free",
"upgrade_url": "https://app.debatetalk.ai/billing/upgrade"
}Pro tier response:
{
"tier": "pro",
"credit_balance_usd": 22.50,
"auto_refill_enabled": true,
"auto_refill_amount_usd": 10.00,
"auto_refill_threshold_usd": 5.00
}Enterprise tier response:
{
"tier": "enterprise",
"support_contact": "enterprise@debatetalk.ai"
}Credits
Credit endpoints are available exclusively to Pro tier accounts. Requests from Free or Enterprise accounts will receive 403 Forbidden with codetier_limit_exceeded.
/v1/user/creditsAuth requiredGet the current credit balance and auto-refill configuration.
{
"balance_usd": 22.50,
"auto_refill_enabled": true,
"auto_refill_amount_usd": 10.00,
"auto_refill_threshold_usd": 5.00
}/v1/user/credits/transactions?limit=50&offset=0Auth requiredGet paginated credit transaction history.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
limit | integer | No | Number of transactions to return. Defaults to 50. |
offset | integer | No | Number of transactions to skip for pagination. Defaults to 0. |
{
"transactions": [
{
"id": "uuid",
"type": "debit",
"amount_usd": -0.042,
"balance_after_usd": 22.458,
"description": "Debate: Should remote work be the default?",
"debate_id": "uuid",
"created_at": "2026-03-20T10:00:00Z"
}
],
"total": 147
}The type field is one of three values. debit represents a debate cost charge. credit represents a refund or manual adjustment.topup represents credits purchased via checkout.
/v1/user/credits/checkoutAuth requiredCreate a Paddle checkout transaction to purchase credits.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in USD to purchase. Minimum $5.00, maximum $500.00. |
{ "amount": 25.00 }Response:
{ "transaction_id": "txn_..." }Pass the returned transaction_id to the Paddle.js overlay to complete the payment. Credits are added to the account balance as soon as Paddle confirms the transaction.
/v1/user/credits/auto-refillAuth requiredEnable or disable auto-refill and configure its threshold and amount.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
enabled | boolean | Yes | Whether auto-refill should be active. |
amount | number | Yes | USD amount to purchase each time auto-refill triggers. |
threshold | number | Yes | USD balance level at which auto-refill activates. |
{
"enabled": true,
"amount": 10.00,
"threshold": 5.00
}When enabled, the system will automatically initiate a Paddle purchase of amount USD using the account's saved payment method whenever the credit balance drops below threshold USD. The response has the same shape as GET /v1/user/credits.
API Keys
API key management is available to Pro and Enterprise accounts. Free accounts will receive 403 Forbidden with code tier_limit_exceeded on all key endpoints.
/v1/user/api-keysAuth requiredList all API keys for the account.
{
"keys": [
{
"id": "uuid",
"name": "Production server",
"prefix": "dt_live_abc123",
"scopes": ["debate:run"],
"is_active": true,
"last_used_at": "2026-03-20T09:00:00Z",
"expires_at": null,
"created_at": "2026-01-10T00:00:00Z"
}
]
}The full key value is never returned by this endpoint. Only the prefix is exposed so you can identify which key is which.
/v1/user/api-keysAuth requiredCreate a new API key. The full key value is only returned in this response and cannot be retrieved again.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
name | string | Yes | A human-readable label for this key, e.g. "Production server". |
scopes | string[] | Yes | Array of permission scopes. Currently only debate:run is supported. |
expires_at | ISO datetime | No | Optional expiration timestamp. Omit for a key with no expiry. |
{
"name": "Production server",
"scopes": ["debate:run"],
"expires_at": "2027-01-01T00:00:00Z"
}Response:
{
"id": "uuid",
"name": "Production server",
"key": "dt_live_abc123def456...",
"prefix": "dt_live_abc123",
"scopes": ["debate:run"],
"created_at": "2026-03-20T00:00:00Z"
}key value immediately. It will not be shown again./v1/user/api-keys/:key_idAuth requiredRevoke an API key immediately. Any in-flight requests using this key will be rejected. Returns 204 No Content.
/v1/user/api-keys/:key_idAuth requiredUpdate a key's name, active status, or expiration date. All fields are optional.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
name | string | No | New display name for the key. |
is_active | boolean | No | Set to false to temporarily disable the key without revoking it. |
expires_at | ISO datetime | No | New expiration timestamp. Set to null to remove the expiration. |
{
"name": "Updated name",
"is_active": false,
"expires_at": "2027-06-01T00:00:00Z"
}Model Recommendation
This endpoint exposes the same model selection logic that Auto mode uses internally. Call it before starting a debate if you want to preview which models would be chosen for a given question, or to use the recommendation as a starting point for a manual configuration.
/v1/models/recommendAuth requiredAsk the API to recommend an optimal model panel for a given question.
Parameters
| Name | Type | Required | Description |
|---|---|---|---|
question | string | Yes | The debate question to analyse. |
num_debaters | integer | Yes | How many debater models to include in the recommendation. |
{
"question": "Will AI reach human-level reasoning by 2030?",
"num_debaters": 3
}Response:
{
"question_type": "Prediction",
"debaters": ["model-id-1", "model-id-2", "model-id-3"],
"synthesizer": "model-id",
"adjudicator": "model-id"
}The question_type field is the classification the router assigned to the question. The returned model IDs can be passed directly toPUT /v1/user/model-config or to the debate endpoint after converting to the required request shape.
Public Endpoints
These endpoints require no authentication and are safe to call from client-side code.
/v1/public/model-statusPublicGet health and latency data for all active models.
Updated approximately every 5 minutes. The checks_24h field shows how many health checks were run in the past 24 hours. uptime_24h is the fraction of checks that returned a healthy response (1.0 = 100%).
{
"models": [
{
"id": "model-id",
"display_name": "Claude Opus",
"provider": "Anthropic",
"is_healthy": true,
"latency_ms": 1240,
"uptime_24h": 0.998,
"avg_latency_24h_ms": 1180,
"checks_24h": 288
}
],
"updated_at": "2026-03-29T12:00:00Z"
}/healthPublicHealth check for the API server itself. Returns immediately with no database or model calls.
{ "status": "ok" }