Advanced toolsNabr — Recitation Corrector

Offline (audio file)

Grade a recorded Quran recitation in one HTTP request and get memorization, tajweed, and phonetic mistakes for a range of verses.

Description

The offline endpoint grades a recitation that is already recorded. Send the URL of the audio file and the verses it covers, and the response holds every confirmed word and every mistake in one result. It needs the same Nabr subscription and API key as the streaming WebSocket.

Use it when the recording exists before grading starts, for example homework a student uploads. Use the streaming WebSocket for live feedback while the user recites.

Endpoint

POST https://api.qurani.ai/gw/qrc/offline
X-Api-Key: <YOUR_API_KEY>
Accept-Language: ar
Content-Type: application/json
HeaderDefaultDescription
Accept-LanguagearLanguage of the message texts: ar, en, es, tr, id, or ur. Omit it for Arabic

Request body

FieldTypeRequiredDescription
audio_urlstringYesPublic http(s) URL of the recording. The server downloads it, so it must be reachable without authentication
chapter_indexintegerYesSurah number, 1–114
start_verse_indexintegerYesFirst verse of the recitation, from 1
end_verse_indexintegerYesLast verse of the recitation. Must not be before start_verse_index
hafz_levelintegerNo1–3, default 1. Same as in streaming
tajweed_levelintegerNo1–5, default 1. Level 1 adds tajweed and phonetic mistakes, see tajweed level

Grading can take a while for long recordings. Set your HTTP client timeout to a few minutes.

Privacy mode

The Help improve Nabr setting does not apply to this endpoint. Recordings sent to it are not used to improve the Nabr AI model.

Usage example

curl -X POST 'https://api.qurani.ai/gw/qrc/offline' \
  -H 'Content-Type: application/json' \
  -H 'X-Api-Key: <YOUR_API_KEY>' \
  -H 'Accept-Language: ar' \
  --max-time 300 \
  -d '{
    "audio_url": "<YOUR_AUDIO_URL>",
    "chapter_index": 107,
    "start_verse_index": 2,
    "end_verse_index": 4,
    "hafz_level": 1,
    "tajweed_level": 1
  }'
const response = await fetch("https://api.qurani.ai/gw/qrc/offline", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-Api-Key": "<YOUR_API_KEY>",
    "Accept-Language": "ar",
  },
  body: JSON.stringify({
    audio_url: "<YOUR_AUDIO_URL>",
    chapter_index: 107,
    start_verse_index: 2,
    end_verse_index: 4,
  }),
});

if (!response.ok) {
  throw new Error(`${response.status}: ${await response.text()}`);
}

const result: GradeAudioFileResult = await response.json();

Response

Example response:

{
  "exit_code": 6,
  "audio_duration_ms": 8254,
  "chapter_index": 107,
  "verse_index": 4,
  "word_index": 2,
  "confirmed_words": [
    {
      "chapter": 107,
      "verse": 2,
      "word": 2
    },
    {
      "chapter": 107,
      "verse": 2,
      "word": 3
    },
    {
      "chapter": 107,
      "verse": 2,
      "word": 4
    },
    {
      "chapter": 107,
      "verse": 3,
      "word": 1
    },
    {
      "chapter": 107,
      "verse": 3,
      "word": 2
    },
    {
      "chapter": 107,
      "verse": 3,
      "word": 3
    },
    {
      "chapter": 107,
      "verse": 3,
      "word": 4
    },
    {
      "chapter": 107,
      "verse": 3,
      "word": 5
    },
    {
      "chapter": 107,
      "verse": 4,
      "word": 1
    },
    {
      "chapter": 107,
      "verse": 4,
      "word": 2
    }
  ],
  "skipped_words": [],
  "tajweed_mistakes": [],
  "letter_mistakes": [
    {
      "chapter": 107,
      "verse": 2,
      "word": 3,
      "letter": 1,
      "speech_like": "يَدُعُّ",
      "message": "لايمكن لفظ (حركة الفتح في حرف ي) في كلمة (يَدُعُّ) كـ (ضمة)",
      "error_code": "letters_mismatch"
    },
    {
      "chapter": 107,
      "verse": 4,
      "word": 2,
      "letter": 0,
      "speech_like": "لِّلْمُصَلِّينَ",
      "message": "الحرف ل من الكلمة لِّلْمُصَلِّينَ مشدد",
      "error_code": "tadeef_missed"
    }
  ],
  "substituted_words": [
    {
      "chapter": 107,
      "verse": 2,
      "word": 1,
      "said": {
        "chapter": 52,
        "verse": 47,
        "word": 6
      },
      "speech_like": "ذَلِكَ",
      "message": "قرأتَ (ذَٰلِكَ) بدل (فَذَٰلِكَ)"
    }
  ]
}

The result is final: there are no pending words, and every word is graded before the response is sent.

  • confirmed_words lists every word matched in the recording. A word in substituted_words is also in confirmed_words, so mark it as wrong.
  • verse_index and word_index give the last confirmed word.
  • exit_code 6 means the check succeeded. See exit codes for the others.

The fields of each list, and every error_code, are the same as in streaming:

Types

type GradeAudioFileRequest = {
  audio_url: string;
  chapter_index: number; // 1–114
  start_verse_index: number;
  end_verse_index: number;
  hafz_level?: number; // 1–3, default 1
  tajweed_level?: number; // 1–5, default 1
};

type QRCWord = {
  chapter: number;
  verse: number;
  word: number;
};

type Mistake = {
  chapter: number;
  verse: number;
  word: number;
  letter: number; // zero-based index in the word's phonemes, not its Arabic characters
  speech_like: string; // the word with tashkeel
  message: string; // localized tip; some include the reason
  error_code?: string | null; // stable id, e.g. "tadeef_extra"
};

type SubstitutedWord = {
  chapter: number; // the expected word
  verse: number;
  word: number;
  said: QRCWord | null; // the Quran word read instead; null if not a Quran word
  speech_like: string; // what was heard, in Arabic letters
  message: string;
};

type GradeAudioFileResult = {
  exit_code: number; // 0–8; 6 = success
  audio_duration_ms: number; // decoded audio length
  chapter_index: number;
  verse_index: number;
  word_index: number;
  confirmed_words: QRCWord[];
  skipped_words: QRCWord[];
  substituted_words: SubstitutedWord[];
  tajweed_mistakes: Mistake[];
  letter_mistakes: Mistake[];
};

Errors

Errors from the grader have a detail object with a stable code and a message:

{
  "detail": {
    "message": "end_verse_index is before start_verse_index",
    "code": "invalid_verse_range"
  }
}

A body that fails schema validation, such as a missing field, has a detail list with one entry per invalid field instead.

StatusMeaningWhat to do
400The X-Api-Key header is missing or the key is invalid. Also sent when audio_url could not be downloaded (audio_fetch_failed)Check the key, or that the URL is public and reachable
401The API key has expiredGenerate a new key from your dashboard
402The subscription has expired or all units are usedRenew or upgrade your plan
422The body is invalid, for example invalid_verse_rangeFix the request. Do not resend it unchanged
503Too many recordings are being graded at onceRetry after a few seconds
502The grader could not be reachedRetry. Contact support if it repeats