Partner API · v1 · read-only

BerQuran Partner API

Let your organization's systems, such as an HR app, read the daily Quran reading activity of members in the groups you coordinate. Only members who say yes are ever included.

The documentation is open to everyone. A key requires signing in and an approved application.

Getting access

Keys are tied to a BerQuran account that coordinates the groups being read. There is no self-serve signup for an API key: every application is read and approved by a person, because what you receive is permission to read people's worship records.

  1. Sign in and coordinate a group. Create a group in the app or on the web, or ask its owner to make you a coordinator.
  2. Mark it as an organization group. In the Partner API console, switch the group to Organization group and share the invite link. Each member then decides for themselves whether to consent to reporting.
  3. Apply. Tell us your organization, a contact, what the data is for, and which groups the key may read.
  4. Get your key. Once approved, create a key in the console. It looks like bqk_live_1a2b3c4d_… and is shown once; we only keep a hash of it.
Access follows the coordinator. If you stop coordinating a group, the key loses that group on the very next request, with no action needed from anyone.

Quota & cost

The API is free, with no paid tier. BerQuran is run as a service to the ummah and is never sold, so there is nothing to buy here.

Need more?
Ask in your application
  • Tell us how many members and how often you sync
  • Quota is set per application, by need
  • Never by payment

Privacy & consent

Three checks run again on every request, never cached:

  • the key's owner still coordinates the group;
  • the owner granted that group to this key;
  • the member consented to organization reporting.

Members who have not consented are left out entirely, not shown with zeros. Tartil Mode scores need a second, separate consent. The API never returns:

  • audio recordings
  • tajwid results per word
  • phone numbers or email addresses
  • internal BerQuran user IDs
  • anyone outside the groups you granted

Members are identified by member_ref: a stable, opaque ID that is different for every partner, so two organizations cannot match the same person.

Authentication

Base URL https://api.berquran.com/v1. Send your key in either header; both are equivalent.

shell
curl https://api.berquran.com/v1/me \
  -H "Authorization: Bearer bqk_live_1a2b3c4d_xxxxxxxxxxxxxxxx"

# or
curl https://api.berquran.com/v1/me -H "X-API-Key: bqk_live_1a2b3c4d_xxxxxxxxxxxxxxxx"

Keep keys on your server, never in a browser or mobile app. If a key leaks, revoke it in the console and create a new one; group access belongs to your application, not the key, so nothing else changes.

Rate limits & quota

Every authenticated response carries your current limits:

X-RateLimit-LimitRequests allowed per minute
X-Quota-LimitRequests allowed per day
X-Quota-RemainingRequests left today

Over the limit you get 429 with a Retry-After header in seconds. The daily quota resets at midnight Western Indonesian Time (UTC+7). Rejected requests also count toward the quota, so back off rather than retry in a loop.

Conventions

  • All endpoints are GET and return JSON.
  • Lists come in one envelope: data, next_cursor and meta. When next_cursor is not null, pass it as ?cursor= to get the next page.
  • Date ranges use from and to (inclusive, YYYY-MM-DD). The default is the last 30 days, the maximum 92.
  • Timestamps are ISO 8601 with a time zone.
  • Errors have a stable machine code. Match on error.code; the human-readable message (in Indonesian) may change.
json
{ "error": { "code": "group_not_granted", "message": "Kunci ini tidak diberi akses ke grup tersebut." } }

Endpoints

All paths are relative to https://api.berquran.com/v1. Try them live in the interactive reference.

GET/healthno key

Health check

No key needed. Answers GET and HEAD, so it works with any uptime monitor.

json
{ "ok": true, "service": "berquran-partner-api", "version": "v1" }
GET/me

About this key

Your organization, quota usage for today, rate limit, and the groups this key may read.

json
{
  "org_name": "PT Contoh Sejahtera",
  "scopes": ["groups:read", "activity:read"],
  "quota": { "limit": 2000, "used": 14, "remaining": 1986 },
  "rate_per_min": 60,
  "groups": [{ "id": 412, "name": "Tadarus Kantor Pusat" }]
}
GET/groups

List granted groups

Groups granted to this key. The gap between members_total and members_consented tells you how many members cannot be read, without saying who.

json
{
  "data": [{
    "id": 412,
    "name": "Tadarus Kantor Pusat",
    "org_name": "PT Contoh Sejahtera",
    "description": "Khatam bersama tiap bulan",
    "members_total": 58,
    "members_consented": 51
  }],
  "next_cursor": null,
  "meta": { "count": 1 }
}
GET/groups/{group_id}/members

List consenting members

Only members who consented to organization reporting. external_ref is filled in by the coordinator (for example an employee ID) and is how you map a member to your own records.

ParameterInTypeDescription
group_idpathintegerA group granted to your key (see /groups).
json
{
  "data": [{
    "member_ref": "9f2c4a71be03d8e6",
    "external_ref": "EMP-00231",
    "name": "Ahmad F.",
    "role": "member",
    "joined_at": "2026-08-31T02:14:09+00:00",
    "consent": { "report": true, "tartil": false },
    "level": 7,
    "khatam_count": 2,
    "distinct_pages": 604,
    "juz": 30,
    "tasks_done": 41
  }],
  "next_cursor": null,
  "meta": { "count": 1 }
}
GET/groups/{group_id}/daily

