Skip to documentation
telspay.Developer documentation · v1Get API access

STAKEHOLDER API · RELEASE 2026.9.0

Connect with confidence.

Everything you need to request access, sign requests and work with member-approved SACCO services.

Your first integration

Use the Telspay API to connect an approved stakeholder system to the SACCO services its members authorise. This release supports member information, savings statements, group membership, fixed deposits and requests that members review.

  1. Create an applicant account, verify your email and enable an authenticator.
  2. Apply for sandbox access. Select your target SACCO, permissions and integration use case.
  3. After approval, create a key. Save the secret securely when it is displayed.
  4. Call sandbox products and the synthetic member fixture. Test failures and retries.
  5. Apply separately for live access with exact public server IPs. Two different administrators must approve it.
  6. Ask each member to grant permission in Connected Apps and give you their opaque member ID.

Approval does not transfer responsibility for lending or payment verification to the integrator. Keep credentials and signing code on your server.

Environments and test data

EnvironmentBase pathData and approval
SANDBOX/api/v1/sandboxSynthetic fixtures, one administrator review; never posts to live financial tables.
LIVE/api/v1/liveSame-SACCO data with current consent; two different administrators, IP allowlist and live activation.
https://YOUR-CONFIGURED-DOMAIN/api/v1/sandbox

Member: mem_00000000000000000000000000000001
Loan product: 1  |  Maximum UGX 1,000,000  |  1–12 months
Fixed product: 1 |  UGX 100,000–1,000,000 |  30–365 days
Fixture savings: UGX 750,000.00
Fixture shares:  UGX 100,000.00

Sandbox POST requests return SIMULATED. They do not simulate loan underwriting, settlement, bank callbacks or a real member review. The fixed sandbox rate (5%) is a test fixture and is never published as a live SACCO product. Use separate keys and configuration per environment.

Sign every request

Send the key ID and a lowercase hexadecimal HMAC-SHA256 signature. The secret supplied at key creation is the HMAC key as the displayed 64-character text; do not hex-decode it. Keys expire after 90 days. Creating a replacement immediately revokes the previous key.

HeaderRule
X-API-Keytptest_… or tplive_… key ID.
X-API-TimestampUnix seconds; keep the server clock within ±300 seconds.
X-API-NonceFresh random 16–96 characters [A–Z, a–z, 0–9, _, -] for every HTTP attempt.
X-API-Signature64 lowercase hexadecimal characters.
Idempotency-KeyRequired for POST; 16–96 characters with the same allowed character set.
Content-Typeapplication/json for POST. GET has an empty body.

Join exactly these six fields with the LF byte ( ). Sign the raw URL path and query in their transmitted order; do not include scheme, host or fragment. Hash the exact bytes of the request body.

UPPERCASE_METHOD
/raw/path?query=as-transmitted
UNIX_TIMESTAMP
NONCE
LOWERCASE_SHA256_OF_RAW_BODY
IDEMPOTENCY_KEY_OR_EMPTY

For GET, the body is an empty string and the last field is empty, so the canonical string ends in LF. For POST, JSON spacing and escaping must match the transmitted body. HTTP headers alone are insufficient without the signature.

$raw = json_encode($payload, JSON_UNESCAPED_SLASHES | JSON_THROW_ON_ERROR);
$canonical = implode("\n", [$method, $requestTarget, $timestamp, $nonce,
    hash('sha256', $raw), $idempotencyKey]);
$signature = hash_hmac('sha256', $canonical, $secret);

HTTPS is mandatory in production. Browser CORS access, OAuth tokens and public API secrets are not supported. Put your integration behind your own authenticated server.

Endpoint reference

