Create a loan
curl --request POST \
--url https://api-staging.bsa.ai/v1/loans \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"customerId": "<string>",
"productId": 123,
"principal": 123,
"externalId": "<string>"
}
'import requests
url = "https://api-staging.bsa.ai/v1/loans"
payload = {
"customerId": "<string>",
"productId": 123,
"principal": 123,
"externalId": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({customerId: '<string>', productId: 123, principal: 123, externalId: '<string>'})
};
fetch('https://api-staging.bsa.ai/v1/loans', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-staging.bsa.ai/v1/loans",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'customerId' => '<string>',
'productId' => 123,
'principal' => 123,
'externalId' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-staging.bsa.ai/v1/loans"
payload := strings.NewReader("{\n \"customerId\": \"<string>\",\n \"productId\": 123,\n \"principal\": 123,\n \"externalId\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api-staging.bsa.ai/v1/loans")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"customerId\": \"<string>\",\n \"productId\": 123,\n \"principal\": 123,\n \"externalId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-staging.bsa.ai/v1/loans")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"customerId\": \"<string>\",\n \"productId\": 123,\n \"principal\": 123,\n \"externalId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyLoans
Create a loan
Submit, approve, and disburse a loan in a single call. Returns an Active loan.
POST
/
v1
/
loans
Create a loan
curl --request POST \
--url https://api-staging.bsa.ai/v1/loans \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"customerId": "<string>",
"productId": 123,
"principal": 123,
"externalId": "<string>"
}
'import requests
url = "https://api-staging.bsa.ai/v1/loans"
payload = {
"customerId": "<string>",
"productId": 123,
"principal": 123,
"externalId": "<string>"
}
headers = {
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
response = requests.post(url, json=payload, headers=headers)
print(response.text)const options = {
method: 'POST',
headers: {Authorization: 'Bearer <token>', 'Content-Type': 'application/json'},
body: JSON.stringify({customerId: '<string>', productId: 123, principal: 123, externalId: '<string>'})
};
fetch('https://api-staging.bsa.ai/v1/loans', options)
.then(res => res.json())
.then(res => console.log(res))
.catch(err => console.error(err));<?php
$curl = curl_init();
curl_setopt_array($curl, [
CURLOPT_URL => "https://api-staging.bsa.ai/v1/loans",
CURLOPT_RETURNTRANSFER => true,
CURLOPT_ENCODING => "",
CURLOPT_MAXREDIRS => 10,
CURLOPT_TIMEOUT => 30,
CURLOPT_HTTP_VERSION => CURL_HTTP_VERSION_1_1,
CURLOPT_CUSTOMREQUEST => "POST",
CURLOPT_POSTFIELDS => json_encode([
'customerId' => '<string>',
'productId' => 123,
'principal' => 123,
'externalId' => '<string>'
]),
CURLOPT_HTTPHEADER => [
"Authorization: Bearer <token>",
"Content-Type: application/json"
],
]);
$response = curl_exec($curl);
$err = curl_error($curl);
curl_close($curl);
if ($err) {
echo "cURL Error #:" . $err;
} else {
echo $response;
}package main
import (
"fmt"
"strings"
"net/http"
"io"
)
func main() {
url := "https://api-staging.bsa.ai/v1/loans"
payload := strings.NewReader("{\n \"customerId\": \"<string>\",\n \"productId\": 123,\n \"principal\": 123,\n \"externalId\": \"<string>\"\n}")
req, _ := http.NewRequest("POST", url, payload)
req.Header.Add("Authorization", "Bearer <token>")
req.Header.Add("Content-Type", "application/json")
res, _ := http.DefaultClient.Do(req)
defer res.Body.Close()
body, _ := io.ReadAll(res.Body)
fmt.Println(string(body))
}HttpResponse<String> response = Unirest.post("https://api-staging.bsa.ai/v1/loans")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"customerId\": \"<string>\",\n \"productId\": 123,\n \"principal\": 123,\n \"externalId\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-staging.bsa.ai/v1/loans")
http = Net::HTTP.new(url.host, url.port)
http.use_ssl = true
request = Net::HTTP::Post.new(url)
request["Authorization"] = 'Bearer <token>'
request["Content-Type"] = 'application/json'
request.body = "{\n \"customerId\": \"<string>\",\n \"productId\": 123,\n \"principal\": 123,\n \"externalId\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyCreates a loan against an existing customer using one of the configured
loan products. The returned loan is already
The
Active and ready for
repayments.
Loan terms (term length, repayment schedule, interest rate,
amortization, etc.) are inherited from the chosen productId, so the
request body carries only the three fields that vary per loan.
Request body
string
required
The externalId of the customer this loan is for — the identifier
you chose when creating the customer, not the
numeric LMS id. The service resolves it against the LMS before
submitting anything; an unknown externalId fails with
not_found
and no loan is created.integer
required
The loan product to use. List available products via
GET /v1/loan-products.
number
required
Requested principal amount. Must be greater than 0 and within the
product’s
minPrincipal/maxPrincipal bounds. The full principal is
approved and disbursed in the same call.string
Optional partner-supplied identifier for the loan (e.g. your wallet
transaction reference). The LMS enforces uniqueness — a duplicate
externalId is rejected with
409 already_exists
(loan_external_id_exists). Once set, you can reference
the loan on every repayment route via
/v1/loans/external/{externalId}/... instead of the numeric LMS id.Example
curl -sf -X POST "$BASE/v1/loans" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{
"customerId": "ext-ada-001",
"productId": 1,
"principal": 10000,
"externalId": "loan-ext-12345"
}'
Response
200 OK returns the freshly-created and disbursed loan object.
{
"id": "501",
"accountNo": "000000501",
"externalId": "loan-ext-12345",
"customerId": "42",
"customerName": "ext-ada-001 Customer",
"loanProductId": "1",
"loanProductName": "7-Day Loan",
"status": "Active",
"principal": 10000,
"approvedPrincipal": 10000,
"currencyCode": "TZS",
"termFrequency": 7,
"termFrequencyType": "Days",
"numberOfRepayments": 1,
"interestRatePerPeriod": 11,
"principalDisbursed": 10000,
"principalPaid": 0,
"principalOutstanding": 10000,
"interestCharged": 1100,
"interestPaid": 0,
"interestOutstanding": 1100,
"feeCharged": 0,
"feePaid": 0,
"feeOutstanding": 0,
"penaltyCharged": 0,
"penaltyPaid": 0,
"penaltyOutstanding": 0,
"totalRepayment": 0,
"totalOutstanding": 11100,
"totalOverpaid": 0,
"nextDueDate": "2026-06-04",
"nextDueAmount": 11100,
"overdue": false,
"disbursedAt": "2026-05-28T14:32:05+03:00",
"dueDateTime": "2026-06-04T14:32:05+03:00",
"repaymentSchedule": [
{ "period": 0, "dueDate": "2026-05-28", "complete": false },
{
"period": 1,
"fromDate": "2026-05-28",
"dueDate": "2026-06-04",
"complete": false,
"principalDue": 10000,
"principalOutstanding": 10000,
"interestDue": 1100,
"interestOutstanding": 1100,
"totalDue": 11100,
"totalOutstanding": 11100
}
],
"repaymentHistory": []
}
id (string) is what every downstream call uses
(repayments, get, etc.).
totalOutstanding is the headline “how much is owed” amount;
nextDueDate / nextDueAmount are the maturity date and the amount
owed by it. dueDateTime is the exact instant the loan falls due and
the overdue penalty fires if it is
still unpaid.
disbursedAt and dueDateTime on this response are computed from the
instant the disbursal was confirmed. Subsequent reads serve them from
the loan’s penalty schedule, which the platform records within about
fifteen minutes; a GET in those first minutes may omit the two fields.
The values agree to the second once present.Asymmetry to be aware of: the request
customerId carries the
customer’s externalId, but the response customerId is the
numeric LMS id (as a string) — the same value every read endpoint
returns. Your externalId still appears on the customer object itself.Errors
| Code | When |
|---|---|
invalid_argument | Missing customerId / productId / principal, principal <= 0, or principal outside the product’s minPrincipal/maxPrincipal (validation_error; the field detail is in message) |
not_found | productId does not exist (product_not_found), or no customer carries the supplied customerId externalId (customer_not_found) |
failed_precondition | The customer is in a state that disallows new loans, e.g. Closed (domain_rule_violation; the reason is in message) |
already_exists | A loan with the supplied externalId already exists (loan_external_id_exists) |

