One REST endpoint returns a consented, explainable, fully-audited credit score for any Sheba-registered Ethiopian consumer — in real time. This is the complete reference: authenticate, send a query, read the score. Built to comply with NBE Directive CRB/02/2019 (Sheba is pre-licence — building toward NBE licensing).
Everything below uses public sandbox credentials — a shared test institution key and a seeded, consented test subject. Safe to share, rate-limited, and scoped to the sandbox. Same scoring engine, audit log and response shape as production; only the data is seeded.
curl -sS -X POST \ "https://pglbndkxmflyfxpuhaaf.supabase.co/functions/v1/credit-query" \ -H "Authorization: Bearer sk_sandbox_sheba_pilot_7f3a9c2e1b8d4056" \ -H "Content-Type: application/json" \ -d '{ "subject_ref": "sub_9f3a", "consent_token": "cnst_sandbox_3b1e7d92", "purpose": "loan_origination" }'
Returns 200 with a Sheba Score of 724 (Good), the factors that drove it, an eligibility recommendation, and an immutable audit_ref.
This calls the live sandbox endpoint directly — the same one your server would hit. Edit the fields and send. (In-browser calls are CORS-scoped to shebacredit.com and localhost; from anywhere else, use the curl above — it works from any server.)
Response will appear here.
Uses the public sandbox key. Rate-limited to 30 requests/hour. No data leaves the sandbox.
Every request carries your institution's secret key in the Authorization header. Keys are hashed (SHA-256) at rest — Sheba never stores the raw key. Auth happens before any request body is parsed; a bad or inactive key fails closed with 401 and writes no audit row.
Authorization: Bearer sk_live_your_institution_key Content-Type: application/json
| Key | Where | Notes |
|---|---|---|
| sk_sandbox_… | Public sandbox | Shared test key on this page. Read-only, rate-limited, sandbox data only. |
| sk_live_… | Your institution | Issued per institution after onboarding. Scoped to your consent agreements and volume tier. Request one → |
POST https://pglbndkxmflyfxpuhaaf.supabase.co/functions/v1/credit-queryOnly POST is supported (a preflight OPTIONS is handled for browsers). Any other method returns 405 with an Allow: POST, OPTIONS header. Typical end-to-end latency is under 500 ms.
Send a JSON body (max 16 KB). Sheba is data-minimising by design: raw fayda_id or phone are rejected — you supply the pseudonymous subject_ref only.
| Field | Type | Description |
|---|---|---|
| subject_refrequired | string | Pseudonymous subject id. Pattern [A-Za-z0-9_-]{1,128}. Never a raw national ID or phone. |
| consent_tokenrequired | string | The consent grant authorising this query. Must be granted, unexpired, and bound to your institution, this subject, and the purpose. |
| purposerequired | enum | One of loan_origination or credit_assessment. The consent's scope must cover the sources this purpose requires. |
| idempotency_keyoptional | string | Replays the original scored response verbatim if you retry — no re-score, no double-count. See Idempotency. |
{
"query_id": "a3f1c8e2-…",
"subject_ref": "sub_9f3a",
"status": "scored",
"score": 724,
"tier": "Good",
"confidence": 0.95,
"scale": [300, 850],
"factors": {
"positive": [
{ "code": "PAY_ONTIME", "label_en": "Consistent on-time payment history", "label_am": "ወጥ የሆነ በወቅቱ የመክፈል ታሪክ", "impact": "high" }
],
"negative": []
},
"recommendation": { "action": "approve_with_conditions", "eligible": true, "indicative_limit_etb": 25000 },
"data_sources": ["telebirr", "sheba_save", "fayda", "education"],
"segment": "urban",
"model_version": "sheba-score-v1.0.0",
"config_hash": "…",
"generated_at": "2026-07-01T10:42:07Z",
"audit_ref": "9c21e7b4-…"
}| Field | Type | Description |
|---|---|---|
| query_id / audit_ref | uuid | Identifier of this query and its immutable audit row (same value). |
| status | enum | scored, building or provisional. See Statuses. |
| score | int · null | 300–850 when scored; null for provisional. |
| tier | string · null | Poor / Fair / Good / Very Good / Excellent. Omitted while building. |
| confidence | float | 0–1, two decimals — how much signal backed the score. |
| factors | object | positive and negative arrays of reason codes, each with EN + Amharic labels and an impact. See Reason codes. |
| recommendation | object | action, eligible (bool), and indicative_limit_etb when applicable. Advisory — the lender always keeps the decision. |
| data_sources | string[] | The scopes granted by the presented consent. If the consumer has revoked an individual source, it is excluded from scoring even though it still appears here — treat this as the consent's grant list, not the exact inputs used. |
| segment | enum | urban, farmer or sme — the sub-model applied. |
| model_version / config_hash | string | The exact model and a tamper-evident hash of its frozen config, recorded on every audit row. |
| Status | Meaning | Fields |
|---|---|---|
| scored | Enough signal for a confident score. | score + tier + confidence. |
| building | Thin file — real but limited history. The score is capped (≤700) and confidence is low, so you never over-trust a sparse file. | score (capped), no tier. |
| provisional | Identity not yet Fayda-verified — we won't emit a score without a verified identity. | score = null, recommendation verify_identity. |
A truly empty file returns 422 thin_file_no_score rather than a fabricated number.
| Tier | Range | Typical recommendation |
|---|---|---|
| Poor | 300–579 | decline_or_refer |
| Fair | 580–669 | review_or_secured_terms |
| Good | 670–739 | approve_with_conditions |
| Very Good | 740–799 | approve_standard_terms |
| Excellent | 800–850 | approve_standard_terms |
The code is the stable, language-independent contract; the labels are what you show a customer. Because NBE CRB/02/2019 requires local-language adverse-action reasons, every code carries both. Use the code in logic; render label_am on the decline notice.
| Code | English | Amharic | Category |
|---|---|---|---|
| PAY_ONTIME | Consistent on-time payment history | ወጥ የሆነ በወቅቱ የመክፈል ታሪክ | Payment |
| PAY_LOAN_ONTIME | Loans repaid on time | ብድሮች በወቅቱ ተከፍለዋል | Payment |
| PAY_MISSED_RECENT | Recently missed one or more payments | በቅርቡ አንድ ወይም ከዚያ በላይ ክፍያዎች አልተከፈሉም | Payment |
| THIN_HISTORY | Limited credit and financial history | ውስን የሆነ የብድርና የፋይናንስ ታሪክ | Payment |
| ACT_LOW_TENURE | Short account / activity history | አጭር የሂሳብ ወይም የእንቅስቃሴ ታሪክ | Activity |
| ACT_VOLATILE_BALANCE | Highly variable account balance | በጣም ተለዋዋጭ የሂሳብ ቀሪ መጠን | Activity |
| ACT_REGULAR_INFLOW | Regular, steady income inflows | መደበኛና የተረጋጋ የገቢ ፍሰት | Activity |
| SAV_STREAK | Sustained savings streak | ቀጣይነት ያለው የቁጠባ ልምድ | Savings |
| SAV_NONE | No savings activity detected | ምንም የቁጠባ እንቅስቃሴ አልተገኘም | Savings |
| ID_VERIFIED | Identity verified with Fayda | ማንነት በፋይዳ ተረጋግጧል | Identity |
| ID_UNVERIFIED | Identity not yet verified with Fayda | ማንነት እስካሁን በፋይዳ አልተረጋገጠም | Identity |
| OB_NOT_CONNECTED | Few connected financial accounts | ጥቂት የተገናኙ የፋይናንስ ሂሳቦች | Identity |
| ENG_EDUCATED | Completed financial-education modules | የፋይናንስ ትምህርት ክፍሎችን አጠናቅቋል | Engagement |
{ "error": { "code": "consent_required", "message": "Consent missing, expired, revoked, …" } }| Code | HTTP | When |
|---|---|---|
| invalid_request | 400 | Malformed JSON, unknown purpose, bad subject_ref, or a raw fayda_id/phone was sent. |
| unauthorized | 401 | Missing/invalid API key, or the institution is not active. No audit row is written. |
| consent_required | 403 | Consent missing, expired, revoked, not bound to this institution/subject, or outside granted scope. Audited as a denial. |
| subject_not_found | 404 | Unknown subject_ref or no feature vector on file. |
| method_not_allowed | 405 | Any method other than POST/OPTIONS. |
| thin_file_no_score | 422 | Insufficient history to produce a score. Audited as a denial. |
| rate_limited | 429 | Quota exceeded — includes a Retry-After header. Audited. |
| internal_error | 500 | Any DB/scoring failure or a failed audit write (we never serve a score we couldn't log). |
Counters are per institution + key, on fixed hour and day windows. Over the limit returns 429 with Retry-After (seconds) and a retry_after_seconds field. If the limiter itself can't be checked, the request is denied — a score is never served unproven.
| Tier | Per hour | Per day |
|---|---|---|
| Pilot (sandbox & early pilots) | 30 | 200 |
| Standard | 600 | 10,000 |
| Custom | Tuned to your portfolio volume — set during onboarding. | |
Include an idempotency_key in the body. If you retry the same key, Sheba replays the original scored response verbatim and returns an Idempotent-Replay: true header — no re-score, no re-count against your quota. Transient 429 denials are not bound to the key, so the same key can still succeed once quota frees up.
-d '{ "subject_ref": "sub_9f3a", "consent_token": "cnst_…", "purpose": "loan_origination", "idempotency_key": "loan-4821-attempt-1" }' # retry with the same key → 200 + header Idempotent-Replay: true
Every query is gated on a live, granted, unexpired consent bound to your institution, the subject, and the purpose. Revoke a data source's consent and it is excluded from the next score — per-source, not all-or-nothing.
Every query — approval and denial — writes one append-only, hash-chained audit row. The chain is independently verifiable end-to-end. This is the NBE CRB/02/2019 pre-lending-query evidence, by construction.
Raw national ID and phone are never accepted, logged, or returned — only the pseudonymous subject_ref. The scorer sees a verified-identity flag, never the identity itself.
Sheba returns a score and machine-readable reason codes. The credit decision — and the consumer-facing adverse-action notice — remain yours. Sheba is pre-licence, building toward NBE licensing.
Submit a de-identified slice of your historical loan file and get back how Sheba's payment factor would have ranked it against what actually happened. Rows are scored in memory with the same subscore_payment() the live model runs — same code, same frozen constants, same config_hash — and then discarded. Only the aggregate is stored, and it is readable in the partner portal under Retrospective tests, which also builds the file for you locally from a CSV.
POST https://pglbndkxmflyfxpuhaaf.supabase.co/functions/v1/batch-evaluate # same Bearer API key as credit-query · JSON body · at most 2,000 rows and 1 MB per run
{
"label": "SME term loans — 2025 vintage",
"rows": [
{ "ref": "LN-4471", "pay_total_count": 12, "pay_ontime_count": 11, "pay_missed_count": 1,
"pay_missed_recent_90d": 0, "pay_oldest_days": 610, "outcome": "repaid" },
{ "ref": "LN-4502", "pay_total_count": 9, "pay_ontime_count": 4, "pay_missed_count": 5,
"pay_missed_recent_90d": 2, "outcome": "dpd_90" }
]
}| Field | Type | Description |
|---|---|---|
| refoptional | string | Your own opaque loan reference. Never a person's identifier. |
| pay_total_countrequired | int | Instalments that fell due. Rows with zero are rejected as no_payment_history. |
| pay_ontime_count · pay_missed_count | int | Instalments paid on time / missed over the life of the loan. |
| pay_missed_recent_90d | int | Instalments missed in the 90 days before the outcome date. |
| pay_oldest_days · pay_loan_ontime_count | int | Days since the borrower's oldest obligation with you; earlier loans repaid on time. |
| pay_misses_observable · pay_default_reported | bool | Both optional. Set the first to false only for a one-sided statement feed that cannot show a miss; the second floors the sub-score for a confirmed default. |
| outcomerequired | enum | Good: repaid, current, good. Bad: dpd_90, default, written_off, bad. Anything else is rejected as unknown_outcome. |
De-identified by contract. A row carrying a key that looks like personal data — national_id, fayda_id, phone, msisdn, email, name, dob, address, account_number, iban and their variants — is rejected as contains_personal_data, never silently stripped.
{
"evaluation_id": "7d0c…",
"model_version": "sheba-score-v1.0.0", "config_hash": "…",
"disclaimer": "Payment sub-score only — not a Sheba Score. …",
"summary": {
"scored": 1140, "rejected": 60, "bad": 97, "good": 1043,
"overall_bad_rate": 0.0851, "mean_payment_subscore": 61.4,
"bands": [ { "band": "0–19", "n": 88, "bad": 31, "bad_rate": 0.3523 }, … five bands … ],
"separation": {
"bottom_two_bands": { "n": 259, "bad": 60, "bad_rate": 0.2317 },
"top_two_bands": { "n": 591, "bad": 18, "bad_rate": 0.0305 },
"lift": 7.6
},
"rejected_reasons": { "unknown_outcome": 41, "no_payment_history": 19 }
}
}A band's bad_rate is null below 10 rows, and lift is null unless both tails carry at least 10 — a figure from a handful of loans is noise, and printing it invites it being quoted. The result is not a Sheba Score: a score needs verified identity and consent, which a historical file cannot carry. Nothing here touches the audit log or a credit file, and the call counts against your ordinary per-call quota only.
The sandbox uses a shared test key. To run real queries on consented consumers, request a dedicated institution key — we'll set up your institution, scope your consent, and tune your volume tier.