MethodPathScopeBehaviour
GET/productsproducts:readReturns products assigned to the client’s SACCO. Interpret configured_rate and interest_method using SACCO policy, not as an assumed APR.
GET/fixed-productsproducts:readRates are in basis points: 700 means 7.00% a year. Tax applies only to gross interest. The accepted booking terms remain fixed.
GET/members/{member_id}members:readRequires unexpired member consent, an active member and the same SACCO as the client.
GET/members/{member_id}/balancesbalances:readSavings is the signed legacy ledger total. Available excludes old active fixed holds and unposted positive entries; new fixed placements and group reservations are already debited. Final spending checks also enforce minimum balance and pending withdrawals.
GET/members/{member_id}/statementstatements:readReturns the signed savings ledger. Includes legacy records as recorded; it is not a bank settlement confirmation. Shares are not a savings transaction.
GET/members/{member_id}/groupsgroups:readReturns up to 100 active memberships. Does not expose other group members’ personal accounts.
GET/members/{member_id}/fixed-depositsfixed:readReturns up to 100 latest placements belonging to this individual member.
POST/loan-intentsloans:requestThe member continues in the existing PIN, KYC, affordability, guarantor and SACCO-review process. This endpoint never disburses or approves a loan.
POST/fixed-deposit-intentsfixed:requestThe member must accept a fresh quote and enter their PIN before funds are reserved from their own savings.
POST/deposit-noticesdeposits:notifyA notice is untrusted payment information. Member acknowledgement and administrator reconciliation do not automatically credit funds; post only through the existing verified payment workflow.
GET/requests/{request_id}scope for the request kindOnly this client can read its request. Live reads still require the member’s current consent for that request kind. The loan reference means submitted, not approved or disbursed.

Append each path to the environment base URL. Money must be a decimal string in UGX. Amounts are computed in integer minor units, rounded half up at the defined interest/tax boundaries. Timestamps without a timezone suffix use UTC. Product IDs are numeric; members and requests use opaque references.

Working request examples

The PHP download includes TelspayClient.php and a CLI quickstart. Set TELSPAY_API_BASE, TELSPAY_API_KEY and TELSPAY_API_SECRET in private server configuration.

require 'TelspayClient.php';
$api = new TelspayClient(getenv('TELSPAY_API_BASE'),
    getenv('TELSPAY_API_KEY'), getenv('TELSPAY_API_SECRET'));
$products = $api->request('GET', 'products');
$member = 'mem_00000000000000000000000000000001';
$balance = $api->request('GET', 'members/'.$member.'/balances');
$page = $api->request('GET', 'members/'.$member.'/statement', null,
    ['limit' => 25, 'after' => 0]);

// Persist with the job BEFORE the first send. Reuse only for this same action.
$jobKey = 'loan_'.bin2hex(random_bytes(16));
$intent = $api->request('POST', 'loan-intents', [
    'member_id' => $member, 'product_id' => 1,
    'amount' => '100000.00', 'months' => 6
], [], $jobKey);
$status = $api->request('GET', 'requests/'.$intent['data']['request_id']);
POST /api/v1/live/fixed-deposit-intents
{
  "member_id": "mem_REPLACE_WITH_CONSENTED_MEMBER_ID",
  "product_id": 1,
  "amount": "500000.00",
  "matures_on": "REPLACE_WITH_FUTURE_YYYY-MM-DD"
}

POST /api/v1/live/deposit-notices
{
  "member_id": "mem_REPLACE_WITH_CONSENTED_MEMBER_ID",
  "amount": "100000.00",
  "reference": "BANK-REFERENCE-001",
  "channel": "Bank transfer"
}

Replace placeholders and choose a currently published product before sending. Loan maximums and terms are validated against the SACCO catalogue; fixed maturity dates must fall within the product’s day limits.

What happens after a request

