From 435de4fe65da9b09e2d4b4d2acd74b949845a0f5 Mon Sep 17 00:00:00 2001
From: "mintlify[bot]" <109931778+mintlify[bot]@users.noreply.github.com>
Date: Sat, 11 Apr 2026 05:32:17 +0000
Subject: [PATCH] Sync API docs with codebase after TRY-825 global AI rate
limiter
- classify: fix response shape (intent as array, add id/object/provider/model/cost_usd/credits_used), add provider query param, remove stale fields (toxicity, categories, platform, channel_id, metadata)
- rubric-classify: fix endpoint path (/v1/rubric-classify not /v1/rubric/classify), update request body (categories as objects with name/description, add multi_label/confidence_threshold), fix response shape
- comments-reply: fix endpoint (/v1/comments/reply with text in body, not path param), update response to match code (classification + reply objects), fix default tone to professional
- feedback: fix request body (classification_id + rating, not request_id + field/expected_value), add corrected_intent/corrected_sentiment params, fix response shape
- health: fix response to match code (services: database/redis/nagl with latency_ms, 503 on degraded)
- rate-limits: add sandbox/trial tiers, document global AI cap (900 RPM sliding window)
- errors: update envelope to match OpenAPI spec, add global_ai_cap rate limit error, fix permission_error type for 402
- webhooks: remove undocumented event types
- openapi.yaml: fix rubric-classify and comments/reply paths, fix usage param indentation
Generated-By: mintlify-agent
---
api-reference/classify.mdx | 302 ++++++++++++------------------
api-reference/comments-reply.mdx | 125 +++++++++----
api-reference/feedback.mdx | 86 ++++++---
api-reference/health.mdx | 97 +++++++---
api-reference/rubric-classify.mdx | 186 ++++++++++++------
errors.mdx | 92 +++++----
openapi.yaml | 43 +++--
rate-limits.mdx | 42 ++++-
webhooks.mdx | 2 -
9 files changed, 579 insertions(+), 396 deletions(-)
diff --git a/api-reference/classify.mdx b/api-reference/classify.mdx
index ac3d730..7de6c2f 100644
--- a/api-reference/classify.mdx
+++ b/api-reference/classify.mdx
@@ -1,16 +1,16 @@
---
title: "Classify Comment"
sidebarTitle: "POST /v1/classify"
-description: "Classify an Arabic or English comment by intent, sentiment, dialect, and toxicity in a single API call."
+description: "Classify an Arabic or English comment by intent, sentiment, dialect, and priority in a single API call."
api: "POST https://api.trynawa.com/v1/classify"
---
-Classify any comment with a single request. Returns intent, sentiment, dialect, and toxicity analysis. Arabic comments are routed to HUMAIN's ALLaM model for dialect detection. English comments are routed to Claude for high-accuracy classification.
+Classify any comment with a single request. Returns intent, sentiment, dialect, priority, and a suggested reply direction. Arabic comments are routed through the NAGL pipeline with dialect detection. English comments are routed to Claude for high-accuracy classification. Supports semantic caching and per-request provider override for A/B testing.
## Request
- Cost: **$0.006** per request. Semantic cache hits are free (`X-NAWA-Cache: HIT`).
+ Cost: **$0.006** per request (6 credits). Semantic cache hits are free (`X-NAWA-Cache: HIT`).
### Headers
@@ -20,14 +20,18 @@ Classify any comment with a single request. Returns intent, sentiment, dialect,
| `Authorization` | Yes | `Bearer nawa_live_sk_xxx` or `Bearer nawa_test_sk_xxx` |
| `Content-Type` | Yes | `application/json` |
+### Query parameters
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `provider` | string | No | Force a specific AI provider for A/B testing: `claude`, `gemini`, or `allam`. Bypasses language-based routing. |
+
### Body parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
-| `text` | string | Yes | The comment text to classify. Max 5,000 characters. |
-| `platform` | string | No | Source platform: `youtube`, `instagram`, `twitter`, `facebook`. Improves accuracy with platform-specific context. |
-| `channel_id` | string | No | Your channel or account identifier for analytics grouping. |
-| `metadata` | object | No | Arbitrary key-value metadata to attach to the classification. |
+| `text` | string | Yes | The comment text to classify. Must be non-empty. |
+| `context` | object | No | Optional context object to improve classification accuracy. |
### Example request
@@ -38,9 +42,7 @@ curl -X POST https://api.trynawa.com/v1/classify \
-H "Authorization: Bearer nawa_test_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
- "text": "متى الجزء الثاني؟",
- "platform": "youtube",
- "channel_id": "ch_123"
+ "text": "ماشاء الله الفيديو حلو مرة"
}'
```
@@ -50,9 +52,7 @@ import { Nawa } from '@nawalabs/sdk'
const nawa = new Nawa({ apiKey: process.env.NAWA_API_KEY })
const { data, error } = await nawa.classify({
- text: 'متى الجزء الثاني؟',
- platform: 'youtube',
- channelId: 'ch_123'
+ text: 'ماشاء الله الفيديو حلو مرة'
})
```
@@ -62,9 +62,7 @@ from nawa import Nawa
nawa = Nawa(api_key="your_api_key")
result = nawa.classify(
- text="متى الجزء الثاني؟",
- platform="youtube",
- channel_id="ch_123"
+ text="ماشاء الله الفيديو حلو مرة"
)
```
@@ -78,22 +76,28 @@ result = nawa.classify(
{
"success": true,
"result": {
- "text": "متى الجزء الثاني؟",
- "intent": "question",
- "intent_confidence": 0.97,
- "sentiment": "neutral",
- "sentiment_confidence": 0.91,
- "dialect": "gulf",
- "dialect_confidence": 0.95,
- "toxicity": "none",
- "toxicity_confidence": 0.99,
- "categories": ["engagement"],
+ "id": "cls_nw_a1b2c3d4e5f6",
+ "object": "classification",
+ "intent": ["praise"],
+ "sentiment": "positive",
"language": "ar",
- "model": "nagl-v1",
- "cached": false
+ "dialect": "gulf",
+ "dialect_confidence": 0.92,
+ "requires_response": false,
+ "priority": "medium",
+ "suggested_reply": {
+ "text": "تعليق إيجابي يعبر عن إعجاب المستخدم بالمحتوى",
+ "direction": "rtl"
+ },
+ "provider": "allam",
+ "model": "sdaia/allam-1-13b-instruct",
+ "fallback_used": false,
+ "tokens_used": null,
+ "cost_usd": 0.006,
+ "credits_used": 6
},
"errors": [],
- "request_id": "req_abc123def456"
+ "request_id": "req_nw_a1b2c3d4e5f67890abcdef12"
}
```
@@ -101,51 +105,55 @@ result = nawa.classify(
| Header | Description |
|--------|-------------|
-| `X-Request-Id` | Unique request identifier for debugging |
+| `X-Request-Id` | Unique request identifier (`req_nw_xxx` format) |
| `X-RateLimit-Limit` | Rate limit ceiling for current window |
| `X-RateLimit-Remaining` | Requests remaining in current window |
| `X-RateLimit-Reset` | Window reset time (RFC 3339) |
-| `X-NAWA-Balance` | Current credit balance in USD |
-| `X-NAWA-Balance-Warning` | `low_balance` when below $5 |
-| `X-NAWA-Cache` | `HIT` if served from semantic cache (no cost) |
+| `X-NAWA-Provider` | AI provider used: `claude`, `gemini`, or `allam` |
+| `X-NAWA-Cache` | `HIT` if served from semantic cache (no cost), `MISS` otherwise |
+| `X-NAWA-Latency` | Processing time in milliseconds |
+| `X-NAWA-Calibration-Version` | Version of the active calibration thresholds |
### Result fields
| Field | Type | Description |
|-------|------|-------------|
-| `text` | string | The original input text |
-| `intent` | string | `question`, `complaint`, `praise`, `suggestion`, `spam`, `other` |
-| `intent_confidence` | number | Confidence score (0–1) |
-| `sentiment` | string | `positive`, `negative`, `neutral`, `mixed` |
-| `sentiment_confidence` | number | Confidence score (0–1) |
-| `dialect` | string \| null | `gulf`, `egyptian`, `levantine`, `msa`. Returns `null` for English text. |
-| `dialect_confidence` | number \| null | Confidence score (0-1). Returns `null` for English text. |
-| `toxicity` | string | `none`, `mild`, `moderate`, `severe` |
-| `toxicity_confidence` | number | Confidence score (0–1) |
-| `categories` | string[] | Content categories: `engagement`, `support`, `feedback`, `spam` |
-| `language` | string | Detected language code (e.g., `ar`, `en`) |
+| `id` | string | Classification ID (`cls_nw_xxx` format) |
+| `object` | string | Always `classification` |
+| `intent` | string[] | Detected intents, e.g. `["praise"]`, `["question", "complaint"]` |
+| `sentiment` | string | `positive`, `negative`, `neutral`, or `mixed` |
+| `language` | string | Detected language: `ar`, `en`, or `mixed` |
+| `dialect` | string or null | Arabic dialect: `gulf`, `egyptian`, `levantine`, `maghrebi`, `msa`. Null for English text. |
+| `dialect_confidence` | number or null | Confidence score (0-1) for dialect detection. Null for English text. |
+| `requires_response` | boolean | Whether the comment warrants a reply |
+| `priority` | string | `high`, `medium`, or `low` |
+| `suggested_reply` | object | Contains `text` (reply direction summary) and `direction` (`ltr` or `rtl`) |
+| `provider` | string | AI provider used: `claude`, `gemini`, or `allam` |
| `model` | string | Model version used for classification |
-| `cached` | boolean | Whether this was served from semantic cache |
+| `fallback_used` | boolean | Whether a fallback provider was used |
+| `tokens_used` | integer or null | Token count (may be null) |
+| `cost_usd` | number | Cost in USD |
+| `credits_used` | integer | Credits deducted |
### Error responses
| Status | Type | When |
|--------|------|------|
-| 400 | `invalid_request_error` | Missing `text`, invalid `platform`, text too long |
+| 400 | `invalid_request_error` | Missing `text`, invalid `provider` query parameter |
| 401 | `authentication_error` | Invalid or missing API key |
-| 402 | `insufficient_credits` | No credits remaining |
-| 429 | `rate_limit_error` | Rate limit exceeded |
+| 402 | `permission_error` | Insufficient credits |
+| 429 | `rate_limit_error` | Per-key or global rate limit exceeded |
| 500 | `api_error` | Internal or provider error |
### More examples
-
+
```bash
curl -X POST https://api.trynawa.com/v1/classify \
-H "Authorization: Bearer nawa_test_sk_xxx" \
-H "Content-Type: application/json" \
- -d '{"text": "This is hands down the best review I have seen on this phone. Subscribed!", "platform": "youtube"}'
+ -d '{"text": "Great video, love the content!"}'
```
Response:
@@ -153,65 +161,39 @@ result = nawa.classify(
{
"success": true,
"result": {
- "text": "This is hands down the best review I have seen on this phone. Subscribed!",
- "intent": "praise",
- "intent_confidence": 0.96,
+ "id": "cls_nw_b2c3d4e5f6a7",
+ "object": "classification",
+ "intent": ["praise"],
"sentiment": "positive",
- "sentiment_confidence": 0.98,
- "dialect": null,
- "dialect_confidence": null,
- "toxicity": "none",
- "toxicity_confidence": 0.99,
- "categories": ["engagement"],
"language": "en",
- "model": "claude-v1",
- "cached": false
- },
- "errors": [],
- "request_id": "req_en_praise_001"
- }
- ```
-
-
-
- ```bash
- curl -X POST https://api.trynawa.com/v1/classify \
- -H "Authorization: Bearer nawa_test_sk_xxx" \
- -H "Content-Type: application/json" \
- -d '{"text": "The audio quality is terrible in this one. Can barely hear anything after the 5 minute mark.", "platform": "youtube"}'
- ```
-
- Response:
- ```json
- {
- "success": true,
- "result": {
- "text": "The audio quality is terrible in this one. Can barely hear anything after the 5 minute mark.",
- "intent": "complaint",
- "intent_confidence": 0.94,
- "sentiment": "negative",
- "sentiment_confidence": 0.96,
"dialect": null,
"dialect_confidence": null,
- "toxicity": "none",
- "toxicity_confidence": 0.97,
- "categories": ["feedback"],
- "language": "en",
- "model": "claude-v1",
- "cached": false
+ "requires_response": false,
+ "priority": "medium",
+ "suggested_reply": {
+ "text": "Positive comment expressing appreciation for the content",
+ "direction": "ltr"
+ },
+ "provider": "claude",
+ "model": "claude-haiku-4-5-20251001",
+ "fallback_used": false,
+ "tokens_used": null,
+ "cost_usd": 0.006,
+ "credits_used": 6
},
"errors": [],
- "request_id": "req_en_complaint_001"
+ "request_id": "req_nw_en_praise_001abcdef1234"
}
```
-
+
+ Force classification through the `allam` provider:
```bash
- curl -X POST https://api.trynawa.com/v1/classify \
+ curl -X POST "https://api.trynawa.com/v1/classify?provider=allam" \
-H "Authorization: Bearer nawa_test_sk_xxx" \
-H "Content-Type: application/json" \
- -d '{"text": "What camera and lens setup are you using for these shots?", "platform": "youtube"}'
+ -d '{"text": "ما شاء الله عليك، محتوى رهيب!"}'
```
Response:
@@ -219,98 +201,38 @@ result = nawa.classify(
{
"success": true,
"result": {
- "text": "What camera and lens setup are you using for these shots?",
- "intent": "question",
- "intent_confidence": 0.97,
- "sentiment": "neutral",
- "sentiment_confidence": 0.92,
- "dialect": null,
- "dialect_confidence": null,
- "toxicity": "none",
- "toxicity_confidence": 0.99,
- "categories": ["engagement"],
- "language": "en",
- "model": "claude-v1",
- "cached": false
- },
- "errors": [],
- "request_id": "req_en_question_001"
- }
- ```
-
-
-
- ```bash
- curl -X POST https://api.trynawa.com/v1/classify \
- -H "Authorization: Bearer nawa_test_sk_xxx" \
- -H "Content-Type: application/json" \
- -d '{"text": "ما شاء الله عليك، محتوى رهيب!", "platform": "youtube"}'
- ```
-
- Response:
- ```json
- {
- "success": true,
- "result": {
- "text": "ما شاء الله عليك، محتوى رهيب!",
- "intent": "praise",
- "intent_confidence": 0.98,
+ "id": "cls_nw_c3d4e5f6a7b8",
+ "object": "classification",
+ "intent": ["praise"],
"sentiment": "positive",
- "sentiment_confidence": 0.97,
+ "language": "ar",
"dialect": "gulf",
"dialect_confidence": 0.93,
- "toxicity": "none",
- "toxicity_confidence": 0.99,
- "categories": ["engagement"],
- "language": "ar",
- "model": "nagl-v1",
- "cached": false
- },
- "errors": [],
- "request_id": "req_ghi789jkl012"
- }
- ```
-
-
-
- ```bash
- curl -X POST https://api.trynawa.com/v1/classify \
- -H "Authorization: Bearer nawa_test_sk_xxx" \
- -H "Content-Type: application/json" \
- -d '{"text": "الصوت وحش أوي في الفيديو ده", "platform": "youtube"}'
- ```
-
- Response:
- ```json
- {
- "success": true,
- "result": {
- "text": "الصوت وحش أوي في الفيديو ده",
- "intent": "complaint",
- "intent_confidence": 0.94,
- "sentiment": "negative",
- "sentiment_confidence": 0.96,
- "dialect": "egyptian",
- "dialect_confidence": 0.97,
- "toxicity": "none",
- "toxicity_confidence": 0.95,
- "categories": ["feedback"],
- "language": "ar",
- "model": "nagl-v1",
- "cached": false
+ "requires_response": false,
+ "priority": "medium",
+ "suggested_reply": {
+ "text": "تعليق إيجابي يعبر عن إعجاب المستخدم بالمحتوى",
+ "direction": "rtl"
+ },
+ "provider": "allam",
+ "model": "sdaia/allam-1-13b-instruct",
+ "fallback_used": false,
+ "tokens_used": null,
+ "cost_usd": 0.006,
+ "credits_used": 6
},
"errors": [],
- "request_id": "req_mno345pqr678"
+ "request_id": "req_nw_ghi789jkl012abcdef1234"
}
```
-
+
```bash
curl -X POST https://api.trynawa.com/v1/classify \
-H "Authorization: Bearer nawa_test_sk_xxx" \
-H "Content-Type: application/json" \
- -d '{"text": "لو سمحت حاول تحكي عن المطاعم بلبنان", "platform": "instagram"}'
+ -d '{"text": "متى الجزء الثاني؟"}'
```
Response:
@@ -318,22 +240,28 @@ result = nawa.classify(
{
"success": true,
"result": {
- "text": "لو سمحت حاول تحكي عن المطاعم بلبنان",
- "intent": "suggestion",
- "intent_confidence": 0.92,
+ "id": "cls_nw_d4e5f6a7b8c9",
+ "object": "classification",
+ "intent": ["question"],
"sentiment": "neutral",
- "sentiment_confidence": 0.88,
- "dialect": "levantine",
- "dialect_confidence": 0.96,
- "toxicity": "none",
- "toxicity_confidence": 0.99,
- "categories": ["engagement"],
"language": "ar",
- "model": "nagl-v1",
- "cached": false
+ "dialect": "gulf",
+ "dialect_confidence": 0.91,
+ "requires_response": true,
+ "priority": "high",
+ "suggested_reply": {
+ "text": "سؤال عن موعد الحلقة القادمة",
+ "direction": "rtl"
+ },
+ "provider": "allam",
+ "model": "sdaia/allam-1-13b-instruct",
+ "fallback_used": false,
+ "tokens_used": null,
+ "cost_usd": 0.006,
+ "credits_used": 6
},
"errors": [],
- "request_id": "req_stu901vwx234"
+ "request_id": "req_nw_stu901vwx234abcdef1234"
}
```
diff --git a/api-reference/comments-reply.mdx b/api-reference/comments-reply.mdx
index d6321d7..8d89cc5 100644
--- a/api-reference/comments-reply.mdx
+++ b/api-reference/comments-reply.mdx
@@ -1,59 +1,75 @@
---
-title: "Reply to Comment"
-sidebarTitle: "POST /v1/comments/:id/reply"
-description: "Generate a context-aware reply to a comment in Arabic or English."
-api: "POST https://api.trynawa.com/v1/comments/{id}/reply"
+title: "Classify and Reply"
+sidebarTitle: "POST /v1/comments/reply"
+description: "Classify a comment and generate a contextual reply in a single API call."
+api: "POST https://api.trynawa.com/v1/comments/reply"
---
-Generate an AI-powered reply that matches the commenter's language and cultural context. For Arabic comments, replies match the detected dialect (Gulf, Egyptian, Levantine, MSA). For English comments, replies are natural and platform-appropriate. Language is auto-detected unless overridden.
+Classify a comment and generate a contextual reply in a single call. The reply matches the commenter's language and dialect. For Arabic comments, replies use the detected dialect (Gulf, Egyptian, Levantine, MSA). For English comments, replies are natural and platform-appropriate. Supports tone control and max length configuration.
- Cost: **$0.008** per request (8 credits). Semantic cache hits are free (`X-NAWA-Cache: HIT`).
+ Cost: **$0.008** per request (8 credits).
## Request
-### Path parameters
+### Headers
+
+| Header | Required | Description |
+|--------|----------|-------------|
+| `Authorization` | Yes | `Bearer nawa_live_sk_xxx` or `Bearer nawa_test_sk_xxx` |
+| `Content-Type` | Yes | `application/json` |
+
+### Query parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
-| `id` | string | Yes | The comment ID to reply to. |
+| `provider` | string | No | Force a specific AI provider: `claude`, `gemini`, or `allam`. |
### Body parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
-| `tone` | string | No | Reply tone: `friendly`, `professional`, `casual`, `formal`. Default: `friendly`. |
-| `max_length` | integer | No | Maximum reply length in characters. Default: 500. |
-| `context` | string | No | Additional context about the channel or video to improve reply relevance. |
-| `language` | string | No | Force reply language: `ar`, `en`, `auto`. Default: `auto` (matches commenter's language). |
+| `text` | string | Yes | The comment text to classify and reply to. Must be non-empty. |
+| `tone` | string | No | Reply tone: `friendly`, `professional`, `casual`, `formal`. Default: `professional`. |
+| `max_length` | integer | No | Maximum reply length in characters (1-2000). Default: `500`. |
### Example request
```bash cURL
-curl -X POST https://api.trynawa.com/v1/comments/cmt_abc123/reply \
+curl -X POST https://api.trynawa.com/v1/comments/reply \
-H "Authorization: Bearer nawa_test_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
- "tone": "friendly",
- "context": "Tech review channel focused on smartphones"
+ "text": "هذا المنتج سيء جداً ولا أنصح به",
+ "tone": "professional",
+ "max_length": 500
}'
```
```typescript TypeScript
-const { data, error } = await nawa.comments.reply('cmt_abc123', {
- tone: 'friendly',
- context: 'Tech review channel focused on smartphones'
+import { Nawa } from '@nawalabs/sdk'
+
+const nawa = new Nawa({ apiKey: process.env.NAWA_API_KEY })
+
+const { data, error } = await nawa.comments.reply({
+ text: 'هذا المنتج سيء جداً ولا أنصح به',
+ tone: 'professional',
+ maxLength: 500
})
```
```python Python
+from nawa import Nawa
+
+nawa = Nawa(api_key="your_api_key")
+
result = nawa.comments.reply(
- comment_id="cmt_abc123",
- tone="friendly",
- context="Tech review channel focused on smartphones"
+ text="هذا المنتج سيء جداً ولا أنصح به",
+ tone="professional",
+ max_length=500
)
```
@@ -67,25 +83,66 @@ result = nawa.comments.reply(
{
"success": true,
"result": {
- "comment_id": "cmt_abc123",
- "reply_text": "إن شاء الله الجزء الثاني قريب! تابعنا عشان ما يفوتك 🔔",
- "reply_dialect": "gulf",
- "tone": "friendly",
- "original_intent": "question",
- "original_dialect": "gulf"
+ "id": "rpl_nw_a1b2c3d4e5f6",
+ "object": "comment_reply",
+ "classification": {
+ "intent": ["complaint"],
+ "sentiment": "negative",
+ "priority": "high",
+ "requires_response": true
+ },
+ "reply": {
+ "text": "نشكرك على ملاحظتك ونعتذر عن أي تجربة غير مرضية. نحرص على تحسين منتجاتنا باستمرار ونقدر رأيك.",
+ "direction": "rtl",
+ "tone": "professional"
+ },
+ "provider": "claude",
+ "model": "claude-haiku-4-5-20251001",
+ "cost_usd": 0.008,
+ "credits_used": 8
},
"errors": [],
- "request_id": "req_rep789xyz012"
+ "request_id": "req_nw_rep789xyz012abcdef12"
}
```
+### Response headers
+
+| Header | Description |
+|--------|-------------|
+| `X-Request-Id` | Unique request identifier (`req_nw_xxx` format) |
+| `X-RateLimit-Limit` | Rate limit ceiling for current window |
+| `X-RateLimit-Remaining` | Requests remaining in current window |
+| `X-RateLimit-Reset` | Window reset time (RFC 3339) |
+| `X-NAWA-Provider` | AI provider used: `claude`, `gemini`, or `allam` |
+| `X-NAWA-Latency` | Processing time in milliseconds |
+
### Result fields
| Field | Type | Description |
|-------|------|-------------|
-| `comment_id` | string | The comment that was replied to |
-| `reply_text` | string | The generated reply text |
-| `reply_dialect` | string \| null | Dialect used in the reply (matches original). `null` for English replies. |
-| `tone` | string | The tone used for the reply |
-| `original_intent` | string | Detected intent of the original comment |
-| `original_dialect` | string | Detected dialect of the original comment |
+| `id` | string | Reply ID (`rpl_nw_xxx` format) |
+| `object` | string | Always `comment_reply` |
+| `classification` | object | Classification of the original comment |
+| `classification.intent` | string[] | Detected intents |
+| `classification.sentiment` | string | `positive`, `negative`, `neutral`, or `mixed` |
+| `classification.priority` | string | `high`, `medium`, or `low` |
+| `classification.requires_response` | boolean | Whether the comment warrants a reply |
+| `reply` | object | The generated reply |
+| `reply.text` | string | Reply text in the commenter's language/dialect |
+| `reply.direction` | string | Text direction: `ltr` or `rtl` |
+| `reply.tone` | string | The tone used for the reply |
+| `provider` | string | AI provider used |
+| `model` | string | Model version used |
+| `cost_usd` | number | Cost in USD |
+| `credits_used` | integer | Credits deducted |
+
+### Error responses
+
+| Status | Type | When |
+|--------|------|------|
+| 400 | `invalid_request_error` | Missing `text`, invalid `tone`, `max_length` out of range, invalid `provider` |
+| 401 | `authentication_error` | Invalid or missing API key |
+| 402 | `permission_error` | Insufficient credits |
+| 429 | `rate_limit_error` | Per-key or global rate limit exceeded |
+| 500 | `api_error` | Internal or provider error |
diff --git a/api-reference/feedback.mdx b/api-reference/feedback.mdx
index 4cddf8b..e5267b0 100644
--- a/api-reference/feedback.mdx
+++ b/api-reference/feedback.mdx
@@ -1,14 +1,14 @@
---
title: "Submit Feedback"
sidebarTitle: "POST /v1/feedback"
-description: "Submit RLHF feedback to improve classification accuracy over time."
+description: "Submit RLHF feedback on classification results to improve accuracy over time."
api: "POST https://api.trynawa.com/v1/feedback"
---
-Submit reinforcement learning from human feedback (RLHF) to continuously improve NAWA's classification accuracy.
+Submit reinforcement learning from human feedback (RLHF) on classification results. Feedback is used for quorum-based pattern confirmation to improve NAWA's classification accuracy.
- This endpoint is **free** -- 0 credits, no cost.
+ This endpoint is **free** -- 0 credits, no cost. Available on all tiers.
@@ -17,14 +17,22 @@ Submit reinforcement learning from human feedback (RLHF) to continuously improve
## Request
+### Headers
+
+| Header | Required | Description |
+|--------|----------|-------------|
+| `Authorization` | Yes | `Bearer nawa_live_sk_xxx` or `Bearer nawa_test_sk_xxx` |
+| `Content-Type` | Yes | `application/json` |
+
### Body parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
-| `request_id` | string | Yes | The `request_id` from the original classification response. |
-| `field` | string | Yes | The field to correct: `intent`, `sentiment`, `dialect`, `toxicity`, `category`. |
-| `expected_value` | string | Yes | The correct value the model should have returned. |
-| `comment` | string | No | Optional free-text explanation of why this correction is needed. |
+| `classification_id` | string | Yes | The `id` (`cls_nw_xxx`) from the original classification response. |
+| `rating` | string | Yes | Feedback rating: `correct`, `incorrect`, or `partial`. |
+| `corrected_intent` | string[] | No | The correct intent values the model should have returned. |
+| `corrected_sentiment` | string | No | The correct sentiment: `positive`, `negative`, `neutral`, or `mixed`. |
+| `comment` | string | No | Free-text explanation of why this correction is needed. |
### Example request
@@ -35,28 +43,39 @@ curl -X POST https://api.trynawa.com/v1/feedback \
-H "Authorization: Bearer nawa_test_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
- "request_id": "req_abc123def456",
- "field": "dialect",
- "expected_value": "levantine",
- "comment": "This is Lebanese Arabic, not Gulf"
+ "classification_id": "cls_nw_a1b2c3d4e5f6",
+ "rating": "incorrect",
+ "corrected_intent": ["suggestion"],
+ "corrected_sentiment": "neutral",
+ "comment": "This is a suggestion, not a complaint"
}'
```
```typescript TypeScript
+import { Nawa } from '@nawalabs/sdk'
+
+const nawa = new Nawa({ apiKey: process.env.NAWA_API_KEY })
+
const { data, error } = await nawa.feedback.submit({
- requestId: 'req_abc123def456',
- field: 'dialect',
- expectedValue: 'levantine',
- comment: 'This is Lebanese Arabic, not Gulf'
+ classificationId: 'cls_nw_a1b2c3d4e5f6',
+ rating: 'incorrect',
+ correctedIntent: ['suggestion'],
+ correctedSentiment: 'neutral',
+ comment: 'This is a suggestion, not a complaint'
})
```
```python Python
+from nawa import Nawa
+
+nawa = Nawa(api_key="your_api_key")
+
result = nawa.feedback.submit(
- request_id="req_abc123def456",
- field="dialect",
- expected_value="levantine",
- comment="This is Lebanese Arabic, not Gulf"
+ classification_id="cls_nw_a1b2c3d4e5f6",
+ rating="incorrect",
+ corrected_intent=["suggestion"],
+ corrected_sentiment="neutral",
+ comment="This is a suggestion, not a complaint"
)
```
@@ -70,11 +89,32 @@ result = nawa.feedback.submit(
{
"success": true,
"result": {
- "feedback_id": "fb_xyz789",
- "status": "accepted",
- "message": "Thank you! Your feedback helps improve NAWA's accuracy."
+ "id": "fb_nw_a1b2c3d4e5f6",
+ "object": "feedback",
+ "classification_id": "cls_nw_a1b2c3d4e5f6",
+ "rating": "incorrect",
+ "acknowledged": true
},
"errors": [],
- "request_id": "req_fb_abc123"
+ "request_id": "req_nw_fb_abc123def45612"
}
```
+
+### Result fields
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `id` | string | Feedback ID (`fb_nw_xxx` format) |
+| `object` | string | Always `feedback` |
+| `classification_id` | string | The classification this feedback applies to |
+| `rating` | string | The submitted rating |
+| `acknowledged` | boolean | Always `true` on success |
+
+### Error responses
+
+| Status | Type | When |
+|--------|------|------|
+| 400 | `invalid_request_error` | Missing `classification_id` or `rating`, invalid `rating` value, invalid `corrected_sentiment` |
+| 401 | `authentication_error` | Invalid or missing API key |
+| 429 | `rate_limit_error` | Rate limit exceeded |
+| 500 | `api_error` | Internal error |
diff --git a/api-reference/health.mdx b/api-reference/health.mdx
index ca110e8..dcd66a7 100644
--- a/api-reference/health.mdx
+++ b/api-reference/health.mdx
@@ -5,58 +5,99 @@ description: "Check the operational status of the NAWA API and its dependencies.
api: "GET https://api.trynawa.com/v1/health"
---
-Check the operational status of the NAWA API. This endpoint is **free** and does **not** require authentication.
+Check the operational status of the NAWA API and its backing services. This endpoint is **free** and does **not** require authentication.
## Request
-```bash
+
+
+```bash cURL
curl https://api.trynawa.com/v1/health
```
+```typescript TypeScript
+const response = await fetch('https://api.trynawa.com/v1/health')
+const health = await response.json()
+console.log(health.status) // "healthy" or "degraded"
+```
+
+```python Python
+import requests
+
+response = requests.get("https://api.trynawa.com/v1/health")
+health = response.json()
+print(health["status"]) # "healthy" or "degraded"
+```
+
+
+
## Response
### Healthy response (200)
+Returned when all services are operational.
+
```json
{
- "success": true,
- "result": {
- "status": "healthy",
- "version": "1.0.0",
- "services": {
- "api": "operational",
- "classification": "operational",
- "database": "operational",
- "cache": "operational"
+ "status": "healthy",
+ "version": "v1",
+ "services": {
+ "database": {
+ "status": "healthy",
+ "latency_ms": 12
+ },
+ "redis": {
+ "status": "healthy",
+ "latency_ms": 3
},
- "timestamp": "2025-01-15T12:00:00Z"
+ "nagl": {
+ "status": "healthy"
+ }
},
- "errors": [],
- "request_id": "req_hlt_abc123"
+ "timestamp": "2026-04-11T12:00:00.000Z",
+ "latency_ms": 15
}
```
-### Degraded response (200)
+### Degraded response (503)
+
+Returned when one or more services are degraded or down.
```json
{
- "success": true,
- "result": {
- "status": "degraded",
- "version": "1.0.0",
- "services": {
- "api": "operational",
- "classification": "degraded",
- "database": "operational",
- "cache": "operational"
+ "status": "degraded",
+ "version": "v1",
+ "services": {
+ "database": {
+ "status": "healthy",
+ "latency_ms": 14
},
- "timestamp": "2025-01-15T12:00:00Z"
+ "redis": {
+ "status": "down",
+ "latency_ms": 5001
+ },
+ "nagl": {
+ "status": "healthy"
+ }
},
- "errors": [],
- "request_id": "req_hlt_def456"
+ "timestamp": "2026-04-11T12:00:00.000Z",
+ "latency_ms": 5015
}
```
+### Result fields
+
+| Field | Type | Description |
+|-------|------|-------------|
+| `status` | string | Overall status: `healthy` or `degraded` |
+| `version` | string | API version (always `v1`) |
+| `services` | object | Status of individual backing services |
+| `services.database` | object | Database connectivity. `status`: `healthy`, `degraded`, or `down`. `latency_ms`: check duration. |
+| `services.redis` | object | Redis cache connectivity. `status`: `healthy`, `degraded`, or `down`. `latency_ms`: check duration. |
+| `services.nagl` | object | NAGL classification engine. `status`: `healthy` or `down`. |
+| `timestamp` | string | ISO 8601 timestamp of this check |
+| `latency_ms` | integer | Total time to run all health checks |
+
- For real-time status monitoring, visit [status.trynawa.com](https://status.trynawa.com).
+ The health endpoint returns HTTP `200` when all services are healthy, and `503` when any service is degraded or down. For real-time status monitoring, visit [status.trynawa.com](https://status.trynawa.com).
diff --git a/api-reference/rubric-classify.mdx b/api-reference/rubric-classify.mdx
index 4489175..c3087b2 100644
--- a/api-reference/rubric-classify.mdx
+++ b/api-reference/rubric-classify.mdx
@@ -1,80 +1,101 @@
---
title: "Rubric Classify"
-sidebarTitle: "POST /v1/rubric/classify"
-description: "Classify a comment against a custom rubric with predefined scoring criteria."
-api: "POST https://api.trynawa.com/v1/rubric/classify"
+sidebarTitle: "POST /v1/rubric-classify"
+description: "Classify text against a custom rubric with user-defined categories and confidence thresholds."
+api: "POST https://api.trynawa.com/v1/rubric-classify"
---
-Classify a comment against a custom rubric. Define your own categories and scoring criteria for domain-specific classification.
+Classify text against a user-defined rubric. Define your own categories with optional descriptions, enable multi-label classification, and set confidence thresholds for domain-specific needs.
- Cost: **$0.003** per request (3 credits). **500 free requests/month** on this endpoint -- no credit card required. Semantic cache hits are free (`X-NAWA-Cache: HIT`).
+ Cost: **$0.003** per request (3 credits). Free-tier keys can use this endpoint. Sandbox keys are not charged credits.
## Request
+### Headers
+
+| Header | Required | Description |
+|--------|----------|-------------|
+| `Authorization` | Yes | `Bearer nawa_live_sk_xxx` or `Bearer nawa_test_sk_xxx` |
+| `Content-Type` | Yes | `application/json` |
+
+### Query parameters
+
+| Parameter | Type | Required | Description |
+|-----------|------|----------|-------------|
+| `provider` | string | No | Force a specific AI provider: `claude`, `gemini`, or `allam`. |
+
### Body parameters
| Parameter | Type | Required | Description |
|-----------|------|----------|-------------|
-| `text` | string | Yes | The comment text to classify. Max 5,000 characters. |
-| `rubric` | object | Yes | The rubric definition with categories and criteria. |
-| `rubric.categories` | string[] | Yes | List of category names to classify against. |
-| `rubric.descriptions` | object | No | Category descriptions to guide the model. Keys are category names, values are description strings. |
-| `platform` | string | No | Source platform for context. |
+| `text` | string | Yes | The text to classify. Must be non-empty. |
+| `rubric` | object | Yes | The rubric definition. |
+| `rubric.categories` | array | Yes | Array of category objects. Each must have a `name` (string). Optional `description` (string) guides the model. Min 1, max 20 categories. |
+| `rubric.multi_label` | boolean | No | Return all matching categories above the threshold. Default: `false` (single best match). |
+| `rubric.confidence_threshold` | number | No | Minimum confidence (0-1) for a category to be included. Default: `0.5`. |
### Example request
```bash cURL
-curl -X POST https://api.trynawa.com/v1/rubric/classify \
+curl -X POST https://api.trynawa.com/v1/rubric-classify \
-H "Authorization: Bearer nawa_test_sk_xxx" \
-H "Content-Type: application/json" \
-d '{
"text": "وين الترجمة العربية؟ مافهمت شي",
"rubric": {
- "categories": ["translation_request", "technical_issue", "content_feedback", "off_topic"],
- "descriptions": {
- "translation_request": "User is asking for subtitles or translation",
- "technical_issue": "Audio, video, or playback problems",
- "content_feedback": "Comments about the content quality",
- "off_topic": "Not related to the video"
- }
- },
- "platform": "youtube"
+ "categories": [
+ {"name": "translation_request", "description": "User is asking for subtitles or translation"},
+ {"name": "technical_issue", "description": "Audio, video, or playback problems"},
+ {"name": "content_feedback", "description": "Comments about the content quality"},
+ {"name": "off_topic", "description": "Not related to the video"}
+ ],
+ "multi_label": false,
+ "confidence_threshold": 0.5
+ }
}'
```
```typescript TypeScript
+import { Nawa } from '@nawalabs/sdk'
+
+const nawa = new Nawa({ apiKey: process.env.NAWA_API_KEY })
+
const { data, error } = await nawa.rubric.classify({
text: 'وين الترجمة العربية؟ مافهمت شي',
rubric: {
- categories: ['translation_request', 'technical_issue', 'content_feedback', 'off_topic'],
- descriptions: {
- translation_request: 'User is asking for subtitles or translation',
- technical_issue: 'Audio, video, or playback problems',
- content_feedback: 'Comments about the content quality',
- off_topic: 'Not related to the video'
- }
- },
- platform: 'youtube'
+ categories: [
+ { name: 'translation_request', description: 'User is asking for subtitles or translation' },
+ { name: 'technical_issue', description: 'Audio, video, or playback problems' },
+ { name: 'content_feedback', description: 'Comments about the content quality' },
+ { name: 'off_topic', description: 'Not related to the video' }
+ ],
+ multiLabel: false,
+ confidenceThreshold: 0.5
+ }
})
```
```python Python
+from nawa import Nawa
+
+nawa = Nawa(api_key="your_api_key")
+
result = nawa.rubric.classify(
text="وين الترجمة العربية؟ مافهمت شي",
rubric={
- "categories": ["translation_request", "technical_issue", "content_feedback", "off_topic"],
- "descriptions": {
- "translation_request": "User is asking for subtitles or translation",
- "technical_issue": "Audio, video, or playback problems",
- "content_feedback": "Comments about the content quality",
- "off_topic": "Not related to the video",
- },
+ "categories": [
+ {"name": "translation_request", "description": "User is asking for subtitles or translation"},
+ {"name": "technical_issue", "description": "Audio, video, or playback problems"},
+ {"name": "content_feedback", "description": "Comments about the content quality"},
+ {"name": "off_topic", "description": "Not related to the video"},
+ ],
+ "multi_label": False,
+ "confidence_threshold": 0.5,
},
- platform="youtube",
)
```
@@ -88,34 +109,83 @@ result = nawa.rubric.classify(
{
"success": true,
"result": {
- "text": "وين الترجمة العربية؟ مافهمت شي",
- "category": "translation_request",
- "category_confidence": 0.94,
- "scores": {
- "translation_request": 0.94,
- "technical_issue": 0.03,
- "content_feedback": 0.02,
- "off_topic": 0.01
- },
- "dialect": "gulf",
- "dialect_confidence": 0.91,
+ "id": "rcl_nw_a1b2c3d4e5f6",
+ "object": "rubric_classification",
+ "categories": [
+ {"name": "translation_request", "confidence": 0.94}
+ ],
+ "multi_label": false,
"language": "ar",
- "model": "nagl-v1",
- "cached": false
+ "provider": "allam",
+ "model": "sdaia/allam-1-13b-instruct",
+ "fallback_used": false,
+ "cost_usd": 0.003,
+ "credits_used": 3
},
"errors": [],
- "request_id": "req_rub123abc456"
+ "request_id": "req_nw_rub123abc456abcdef12"
}
```
+### Response headers
+
+| Header | Description |
+|--------|-------------|
+| `X-Request-Id` | Unique request identifier (`req_nw_xxx` format) |
+| `X-RateLimit-Limit` | Rate limit ceiling for current window |
+| `X-RateLimit-Remaining` | Requests remaining in current window |
+| `X-RateLimit-Reset` | Window reset time (RFC 3339) |
+| `X-NAWA-Provider` | AI provider used: `claude`, `gemini`, or `allam` |
+| `X-NAWA-Latency` | Processing time in milliseconds |
+
### Result fields
| Field | Type | Description |
|-------|------|-------------|
-| `category` | string | The top matching category from your rubric |
-| `category_confidence` | number | Confidence score (0–1) for the top category |
-| `scores` | object | Confidence scores for all rubric categories |
-| `dialect` | string | Detected Arabic dialect |
-| `dialect_confidence` | number | Dialect confidence score (0–1) |
-| `language` | string | Detected language code |
-| `cached` | boolean | Whether served from semantic cache |
+| `id` | string | Classification ID (`rcl_nw_xxx` format) |
+| `object` | string | Always `rubric_classification` |
+| `categories` | array | Array of matched categories, each with `name` (string) and `confidence` (number, 0-1) |
+| `multi_label` | boolean | Whether multi-label mode was used |
+| `language` | string | Detected language: `ar`, `en`, or `mixed` |
+| `provider` | string | AI provider used |
+| `model` | string | Model version used |
+| `fallback_used` | boolean | Whether a fallback provider was used |
+| `cost_usd` | number | Cost in USD |
+| `credits_used` | integer | Credits deducted |
+
+### Multi-label example
+
+When `multi_label` is `true`, the response includes all categories above the confidence threshold:
+
+```json
+{
+ "success": true,
+ "result": {
+ "id": "rcl_nw_x9y8z7w6v5u4",
+ "object": "rubric_classification",
+ "categories": [
+ {"name": "translation_request", "confidence": 0.88},
+ {"name": "content_feedback", "confidence": 0.62}
+ ],
+ "multi_label": true,
+ "language": "ar",
+ "provider": "claude",
+ "model": "claude-haiku-4-5-20251001",
+ "fallback_used": false,
+ "cost_usd": 0.003,
+ "credits_used": 3
+ },
+ "errors": [],
+ "request_id": "req_nw_multi_abc123def45612"
+}
+```
+
+### Error responses
+
+| Status | Type | When |
+|--------|------|------|
+| 400 | `invalid_request_error` | Missing `text` or `rubric`, empty categories, more than 20 categories, invalid `provider` |
+| 401 | `authentication_error` | Invalid or missing API key |
+| 402 | `permission_error` | Insufficient credits |
+| 429 | `rate_limit_error` | Per-key or global rate limit exceeded |
+| 500 | `api_error` | Internal or provider error |
diff --git a/errors.mdx b/errors.mdx
index fdcf365..d868a68 100644
--- a/errors.mdx
+++ b/errors.mdx
@@ -10,20 +10,17 @@ Every NAWA API error returns a consistent JSON envelope with machine-readable co
```json
{
- "success": false,
- "result": null,
- "errors": [
- {
- "type": "invalid_request_error",
- "code": "missing_required_param",
- "message": "The 'text' parameter is required.",
- "display_message": "Please provide the comment text to classify.",
- "param": "text",
- "doc_url": "https://developers.trynawa.com/errors#missing_required_param",
- "suggested_action": "Include the 'text' field in your request body."
- }
- ],
- "request_id": "req_abc123def456"
+ "type": "error",
+ "error": {
+ "type": "invalid_request_error",
+ "code": "missing_field",
+ "message": "`text` is required",
+ "display_message": "The required field `text` is missing.",
+ "param": "text",
+ "doc_url": "https://developers.trynawa.com/errors#missing-field",
+ "suggested_action": "Include `text` in your request body."
+ },
+ "request_id": "req_nw_a1b2c3d4e5f67890abcdef12"
}
```
@@ -31,11 +28,11 @@ Every NAWA API error returns a consistent JSON envelope with machine-readable co
| Field | Type | Description |
|-------|------|-------------|
-| `type` | string | Error category (see taxonomy below) |
+| `type` | string | Error category (see taxonomy below): `authentication_error`, `permission_error`, `rate_limit_error`, `invalid_request_error`, `not_found_error`, `api_error` |
| `code` | string | Machine-readable error code |
| `message` | string | Technical description for developers |
| `display_message` | string | User-safe message suitable for end users |
-| `param` | string \| null | The parameter that caused the error, if applicable |
+| `param` | string or null | The parameter that caused the error, if applicable |
| `doc_url` | string | Link to documentation for this specific error |
| `suggested_action` | string | What to do to fix the error |
@@ -175,22 +172,25 @@ The API key is missing, invalid, expired, or revoked.
-### `insufficient_credits` (402)
+### `permission_error` (402)
Your account balance is insufficient for the requested operation.
-
- **Cause:** Your credit balance has reached $0. There is no grace period -- paid endpoints stop immediately.
+
+ **Cause:** Your credit balance has reached $0. Paid endpoints stop immediately.
- **Fix:** Purchase a credit pack from the dashboard to restore access instantly.
+ **Fix:** Purchase credits from the dashboard to restore access.
```json
{
- "type": "insufficient_credits",
- "code": "balance_exhausted",
- "message": "Credit balance exhausted. Purchase credits to continue.",
- "suggested_action": "Buy a credit pack at trynawa.com/developers/keys."
+ "type": "permission_error",
+ "code": "insufficient_credits",
+ "message": "Credit balance is $0.00",
+ "display_message": "Your account has insufficient credits.",
+ "param": null,
+ "doc_url": "https://developers.trynawa.com/billing",
+ "suggested_action": "Purchase credits at https://trynawa.com/billing"
}
```
@@ -198,11 +198,11 @@ Your account balance is insufficient for the requested operation.
### `rate_limit_error` (429)
-You've exceeded the rate limit for your current tier.
+You have exceeded a rate limit. The `Retry-After` header tells you how many seconds to wait. The `X-NAWA-RateLimit-Reason` header indicates which limit was hit.
-
- **Cause:** Too many requests in the current time window.
+
+ **Cause:** Too many requests from this API key in the current time window.
**Fix:** Wait until the `X-RateLimit-Reset` time, then retry. Consider upgrading your tier.
@@ -210,27 +210,55 @@ You've exceeded the rate limit for your current tier.
{
"type": "rate_limit_error",
"code": "rate_limit_exceeded",
- "message": "Rate limit exceeded: 120 requests per minute for Growth tier.",
- "suggested_action": "Wait until the rate limit resets or upgrade your tier."
+ "message": "Rate limit exceeded: minute_limit",
+ "display_message": "You have exceeded the request rate limit for your plan.",
+ "param": null,
+ "doc_url": "https://developers.trynawa.com/rate-limits",
+ "suggested_action": "Wait 45s and retry, or upgrade your plan for higher limits."
}
```
- The `X-NAWA-RateLimit-Reason` response header provides additional context (`minute_limit` or `sandbox_exhausted`).
+ The `X-NAWA-RateLimit-Reason` header will be `minute_limit`.
+
+
+
+ **Cause:** Aggregate AI-bound traffic across all users has exceeded the global capacity cap. This is temporary and not specific to your key.
+
+ **Fix:** Wait the number of seconds in the `Retry-After` header, then retry.
+
+ ```json
+ {
+ "type": "rate_limit_error",
+ "code": "rate_limit_exceeded",
+ "message": "Global AI rate limit exceeded (900 RPM). Retry in 12s.",
+ "display_message": "The API is experiencing high traffic. Please retry shortly.",
+ "param": null,
+ "doc_url": "https://developers.trynawa.com/rate-limits#global-ai-cap",
+ "suggested_action": "Wait for the Retry-After duration and retry."
+ }
+ ```
+
+ The `X-NAWA-RateLimit-Reason` header will be `global_ai_cap`.
- **Cause:** You've used all 100 lifetime requests on your free key.
+ **Cause:** You have used all 100 lifetime requests on your free key.
- **Fix:** Buy credits to unlock a live key.
+ **Fix:** Purchase credits to unlock a live key.
```json
{
"type": "rate_limit_error",
"code": "sandbox_exhausted",
"message": "Free key lifetime limit of 100 requests reached.",
+ "display_message": "Your free API key has reached its lifetime limit.",
+ "param": null,
+ "doc_url": "https://developers.trynawa.com/billing",
"suggested_action": "Buy credits to create a live API key at trynawa.com/developers/keys."
}
```
+
+ The `X-NAWA-RateLimit-Reason` header will be `sandbox_exhausted`.
diff --git a/openapi.yaml b/openapi.yaml
index 17706f2..a42145c 100644
--- a/openapi.yaml
+++ b/openapi.yaml
@@ -457,7 +457,7 @@ paths:
"500":
$ref: "#/components/responses/InternalError"
- /rubric/classify:
+ /rubric-classify:
post:
operationId: rubricClassify
summary: Classify with custom rubric
@@ -567,7 +567,7 @@ paths:
"500":
$ref: "#/components/responses/InternalError"
- /comments/{id}/reply:
+ /comments/reply:
post:
operationId: generateReply
summary: Generate reply to comment
@@ -579,25 +579,31 @@ paths:
x-nawa-cache: true
x-nawa-free: false
parameters:
- - name: id
- in: path
- required: true
- description: The comment ID to reply to
+ - name: provider
+ in: query
+ required: false
+ description: Force a specific AI provider (for A/B testing)
schema:
type: string
- examples:
- - "cmt_abc123"
+ enum: [claude, gemini, allam]
requestBody:
- required: false
+ required: true
content:
application/json:
schema:
type: object
+ required: [text]
properties:
+ text:
+ type: string
+ minLength: 1
+ description: Comment text to classify and reply to
+ examples:
+ - "هذا المنتج سيء جداً ولا أنصح به"
tone:
type: string
enum: [friendly, professional, casual, formal]
- default: friendly
+ default: professional
description: Reply tone
max_length:
type: integer
@@ -605,17 +611,10 @@ paths:
maximum: 2000
default: 500
description: Maximum reply length in characters
- context:
- type: string
- description: Additional context about the channel or video
- language:
- type: string
- enum: [ar, en, auto]
- default: auto
- description: "Force reply language. Default: auto (matches commenter's language)."
example:
- tone: "friendly"
- context: "Tech review channel focused on smartphones"
+ text: "هذا المنتج سيء جداً ولا أنصح به"
+ tone: "professional"
+ max_length: 500
responses:
"200":
description: Generated reply
@@ -996,17 +995,17 @@ paths:
- name: from
in: query
required: false
+ description: Start date (ISO 8601). Default: start of current month.
schema:
type: string
format: date-time
- description: Start date (ISO 8601). Default: start of current month.
- name: to
in: query
required: false
+ description: End date (ISO 8601). Default: now.
schema:
type: string
format: date-time
- description: End date (ISO 8601). Default: now.
- name: group_by
in: query
required: false
diff --git a/rate-limits.mdx b/rate-limits.mdx
index 26c3d15..fb06359 100644
--- a/rate-limits.mdx
+++ b/rate-limits.mdx
@@ -1,24 +1,35 @@
---
title: "Rate Limits"
sidebarTitle: "Rate Limits"
-description: "Rate limit tiers, response headers, and strategies for handling 429 errors gracefully."
+description: "Rate limit tiers, response headers, the global AI cap, and strategies for handling 429 errors gracefully."
---
-NAWA enforces per-minute rate limits on each API key to ensure fair usage and platform stability.
+NAWA enforces per-key rate limits on each API key to ensure fair usage and platform stability. A separate global AI cap protects against upstream provider limits.
-## Tier table
+## Per-key rate limit tiers
| Tier | Requests/min | How you get it |
|------|-------------|----------------|
-| **Free** | 10 | Free keys (`nawa_test_sk_`) |
+| **Sandbox** | 10 | Free keys (`nawa_test_sk_`) -- 100 lifetime request cap |
+| **Trial** | 60 | Trial keys with credits |
| **Growth** | 120 | Live keys (`nawa_live_sk_`) with credits |
| **Enterprise** | 300 | Contact [sales@trynawa.com](mailto:sales@trynawa.com) |
| **Enterprise+** | 1,000 | Contact [sales@trynawa.com](mailto:sales@trynawa.com) |
- Free keys are rate-limited to 10 requests/minute and have a hard cap of 100 lifetime requests. Live keys start at the Growth tier (120/min). Enterprise tiers are available on request -- contact [sales@trynawa.com](mailto:sales@trynawa.com).
+ Sandbox keys are rate-limited to 10 requests/minute and have a hard cap of 100 lifetime requests. Live keys start at the Growth tier (120/min). Enterprise tiers are available on request.
+## Global AI cap
+
+In addition to per-key limits, NAWA enforces a global rate cap on AI-bound requests across all users. This prevents aggregate traffic from exceeding the upstream AI provider ceiling.
+
+- **Default cap:** 900 requests per minute (sliding window)
+- **Scope:** All requests that invoke an AI provider (classify, rubric-classify, comments/reply, translate, moderate)
+- **Fail-open:** If the rate-limit infrastructure is unavailable, requests pass through normally
+
+When the global cap is hit, you receive a `429` response with a `Retry-After` header indicating when capacity will be available. This is distinct from per-key rate limiting and affects all users simultaneously during high-traffic periods.
+
## Rate limit headers
Every API response includes rate limit headers:
@@ -27,19 +38,19 @@ Every API response includes rate limit headers:
|--------|-------------|---------|
| `X-RateLimit-Limit` | Maximum requests allowed per minute for your tier | `120` |
| `X-RateLimit-Remaining` | Requests remaining in the current window | `42` |
-| `X-RateLimit-Reset` | When the current window resets (RFC 3339) | `2025-01-15T12:01:00Z` |
+| `X-RateLimit-Reset` | When the current window resets (RFC 3339) | `2026-04-11T12:01:00Z` |
On `429` responses, additional headers are included:
| Header | Description | Example |
|--------|-------------|---------|
| `Retry-After` | Seconds to wait before retrying | `8` |
-| `X-NAWA-RateLimit-Reason` | Which limit was hit | `minute_limit` or `sandbox_exhausted` |
+| `X-NAWA-RateLimit-Reason` | Which limit was hit | `minute_limit`, `sandbox_exhausted`, or `global_ai_cap` |
## Semantic cache and rate limits
- Semantic cache hits (`X-NAWA-Cache: HIT`) do **not** count toward your rate limits. If you're classifying similar comments repeatedly, caching effectively increases your throughput.
+ Semantic cache hits (`X-NAWA-Cache: HIT`) do **not** count toward your rate limits. If you are classifying similar comments repeatedly, caching effectively increases your throughput.
## Handling 429 errors
@@ -76,9 +87,9 @@ import time
import random
from nawa import Nawa
-def classify_with_retry(nawa: Nawa, text: str, platform: str, max_retries: int = 3):
+def classify_with_retry(nawa: Nawa, text: str, max_retries: int = 3):
for attempt in range(max_retries + 1):
- result = nawa.classify(text=text, platform=platform)
+ result = nawa.classify(text=text)
if not result.error:
return result.data
@@ -92,6 +103,17 @@ def classify_with_retry(nawa: Nawa, text: str, platform: str, max_retries: int =
raise Exception(result.error.message)
```
+```bash cURL
+# Check the Retry-After header and wait before retrying
+curl -s -o /dev/null -w "%{http_code}" \
+ -H "Authorization: Bearer nawa_live_sk_xxx" \
+ -H "Content-Type: application/json" \
+ -d '{"text": "test"}' \
+ https://api.trynawa.com/v1/classify
+
+# On 429, read the Retry-After header value and sleep that many seconds
+```
+
diff --git a/webhooks.mdx b/webhooks.mdx
index 2ef4922..7eea37b 100644
--- a/webhooks.mdx
+++ b/webhooks.mdx
@@ -12,8 +12,6 @@ Receive real-time notifications when events occur in your NAWA account. Webhooks
|-------|-------------|---------|
| `classification.completed` | A classification request succeeded | After `/v1/classify` completes |
| `classification.failed` | A classification request failed | On provider or internal errors |
-| `comment.new` | A new comment was ingested | When a connected platform receives a comment |
-| `comment.replied` | A reply was posted | After `/v1/comments/:id/reply` succeeds |
| `credits.low` | Credit balance below threshold | Balance drops below $5 |
| `credits.exhausted` | Credit balance is $0 | Balance reaches $0 |