Async batch transcription and document text extraction. Authenticate with a Bearer rtk_ key.

Authentication

Pass your API key as a Bearer token on every request.

Authorization: Bearer rtk_…
  • Keys are scoped to a single organization: every request, upload, job and quota check stays inside that organization — a key can never see another organization's data.
  • A revoked key stops authenticating immediately, with no grace period — the next request made with it gets a 401.
  • If your organization is suspended or permanently deleted, every key on it stops working immediately too, even though the key record itself is untouched.
  • GET /languages and GET /openapi.json are the two exceptions — public reference endpoints that need no key at all.

Quotas and credits

Every service draws from one shared credit pool per organization — there's no separate quota per service.

  • A transcription job is charged one credit per second of audio, rounded up.
  • A document job is charged OCR_CREDITS_PER_PAGE credits per page, deducted once the page count is known — at completion, not at submit time.

The submit-time check is loose

Submitting a job only checks that your organization has credits remaining, not that the job's eventual cost fits inside what's left. A large job can push your balance negative before it finishes — it is never rejected up front for being too expensive to complete.

View your balance and full ledger

Errors

Every error response has the same shape:

{ "error": { "code": "...", "message": "..." } }

Codes returned by the endpoints below:

CodeStatusMeaning
UNAUTHORIZED401Missing, malformed, unknown or revoked API key — or the organization behind it is suspended or deleted.
RATE_LIMITED429You've exceeded the rate limit below.
FORBIDDEN403The audio_token or document_token you supplied belongs to a different organization.
INVALID_REQUEST400The request body failed validation.
QUOTA_EXCEEDED402Your organization has no credits remaining.
JOB_NOT_FOUND404No job with that id exists for your organization.
JOB_NOT_READY409The job hasn't reached a completed state yet.
FORMAT_NOT_AVAILABLE404That result format wasn't requested when the job was submitted.
DOCUMENT_TOO_LARGE413The document exceeds the configured size limit.
DOCUMENT_NOT_FOUND404The uploaded document object could not be found.
INVALID_SOURCE400The document or audio source could not be fetched.
UNSUPPORTED_FORMAT415The document extraction service rejected the file format.
TOO_MANY_PAGES413The document exceeds the extraction service's page limit.
ENCRYPTED_PDF422The PDF is password-protected and can't be processed.
OCR_UNAVAILABLE503The document extraction service is temporarily unavailable.

Rate limits

Enforced per API key, not per organization. Design against 60 requests per minute per key, refilling at about one request per second, so a key that hasn't been used recently can burst up to 60 requests at once. A request over the limit gets 429 RATE_LIMITED with a Retry-After header giving the number of seconds to wait.

Treat 429 and Retry-After as authoritative rather than tracking an exact budget yourself. The limiter currently holds its counters in memory per running instance, so the figure above is not a hard guarantee: counters reset when the service is deployed or restarted, and a horizontally scaled deployment enforces the allowance per instance rather than globally.

Webhooks

Set callback_url when you submit a job and rafiq-api POSTs the result to it once the job reaches a terminal state, so you don't have to poll GET .../{id}.

The payload shape depends on the job type and outcome:

// Transcription — completed
{ "id": "…", "status": "completed", "transcript": { ... } }

// Transcription — failed
{ "id": "…", "status": "failed", "error": { "code": "...", "message": "..." } }

// Document — completed
{ "id": "…", "status": "completed", "pages": 12, "formats": ["md"] }

// Document — failed
{ "id": "…", "status": "failed", "error": { "code": "...", "message": "..." } }

A completed document job's payload carries only the page count and formats, never the extracted text — fetch that separately with GET /api/v1/documents/{id}?format=md|txt|json.

A signed delivery carries an X-Rafiq-Signature header: the HMAC-SHA256 hex digest of the exact raw request body, computed with your organization's webhook signing secret.

X-Rafiq-Signature: <hex hmac-sha256>

Verify against the exact bytes of the body as received. Parsing the JSON and re-serializing it before verifying produces a different string and will fail intermittently.

Deliveries with no signing secret configured

If your organization has no signing secret, the delivery still goes out — just without the X-Rafiq-Signature header, never signed with an empty key. A signature computed from an empty secret is something anyone could reproduce, so no header is a more honest signal than a fake one: treat an unsigned delivery as unverified.

Delivery is retried up to 3 times, waiting 1 second then 2 seconds between attempts, if your endpoint doesn't return a successful response. After the last attempt fails, rafiq-api stops trying.

Manage your signing secret in Settings → Webhooks

Verify a delivery (Node.js)

