Developer Docs

The Sheba Credit API.

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).

Quickstart

Score a subject in one call.

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.

Try it live

Send a real request from your browser.

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.

Authentication

Bearer API key.

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
KeyWhereNotes
sk_sandbox_…Public sandboxShared test key on this page. Read-only, rate-limited, sandbox data only.
sk_live_…Your institutionIssued per institution after onboarding. Scoped to your consent agreements and volume tier. Request one →
Endpoint

One endpoint. One method.

POST https://pglbndkxmflyfxpuhaaf.supabase.co/functions/v1/credit-query

Only 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.

Request body

JSON fields.

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.

FieldTypeDescription
subject_refrequiredstringPseudonymous subject id. Pattern [A-Za-z0-9_-]{1,128}. Never a raw national ID or phone.
consent_tokenrequiredstringThe consent grant authorising this query. Must be granted, unexpired, and bound to your institution, this subject, and the purpose.
purposerequiredenumOne of loan_origination or credit_assessment. The consent's scope must cover the sources this purpose requires.
idempotency_keyoptionalstringReplays the original scored response verbatim if you retry — no re-score, no double-count. See Idempotency.
Response · 200

The scored response.

{
  "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-…"
}
FieldTypeDescription
query_id / audit_refuuidIdentifier of this query and its immutable audit row (same value).
statusenumscored, building or provisional. See Statuses.
scoreint · null300–850 when scored; null for provisional.
tierstring · nullPoor / Fair / Good / Very Good / Excellent. Omitted while building.
confidencefloat0–1, two decimals — how much signal backed the score.
factorsobjectpositive and negative arrays of reason codes, each with EN + Amharic labels and an impact. See Reason codes.
recommendationobjectaction, eligible (bool), and indicative_limit_etb when applicable. Advisory — the lender always keeps the decision.
data_sourcesstring[]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.
segmentenumurban, farmer or sme — the sub-model applied.
model_version / config_hashstringThe exact model and a tamper-evident hash of its frozen config, recorded on every audit row.
Statuses

Not every file is scorable — and that's honest.

StatusMeaningFields
scoredEnough signal for a confident score.score + tier + confidence.
buildingThin 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.
provisionalIdentity 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.

Score tiers

300–850, five bands.

TierRangeTypical recommendation
Poor300–579decline_or_refer
Fair580–669review_or_secured_terms
Good670–739approve_with_conditions
Very Good740–799approve_standard_terms
Excellent800–850approve_standard_terms
Reason codes

Every score is explainable — in English and Amharic.

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.

CodeEnglishAmharicCategory
PAY_ONTIMEConsistent on-time payment historyወጥ የሆነ በወቅቱ የመክፈል ታሪክPayment
PAY_LOAN_ONTIMELoans repaid on timeብድሮች በወቅቱ ተከፍለዋልPayment
PAY_MISSED_RECENTRecently missed one or more paymentsበቅርቡ አንድ ወይም ከዚያ በላይ ክፍያዎች አልተከፈሉምPayment
THIN_HISTORYLimited credit and financial historyውስን የሆነ የብድርና የፋይናንስ ታሪክPayment
ACT_LOW_TENUREShort account / activity historyአጭር የሂሳብ ወይም የእንቅስቃሴ ታሪክActivity
ACT_VOLATILE_BALANCEHighly variable account balanceበጣም ተለዋዋጭ የሂሳብ ቀሪ መጠንActivity
ACT_REGULAR_INFLOWRegular, steady income inflowsመደበኛና የተረጋጋ የገቢ ፍሰትActivity
SAV_STREAKSustained savings streakቀጣይነት ያለው የቁጠባ ልምድSavings
SAV_NONENo savings activity detectedምንም የቁጠባ እንቅስቃሴ አልተገኘምSavings
ID_VERIFIEDIdentity verified with Faydaማንነት በፋይዳ ተረጋግጧልIdentity
ID_UNVERIFIEDIdentity not yet verified with Faydaማንነት እስካሁን በፋይዳ አልተረጋገጠምIdentity
OB_NOT_CONNECTEDFew connected financial accountsጥቂት የተገናኙ የፋይናንስ ሂሳቦችIdentity
ENG_EDUCATEDCompleted financial-education modulesየፋይናንስ ትምህርት ክፍሎችን አጠናቅቋልEngagement
Errors

Every failure is typed and fail-closed.

{ "error": { "code": "consent_required", "message": "Consent missing, expired, revoked, …" } }
CodeHTTPWhen
invalid_request400Malformed JSON, unknown purpose, bad subject_ref, or a raw fayda_id/phone was sent.
unauthorized401Missing/invalid API key, or the institution is not active. No audit row is written.
consent_required403Consent missing, expired, revoked, not bound to this institution/subject, or outside granted scope. Audited as a denial.
subject_not_found404Unknown subject_ref or no feature vector on file.
method_not_allowed405Any method other than POST/OPTIONS.
thin_file_no_score422Insufficient history to produce a score. Audited as a denial.
rate_limited429Quota exceeded — includes a Retry-After header. Audited.
internal_error500Any DB/scoring failure or a failed audit write (we never serve a score we couldn't log).
Rate limits

Per-institution quotas, fail-closed.

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.

TierPer hourPer day
Pilot (sandbox & early pilots)30200
Standard60010,000
CustomTuned to your portfolio volume — set during onboarding.
Idempotency

Safe retries, no double-billing.

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
Consent & audit

Consent-gated, minimised, and immutably logged.

Fail-closed consent

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.

Immutable audit trail

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.

Data minimisation

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.

Lender keeps the decision

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.

Retrospective evaluation

Test the payment factor on your own book — before you integrate.

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" }
  ]
}
FieldTypeDescription
refoptionalstringYour own opaque loan reference. Never a person's identifier.
pay_total_countrequiredintInstalments that fell due. Rows with zero are rejected as no_payment_history.
pay_ontime_count · pay_missed_countintInstalments paid on time / missed over the life of the loan.
pay_missed_recent_90dintInstalments missed in the 90 days before the outcome date.
pay_oldest_days · pay_loan_ontime_countintDays since the borrower's oldest obligation with you; earlier loans repaid on time.
pay_misses_observable · pay_default_reportedboolBoth 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.
outcomerequiredenumGood: 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.

Ready for a production key?

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.