> For clean Markdown of any page, append .md to the page URL.
> For a complete documentation index, see https://docs.v2.rasen.ai/llms.txt.
> For AI client integration (Claude Code, Cursor, etc.), connect to the MCP server at https://docs.v2.rasen.ai/_mcp/server.

# Validate a draft change

POST https://api.v2.rasen.ai/v1/agents/{agent_id}/draft/validate
Content-Type: application/json

Validates the submitted patch over the current draft without saving or making external calls. If-Match fixes which draft is checked. It does not guarantee a call will be admitted. Retained retrieval_snapshot_id and fallback_source_ids values are checked only for workspace, agent-ownership and source-state eligibility; publication validation derives knowledge from the direct agent library.

Reference: https://docs.v2.rasen.ai/api-reference/agents/draft/validate

## Authentication

- `Authorization` header (bearer token, required) — Create and manage workspace-owned keys through the existing app session routes, outside this public API. Creator and manager identities are audit actors only. Key validity follows workspace policy, expiry and explicit revocation, independently of creator membership. Do not include a workspace-selection header.

## Request

### Path parameters

- `agent_id` (string, required) — Resource identity within the authenticated workspace.

### Headers

- `If-Match` (string, required) — Strong ETag from the resource being changed. Draft/tool/variable/publication operations use the complete agent draft ETag; number operations use the number ETag; MCP update uses its own ETag. Never retry a stale edit by blindly using a fresh ETag. Agent identity edits use the identity ETag; knowledge-source reingest uses the source ETag. A stale revision returns 409: draft_conflict for agent, draft, variable, tool, MCP server and publication-token checks; conflict with details.reason=stale_revision for phone numbers and knowledge sources. A missing header returns 428 revision_required. Same-intent replay uses the original If-Match and returns its receipt before checking current revision.

### Body (application/json)

This endpoint expects a DraftPatch.

- `prompt` (string, optional)
- `voice` (VoiceSelection, optional)
- `settings` (SettingsPatch, optional) — Partial merge of these named settings. Absent fields keep their values. Null resets only nullable fields to platform defaults/unset. Arrays replace their field; extraction_fields=[] removes definitions. No serving-model, checkpoint, temperature or campaign retry knobs. Draft excludes inactive noise-suppression controls.
- `retrieval_snapshot_id` (string, optional, nullable) — Retained compatibility field. If set, the snapshot must belong to this agent. Publication ignores this field and builds or reuses an immutable snapshot from the direct agent library.
- `fallback_source_ids` (list of string, optional) — Retained compatibility field. Values must identify current fallback sources eligible for this agent: agent-owned sources or linked workspace sources. Publication ignores this field and freezes fallback context from the direct agent library. A write replaces the entire compatibility list.
- `default_tools` (DefaultToolsPatch, optional) — Per-key merge: each policy you include merges field by field; policies you omit stay unchanged. New built-in policies may be added later; older clients that omit them keep them unchanged. end_call is enabled by default for every agent; only an explicit end_call.enabled=false turns it off. transfer starts disabled with a null destination.

## Response

### 200

Validate a draft change.

- `valid` (boolean, required)
- `errors` (list of ValidationIssue, required)
- `warnings` (list of ValidationIssue, required)
- `readiness` (Readiness, required) — A snapshot, not a reservation. Admission rechecks current readiness. Publication, model support, knowledge and phone routing are separate prerequisites. Reasons can include credential_unavailable when the live version has tools that pin a revoked credential version.

## Errors

### 400 Bad Request Error

Invalid input or cursor. Correct the request.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 401 Unauthorized Error

Invalid, expired or revoked API key. Creator departure or membership changes do not invalidate a workspace key.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 403 Forbidden Error

Operation not allowed by current credential or workspace access.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 404 Not Found Error

Resource absent or not visible in this workspace.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 409 Conflict Error

Conflict. Not retryable: read the current state, fix the request and send a new request. The one exception is idempotency_in_progress, which carries Retry-After: wait, then resend the identical request with the same Idempotency-Key. The operation lists its codes in x-error-codes; branch on error.code and, for conflict, error.details.reason.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 413 Content Too Large Error

Body or upload exceeds its allowed limit.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 428 Precondition Error

Required If-Match revision is absent.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 429 Too Many Requests Error

Rate limited. Respect Retry-After and retry the same intent where permitted.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 500 Internal Server Error