RequestMember actionOutcome visible to the client
Loan intentReview, complete existing loan application, affordability and guarantors, enter PIN.SUBMITTED_TO_SACCO with loan:id. Lending review continues in the SACCO system.
Fixed-deposit intentReview a fresh quote, accept terms, confirm PIN.COMPLETED with fd_… after reservation from the member’s own savings.
Deposit noticeAcknowledge the claimed payment with PIN.MEMBER_ACKNOWLEDGED, then REFERRED_FOR_RECONCILIATION after admin referral. Neither status proves settlement.
Any live intentMember declines or seven-day review period ends.REJECTED or EXPIRED.
Any sandbox intentNo real member action.SIMULATED.

Poll GET /requests/{request_id} at a modest interval. Webhooks are not implemented in this release. Consent must remain valid for the request kind, including status reads. A result reference is a traceable handoff, not permission to access another API object.

Pagination, retries and limits

Statement pages return ascending transaction IDs and pagination.next_after. Pass that value as after to continue; a null cursor means the current end. The default limit is 25 and maximum 100. Group and fixed-deposit lists are capped at 100 in this version.

POST requests require a durable idempotency key. Store it with the business operation before sending. Repeat the same payload and key after a timeout, with a new timestamp, nonce and signature. Reusing the key for different data returns IDEMPOTENCY_CONFLICT. Do not automatically retry with a new key.

The default client limit is 60 requests per minute, with a 120-per-minute IP authentication limit. Bodies are capped at 64 KiB. Use bounded exponential backoff with jitter for 429 and temporary 5xx responses, and retain the original idempotency key. Stop on permission or validation errors until corrected.

Clock drift, nonce reuse, a paused client, expired key, changed allowlist or revoked consent can invalidate a previously successful call. Rotating a key revokes the old key immediately; coordinate deployment of the replacement.

Errors and support

{
  "error": {
    "code": "MEMBER_NOT_FOUND",
    "message": "Member not found or not authorised."
  },
  "request_id": "trace_..."
}
HTTPCommon codesAction
400 / 422INVALID_JSON, IDEMPOTENCY_REQUIRED, INVALID_INPUT, INVALID_AMOUNT, LOAN_LIMIT, DEPOSIT_LIMITCorrect the payload or configured product.
401INVALID_SIGNATURECheck key status, environment, exact signed bytes and clock.
403IP_DENIED, SCOPE_DENIED, ACCOUNT_UNAVAILABLEReview allowlist, scopes and account status.
404MEMBER_NOT_FOUND, REQUEST_NOT_FOUND, ENDPOINT_NOT_FOUNDCheck ownership, current consent and exact path.
409REPLAY_DETECTED, IDEMPOTENCY_CONFLICTUse a fresh nonce; do not reuse an operation key with changed data.
413 / 415BODY_TOO_LARGE, JSON_REQUIREDUse a JSON object below 64 KiB.
426HTTPS_REQUIREDUse the production HTTPS endpoint.
429RATE_LIMITEDBack off with jitter; avoid repeated authentication attempts.
503LIVE_DISABLED, SETUP_REQUIREDContact the SACCO platform operator.
500INTERNAL_ERRORRetain the reference; retry cautiously with the same idempotency key.

Send support the top-level request reference, endpoint, UTC timestamp and HTTP status. Do not send API secrets, signatures, member PINs, raw statements or full personal records.

Integration security responsibilities

  • Keep secrets in a server secret manager or private configuration. Never commit them, embed them in mobile apps or send them in URLs.
  • Use distinct sandbox/live keys, minimum scopes, exact server allowlists and HTTPS certificate verification.
  • Protect your own staff accounts with MFA. Minimise stored member data and honour consent and purpose restrictions.
  • Log request references and error codes, rather than credentials or personal payloads.
  • Test negative cases: other-member access, expired consent, wrong environment, altered body, repeated nonce and duplicate job delivery.
  • Use only the existing verified bank/payment process to recognise incoming funds. An API notice is never payment evidence.

Controls follow the principles in the OWASP REST Security Cheat Sheet and OWASP API Security Top 10. They are not a security certification. Operators must complete deployment review, backups, monitoring and runtime support checks before live use.