API docs
Verify a buyer's payment claim against the seller's expected sale and settled bank alerts. One endpoint, one deterministic verdict.
Overview
Khusela is payment-risk verification for South African micro-sellers. A buyer sends proof of payment; Khusela checks the document for editing traces, compares the claim to an expected sale, and looks for the same document under another seller.
Seller bot
Receives the buyer's screenshot or PDF on WhatsApp.
POST /check
Uploads the file with the seller's Bearer token.
Rules
Document, payment, and duplicate signals are evaluated.
Verdict
STOP, WAIT, or MATCH with a signal list.
Containment
Hold, request confirmation, or release goods.
Purge
Raw file is deleted within 24 hours.
https://api.khusela.internalAll request and response bodies are JSON except the multipart upload to /check. Timestamps are ISO 8601 in SAST.
Authentication
Every request uses a seller token in the Authorization header. The token resolves to one row in sellers; the seller ID is never accepted from the request body.
Authorization: Bearer khs_live_7f3a…
Register a seller
/sellers/registerbusiness_namestringLegal or trading name. Required, 2–120 characters.owner_namestringPerson responsible for the account.phonestringE.164 WhatsApp number used by the bot.The response contains the seller record and a one-time auth_token. Store it immediately; only a hash is retained.
Verify a payment
/checkMultipart form upload. The file must be JPEG, PNG, or PDF and no larger than 5 MB.
filebinaryPayment screenshot or bank-statement PDF. Required.expected_sale_idstringReference such as ES-88214. Required for MATCH eligibility.claimed_amountdecimalOptional buyer-stated amount. OCR wins if omitted.notestringOptional seller context, max 500 characters.curl -X POST https://api.khusela.internal/check \ -H "Authorization: Bearer khs_live_7f3a…" \ -F "file=@receipt.png" \ -F "expected_sale_id=ES-88213"
Response
{
"check_id": "CHK-10482",
"verdict": "MATCH",
"confidence": 98.4,
"reasons": [
"amount_matches_expected_sale",
"recipient_matches",
"bank_alert_found"
],
"duplicate": false,
"retention": "raw_file_deleted_in_24h"
}
The response is returned only after the verdict is written to checks and the fingerprint to fingerprints. A duplicate hit from another seller returns STOP even when every amount signal passes.
Verdicts
MATCH≥ 90Claim, expected sale, recipient, and bank alert agree. Release may proceed.WAIT60–89Something needs a human: missing bank alert, amount mismatch, or low OCR confidence.STOP< 60Fraud risk: cross-seller duplicate, edited document, or blocking signal. Never auto-release.Scores are deterministic for the same inputs and rule version. The rule version is stored with every check so a historical verdict can be reproduced.
Common reason codes
- amount_matches_expected_sale — exact amount match, zero tolerance.
- recipient_matches — fuzzy name match at 90% or better.
- bank_alert_found — settled event with the same amount and reference.
- amount_mismatch — claim differs from the expected sale; caps at WAIT.
- font_inconsistency — edited text layer detected; forces STOP.
- cross_seller_duplicate — perceptual hash hit under another seller; forces STOP.
Data models
SQLite via SQLAlchemy. Four tables hold the entire verification state; raw files live outside the database and expire in 24 hours.
sellers
idintegerPrimary key.business_namestringDisplayed to buyers.auth_tokenstringHashed; indexed for lookup.created_atdatetimeUTC storage, SAST display.expected_sales
idintegerPrimary key.seller_idintegerFK → sellers.id.expected_amountdecimalStored as cents to avoid float drift.recipient_namestringCompared with fuzzy matching.checks
idintegerPrimary key.seller_idintegerFK → sellers.id.verdictstringSTOP, WAIT, or MATCH.file_hashstringSHA-256 of the uploaded bytes.created_atdatetimeUsed for the 90-day history window.fingerprints
idintegerPrimary key.check_idintegerFK → checks.id.seller_idintegerIndexed; duplicate query excludes the current seller.perceptual_hashstring64-bit dHash, indexed for proximity search.Errors
401unauthorizedMissing, malformed, or revoked Bearer token.413file_too_largeUpload exceeds 5 MB.415unsupported_media_typeNot JPEG, PNG, or PDF.422unreadable_documentHeader parse or sandboxed PDF extraction failed.429rate_limitedMore than 60 checks per minute for one seller.503verification_pausedWorkspace automation is paused.{
"error": "unreadable_document",
"message": "The PDF could not be parsed in the sandbox.",
"request_id": "req_8f21c0"
}
Safety limits
- Maximum upload size is 5 MB, enforced before the body is fully read.
- Allowed MIME types are image/jpeg, image/png, and application/pdf.
- PDFs are parsed in an unprivileged Docker container with no network access and a 5 second CPU budget.
- Duplicate lookup only considers fingerprints from other sellers, so a seller cannot poison their own history.
- A STOP verdict can never be auto-released, even if the amount later matches.
- Requests are rate limited to 60 checks per minute per seller token.
Retention
Raw receipts are deleted 24 hours after upload by a scheduled worker that runs every 15 minutes. Verdicts and fingerprints persist for 90 days, then are aggregated into seller-level counts. Confirmed fraud incidents keep metadata only; the raw document is never retained for a case.