Internal error. The effect may have happened. Retry the same request with the same Idempotency-Key; read the resource before creating a new intent. For an operation that takes an Idempotency-Key, a client retry with the same key is safe: the server replays the stored outcome if the first attempt took effect; if the first attempt had no effect, the retry performs the operation once. Never retry with a new key.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

### 503 Service Unavailable Error

Service unavailable. Respect Retry-After where present. For an operation that takes an Idempotency-Key, a client retry with the same key is safe: the server replays the stored outcome if the first attempt took effect; if the first attempt had no effect, the retry performs the operation once. Never retry with a new key.

- `error` (ErrorResponseError, required) — Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

## Types

### VoiceSelection

- `voice_id` (string, required, nullable) — Choose from GET /voices. Null explicitly resets to the supported default.

### SettingsPatch

Partial merge of these named settings. Absent fields keep their values. Null resets only nullable fields to platform defaults/unset. Arrays replace their field; extraction_fields=[] removes definitions. No serving-model, checkpoint, temperature or campaign retry knobs. Draft excludes inactive noise-suppression controls.

- `purpose` (string, optional, nullable) — What the agent is for. Null clears the value; omission keeps the current value.
- `goals` (list of string, optional, nullable) — Ordered goals for the agent. An empty array or null clears the goals; omission keeps the current value.
- `call_direction` (enum, optional, nullable) — The intended call direction. Null clears the value; omission keeps the current value.
  - Allowed values: `inbound`, `outbound`
- `grammatical_gender` (enum, optional, nullable) — The grammatical gender used for language generation and compatible voice selection. Null clears the value; omission keeps the current value.
  - Allowed values: `female`, `male`
- `additional_context` (string, optional, nullable) — Additional bounded context for the agent. Null clears the value; omission keeps the current value.
- `language` (enum, optional) — Agent language. Existing en and hi values remain supported; hinglish is additive. Omission in PATCH keeps the existing value.
  - Allowed values: `en`, `hi`, `hinglish`
- `agent_name` (string, optional, nullable)
- `first_message` (string, optional)
- `timezone` (string, optional) — IANA timezone.
- `max_call_duration_seconds` (integer, optional, nullable) — Platform default on create/reset: 1800; omission in PATCH keeps the existing value.
- `inactivity_timeout_seconds` (integer, optional, nullable) — Platform default on create/reset: 30; omission in PATCH keeps the existing value.
- `user_turn_timeout_ms` (integer, optional, nullable)
- `ivr_hangup` (boolean, optional)
- `voicemail_detection` (boolean, optional)
- `dtmf_enabled` (boolean, optional)
- `dtmf_timeout_seconds` (integer, optional)
- `dtmf_digit_limit` (integer, optional)
- `dtmf_termination_key` (string, optional, nullable)
- `bg_audio` (enum, optional, nullable)
  - Allowed values: `office`, `call_centre`, `cafe`
- `bg_volume` (integer, optional)
- `extraction_fields` (list of ExtractionField, optional)

### DefaultToolsPatch

Per-key merge: each policy you include merges field by field; policies you omit stay unchanged. New built-in policies may be added later; older clients that omit them keep them unchanged. end_call is enabled by default for every agent; only an explicit end_call.enabled=false turns it off. transfer starts disabled with a null destination.

- `end_call` (EndCallPolicyPatch, optional) — Only enable or disable. Name, prompt, description and parameters are not editable. Independent duration limits and external hangup still apply. Default enabled=true; only an explicit false disables it. In a PATCH, fields merge one by one: an omitted field keeps its value.
- `transfer` (TransferPolicyPatch, optional) — Enabled requires a destination_number. Disabled may retain a destination for later use. Fixed E164 transfer only in this version. In a PATCH, fields merge one by one: an omitted field keeps its value. The merged result must still pass the check that an enabled transfer has a destination_number.

### ValidationIssue

- `field` (string, required)
- `code` (string, required)
- `message` (string, required)

### Readiness

A snapshot, not a reservation. Admission rechecks current readiness. Publication, model support, knowledge and phone routing are separate prerequisites. Reasons can include credential_unavailable when the live version has tools that pin a revoked credential version.

- `state` (string, required) — Open response vocabulary. Preserve unknown values; never assume they mean success.
- `reasons` (list of ReadinessReasonsItems, required)

### ErrorResponseError

Only code and message are required. Rasen sends details, request_id and retry on every error, but clients must parse an error that lacks them, so a typed error is never lost to a parsing failure.

