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 |