Daily activity per member

The main endpoint for HR use: one row per member per day they read. Days without reading have no row. tartil is null unless that member separately consented to share Tartil Mode scores.

ParameterInTypeDescription
group_idpathintegerA group granted to your key (see /groups).
fromquerydateYYYY-MM-DD. Defaults to 29 days before to.
toquerydateYYYY-MM-DD. Defaults to today. Max window 92 days.
member_refquerystringLimit to one member (from /members).
cursorquerystringThe next_cursor from the previous page.
page_sizequeryinteger1 to 200. Default 100.
json
{
  "data": [{
    "member_ref": "9f2c4a71be03d8e6",
    "day": "2026-10-08",
    "sessions": 2,
    "voiced_seconds": 1260,
    "words": 1830,
    "pages": 12,
    "distinct_pages": 10,
    "ayat": 141,
    "huruf": 7904,
    "tartil": null
  }],
  "next_cursor": "eyJkIjoiMjAyNi0xMC0wOCIsInUiOjE4M30",
  "meta": { "from": "2026-09-10", "to": "2026-10-09", "count": 100 }
}
GET/groups/{group_id}/daily/summary

Daily totals for a group

One row per day for the whole group: how many consenting members read, and how much.

ParameterInTypeDescription
group_idpathintegerA group granted to your key (see /groups).
fromquerydateYYYY-MM-DD. Defaults to 29 days before to.
toquerydateYYYY-MM-DD. Defaults to today. Max window 92 days.
json
{
  "data": [{
    "day": "2026-10-08",
    "active_members": 37,
    "sessions": 64,
    "voiced_seconds": 40210,
    "words": 58113,
    "pages": 384,
    "ayat": 4512
  }],
  "next_cursor": null,
  "meta": { "from": "2026-09-10", "to": "2026-10-09", "count": 30 }
}
GET/groups/{group_id}/sessions

Reading sessions

Individual reading sessions, newest first, with the page range read.

ParameterInTypeDescription
group_idpathintegerA group granted to your key (see /groups).
fromquerydateYYYY-MM-DD. Defaults to 29 days before to.
toquerydateYYYY-MM-DD. Defaults to today. Max window 92 days.
member_refquerystringLimit to one member (from /members).
cursorquerystringThe next_cursor from the previous page.
page_sizequeryinteger1 to 200. Default 100.
json
{
  "data": [{
    "session_id": 880214,
    "member_ref": "9f2c4a71be03d8e6",
    "started_at": "2026-10-08T22:41:03+00:00",
    "ended_at": "2026-10-08T23:02:47+00:00",
    "page_start": 302,
    "page_end": 309,
    "voiced_seconds": 1180,
    "words": 1702,
    "pages": 8,
    "ayat": 96,
    "tartil": true,
    "tartil_percent": null
  }],
  "next_cursor": null,
  "meta": { "from": "2026-09-10", "to": "2026-10-09", "count": 1 }
}
GET/groups/{group_id}/sessions/{session_id}/tajwid

Tajwid summary of a session

Per-rule totals for one Tartil Mode session. Only for members who consented to share Tartil scores; otherwise 403 tartil_not_consented. Per-word results are never exposed.

ParameterInTypeDescription
group_idpathintegerA group granted to your key (see /groups).
session_idpathintegerFrom /sessions.
json
{
  "data": [
    { "code": "madd", "name": "Mad", "total": 48, "ok": 45, "percent": 94 },
    { "code": "ghunnah", "name": "Ghunnah", "total": 21, "ok": 19, "percent": 90 }
  ],
  "next_cursor": null,
  "meta": { "count": 2 }
}

Reading the numbers

  • words, pages, ayat and huruf are summed per session. Reading the same page twice in a day counts twice, just like the reward count in the app. For “how many different pages” use distinct_pages.
  • voiced_seconds is time the member was actually reciting, not time the app was open.
  • tartil: null means not permitted, not zero. Never treat it as a poor score.
  • Activity is summarised shortly after each session, so the current day can lag a few minutes behind the app.
  • BerQuran does not grade whether a page “passed”. These are reading records, not exam results; please don't use them to rank or penalise people.

Errors

StatusCodeMeaning
400bad_date · range_too_wide · bad_cursorDate is not YYYY-MM-DD, from is after to, the window exceeds 92 days, or the cursor is malformed.
401invalid_keyThe key is missing, malformed, or unknown.
403key_revoked · key_expiredThe key was revoked or has expired. Create a new one in the console.
403client_pending · client_suspended · client_revokedYour application is not (or no longer) approved.
403group_not_grantedThe key has no access to that group, or the group does not exist.
403tartil_not_consentedNo member in the group consented to share Tartil scores.
404member_not_found · not_foundMember is not in the group or has not consented; or the session is outside your scope.
422bad_requestA parameter has the wrong type. See error.detail.
429rate_limited · quota_exceededToo many requests this minute, or the daily quota is used up. Wait Retry-After seconds.
503not_configuredThe service is temporarily unavailable.

Ready to connect your organization?

Sign in, mark your group as an organization group, and send your application.