- `code` (string, required) — Stable code. Branch on code (and details.reason for conflict), never on message. Each code has one HTTP status; see components.x-error-catalog.
- `message` (string, required) — Safe description; no provider payload or submitted secrets.
- `details` (map from string to any, optional) — Safe code-specific details. The public fields for each code are listed in components.x-error-catalog. Details never contain a request fingerprint, a hash of a request, secret material or another tenant's data. forbidden may include required_capabilities (a list).
- `request_id` (string, optional) — Same value as the X-Request-Id header. Quote it to support.
- `retry` (string, optional) — Retry class of this code: no (fix the request or state first), yes (retry the same request and the same Idempotency-Key), retry_after (wait for Retry-After, then retry the same request). Open response vocabulary. Preserve unknown values; never assume they mean success.

### ExtractionField

Names are unique. choices is required only for selector. required is an extraction instruction, not a guarantee the caller supplies the fact. Missing evidence must not invent a value.

- `name` (string, required)
- `type` (enum, required)
  - Allowed values: `text`, `boolean`, `number`, `selector`, `datetime`
- `description` (string, optional)
- `required` (boolean, optional)
- `choices` (list of string, optional)

### EndCallPolicyPatch

Only enable or disable. Name, prompt, description and parameters are not editable. Independent duration limits and external hangup still apply. Default enabled=true; only an explicit false disables it. In a PATCH, fields merge one by one: an omitted field keeps its value.

- `enabled` (boolean, optional)

### TransferPolicyPatch

Enabled requires a destination_number. Disabled may retain a destination for later use. Fixed E164 transfer only in this version. In a PATCH, fields merge one by one: an omitted field keeps its value. The merged result must still pass the check that an enabled transfer has a destination_number.

- `enabled` (boolean, optional)
- `destination_number` (string, optional, nullable)

### ReadinessReasonsItems

- `code` (string, required) — Registered refusal or capability reason.
- `message` (string, required) — Safe next step for the integrator.

## Examples

**Request**

```json
{
  "prompt": "Confirm the appointment for {{customer_name}}.",
  "settings": {
    "language": "en",
    "first_message": "Hello, I am calling to confirm your appointment.",
    "extraction_fields": [
      {
        "name": "appointment_confirmed",
        "type": "boolean",
        "required": false
      }
    ]
  }
}
```

**Response**

```json
{
  "valid": true,
  "errors": [
    {
      "field": "example",
      "code": "example",
      "message": "Assign an outbound number to this agent."
    }
  ],
  "warnings": [
    {
      "field": "example",
      "code": "example",
      "message": "Assign an outbound number to this agent."
    }
  ],
  "readiness": {
    "state": "ready",
    "reasons": [
      {
        "code": "example",
        "message": "Assign an outbound number to this agent."
      }
    ]
  }
}
```

**SDK Code**

```python agents_draft_validate_example
import requests

url = "https://api.v2.rasen.ai/v1/agents/agent_id/draft/validate"

payload = {
    "prompt": "Confirm the appointment for {{customer_name}}.",
    "settings": {
        "language": "en",
        "first_message": "Hello, I am calling to confirm your appointment.",
        "extraction_fields": [
            {
                "name": "appointment_confirmed",
                "type": "boolean",
                "required": False
            }
        ]
    }
}
headers = {
    "If-Match": "\"draft-17\"",
    "Authorization": "Bearer <api_key>",
    "Content-Type": "application/json"
}

response = requests.post(url, json=payload, headers=headers)

print(response.json())
```

```javascript agents_draft_validate_example
const url = 'https://api.v2.rasen.ai/v1/agents/agent_id/draft/validate';
const options = {
  method: 'POST',
  headers: {
    'If-Match': '"draft-17"',
    Authorization: 'Bearer <api_key>',
    'Content-Type': 'application/json'
  },
  body: '{"prompt":"Confirm the appointment for {{customer_name}}.","settings":{"language":"en","first_message":"Hello, I am calling to confirm your appointment.","extraction_fields":[{"name":"appointment_confirmed","type":"boolean","required":false}]}}'
};

try {
  const response = await fetch(url, options);
  const data = await response.json();
  console.log(data);
} catch (error) {
  console.error(error);
}
```