const crypto = require("crypto");

// rawBody must be the exact, unparsed request body — the same bytes rafiq-api
// signed. Verifying JSON.parse(rawBody) re-serialized will fail intermittently:
// key order and whitespace are not guaranteed to round-trip identically.
function isValidSignature(rawBody, signatureHeader, secret) {
  const expected = crypto
    .createHmac("sha256", secret)
    .update(rawBody)
    .digest("hex");

  return (
    signatureHeader.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signatureHeader), Buffer.from(expected))
  );
}

Endpoints

Getting started

post/api/v1/upload-url

Request a pre-signed upload URL

Request bodyrequired

  • content_typestring

Responses

  • 200Upload URL plus the object token, returned as both `token` (generic) and `audio_token` (legacy alias).
  • 401Unauthorized
  • 429Rate limited
curl -X POST "/api/v1/upload-url" \
  -H "Authorization: Bearer rtk_…" \
  -H "Content-Type: application/json" \
  -d '{
  "content_type": "string"
}'

Speech to text

post/api/v1/transcripts

Create a transcription job

Request bodyrequired

  • audio_tokenstring
  • audio_urlstring <uri>
  • languagestring · one of: id, en · default: id
  • callback_urlstring <uri>

Responses

  • 201Job accepted (status: pending)
  • 400Invalid request
  • 401Unauthorized
  • 402Quota exceeded
  • 429Rate limited
curl -X POST "/api/v1/transcripts" \
  -H "Authorization: Bearer rtk_…" \
  -H "Content-Type: application/json" \
  -d '{
  "audio_token": "string",
  "audio_url": "https://example.com/file",
  "language": "id",
  "callback_url": "https://example.com/file"
}'
get/api/v1/transcripts

List transcription jobs

Parameters

  • limitquery · integer
  • cursorquery · string

Responses

  • 200Paginated job list
curl -X GET "/api/v1/transcripts?limit=1&cursor=string" \
  -H "Authorization: Bearer rtk_…"
get/api/v1/transcripts/{id}

Poll a transcription job

Parameters

  • idpath · string · required

Responses

  • 200Job with status and (when completed) transcript
  • 404Job not found
curl -X GET "/api/v1/transcripts/id_123" \
  -H "Authorization: Bearer rtk_…"
delete/api/v1/transcripts/{id}

Delete a transcription job and its audio

Parameters

  • idpath · string · required

Responses

  • 200Deleted
  • 404Job not found
curl -X DELETE "/api/v1/transcripts/id_123" \
  -H "Authorization: Bearer rtk_…"
get/api/v1/languages

List supported transcription languages

Responses

  • 200Supported languages
curl -X GET "/api/v1/languages"

Documents

post/api/v1/documents

Create a document extraction job

Request bodyrequired

  • document_tokenstring
  • document_urlstring <uri>
  • formatsarray · default: md
  • langstring · default: id,en
  • force_ocrboolean · default: false
  • callback_urlstring <uri>

Responses

  • 201Job accepted (status: processing)
  • 400Invalid request or source
  • 401Unauthorized
  • 402Quota exceeded
  • 403document_token belongs to another organization
  • 404Uploaded document object not found
  • 413Document too large or too many pages
  • 415Unsupported document format
  • 422Encrypted PDF
  • 429Rate limited
  • 503Extraction service unavailable
curl -X POST "/api/v1/documents" \
  -H "Authorization: Bearer rtk_…" \
  -H "Content-Type: application/json" \
  -d '{
  "document_token": "string",
  "document_url": "https://example.com/file",
  "formats": [
    "md"
  ],
  "lang": "id,en",
  "force_ocr": false,
  "callback_url": "https://example.com/file"
}'
get/api/v1/documents

List document extraction jobs

Parameters

  • limitquery · integer
  • cursorquery · string

Responses

  • 200Paginated job list
curl -X GET "/api/v1/documents?limit=1&cursor=string" \
  -H "Authorization: Bearer rtk_…"
get/api/v1/documents/{id}

Poll a document job, or fetch its extracted text

Parameters

  • idpath · string · required
  • formatquery · string

Responses

  • 200Job status, or the extracted text
  • 404Job not found, or format not requested
  • 409Job is not completed yet
curl -X GET "/api/v1/documents/id_123?format=md" \
  -H "Authorization: Bearer rtk_…"
delete/api/v1/documents/{id}

Delete a document job and its stored file

Parameters

  • idpath · string · required

Responses

  • 200Deleted
  • 404Job not found
curl -X DELETE "/api/v1/documents/id_123" \
  -H "Authorization: Bearer rtk_…"