```ruby agents_draft_validate_example
require 'uri'
require 'net/http'

url = URI("https://api.v2.rasen.ai/v1/agents/agent_id/draft/validate")

http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true

request = Net::HTTP::Post.new(url)
request["If-Match"] = '"draft-17"'
request["Authorization"] = 'Bearer <api_key>'
request["Content-Type"] = 'application/json'
request.body = "{\n  \"prompt\": \"Confirm the appointment for {{customer_name}}.\",\n  \"settings\": {\n    \"language\": \"en\",\n    \"first_message\": \"Hello, I am calling to confirm your appointment.\",\n    \"extraction_fields\": [\n      {\n        \"name\": \"appointment_confirmed\",\n        \"type\": \"boolean\",\n        \"required\": false\n      }\n    ]\n  }\n}"

response = http.request(request)
puts response.read_body
```

```java agents_draft_validate_example
import com.mashape.unirest.http.HttpResponse;
import com.mashape.unirest.http.Unirest;

HttpResponse<String> response = Unirest.post("https://api.v2.rasen.ai/v1/agents/agent_id/draft/validate")
  .header("If-Match", "\"draft-17\"")
  .header("Authorization", "Bearer <api_key>")
  .header("Content-Type", "application/json")
  .body("{\n  \"prompt\": \"Confirm the appointment for {{customer_name}}.\",\n  \"settings\": {\n    \"language\": \"en\",\n    \"first_message\": \"Hello, I am calling to confirm your appointment.\",\n    \"extraction_fields\": [\n      {\n        \"name\": \"appointment_confirmed\",\n        \"type\": \"boolean\",\n        \"required\": false\n      }\n    ]\n  }\n}")
  .asString();
```

```php agents_draft_validate_example
<?php
require_once('vendor/autoload.php');

$client = new \GuzzleHttp\Client();

$response = $client->request('POST', 'https://api.v2.rasen.ai/v1/agents/agent_id/draft/validate', [
  'body' => '{
  "prompt": "Confirm the appointment for {{customer_name}}.",
  "settings": {
    "language": "en",
    "first_message": "Hello, I am calling to confirm your appointment.",
    "extraction_fields": [
      {
        "name": "appointment_confirmed",
        "type": "boolean",
        "required": false
      }
    ]
  }
}',
  'headers' => [
    'Authorization' => 'Bearer <api_key>',
    'Content-Type' => 'application/json',
    'If-Match' => '"draft-17"',
  ],
]);

echo $response->getBody();
```

```csharp agents_draft_validate_example
using RestSharp;

var client = new RestClient("https://api.v2.rasen.ai/v1/agents/agent_id/draft/validate");
var request = new RestRequest(Method.POST);
request.AddHeader("If-Match", "\"draft-17\"");
request.AddHeader("Authorization", "Bearer <api_key>");
request.AddHeader("Content-Type", "application/json");
request.AddParameter("application/json", "{\n  \"prompt\": \"Confirm the appointment for {{customer_name}}.\",\n  \"settings\": {\n    \"language\": \"en\",\n    \"first_message\": \"Hello, I am calling to confirm your appointment.\",\n    \"extraction_fields\": [\n      {\n        \"name\": \"appointment_confirmed\",\n        \"type\": \"boolean\",\n        \"required\": false\n      }\n    ]\n  }\n}", ParameterType.RequestBody);
IRestResponse response = client.Execute(request);
```

```swift agents_draft_validate_example
import Foundation

let headers = [
  "If-Match": "\"draft-17\"",
  "Authorization": "Bearer <api_key>",
  "Content-Type": "application/json"
]
let parameters = [
  "prompt": "Confirm the appointment for {{customer_name}}.",
  "settings": [
    "language": "en",
    "first_message": "Hello, I am calling to confirm your appointment.",
    "extraction_fields": [
      [
        "name": "appointment_confirmed",
        "type": "boolean",
        "required": false
      ]
    ]
  ]
] as [String : Any]

let postData = JSONSerialization.data(withJSONObject: parameters, options: [])

let request = NSMutableURLRequest(url: NSURL(string: "https://api.v2.rasen.ai/v1/agents/agent_id/draft/validate")! as URL,
                                        cachePolicy: .useProtocolCachePolicy,
                                    timeoutInterval: 10.0)
request.httpMethod = "POST"
request.allHTTPHeaderFields = headers
request.httpBody = postData as Data

let session = URLSession.shared
let dataTask = session.dataTask(with: request as URLRequest, completionHandler: { (data, response, error) -> Void in
  if (error != nil) {
    print(error as Any)
  } else {
    let httpResponse = response as? HTTPURLResponse
    print(httpResponse)
  }
})

dataTask.resume()
```