Record a repayment
curl --request POST \
--url https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/repayments \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"transactionAmount": 123,
"transactionDate": "<string>",
"transactionAt": "<string>",
"idempotencyKey": "<string>"
}
'import requests
url = "https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/repayments"
payload = {
"transactionAmount": 123,
"transactionDate": "<string>",
"transactionAt": "<string>",
"idempotencyKey": "<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({
transactionAmount: 123,
transactionDate: '<string>',
transactionAt: '<string>',
idempotencyKey: '<string>'
})
};
fetch('https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/repayments', 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/external/{loan_external_id}/repayments",
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([
'transactionAmount' => 123,
'transactionDate' => '<string>',
'transactionAt' => '<string>',
'idempotencyKey' => '<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/external/{loan_external_id}/repayments"
payload := strings.NewReader("{\n \"transactionAmount\": 123,\n \"transactionDate\": \"<string>\",\n \"transactionAt\": \"<string>\",\n \"idempotencyKey\": \"<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/external/{loan_external_id}/repayments")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"transactionAmount\": 123,\n \"transactionDate\": \"<string>\",\n \"transactionAt\": \"<string>\",\n \"idempotencyKey\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/repayments")
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 \"transactionAmount\": 123,\n \"transactionDate\": \"<string>\",\n \"transactionAt\": \"<string>\",\n \"idempotencyKey\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"loanStatus": "<string>",
"totalOutstanding": 123,
"availableCreditLimit": 123
}Repayments
Record a repayment
Post a regular installment repayment.
POST
/
v1
/
loans
/
external
/
{loan_external_id}
/
repayments
Record a repayment
curl --request POST \
--url https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/repayments \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"transactionAmount": 123,
"transactionDate": "<string>",
"transactionAt": "<string>",
"idempotencyKey": "<string>"
}
'import requests
url = "https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/repayments"
payload = {
"transactionAmount": 123,
"transactionDate": "<string>",
"transactionAt": "<string>",
"idempotencyKey": "<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({
transactionAmount: 123,
transactionDate: '<string>',
transactionAt: '<string>',
idempotencyKey: '<string>'
})
};
fetch('https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/repayments', 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/external/{loan_external_id}/repayments",
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([
'transactionAmount' => 123,
'transactionDate' => '<string>',
'transactionAt' => '<string>',
'idempotencyKey' => '<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/external/{loan_external_id}/repayments"
payload := strings.NewReader("{\n \"transactionAmount\": 123,\n \"transactionDate\": \"<string>\",\n \"transactionAt\": \"<string>\",\n \"idempotencyKey\": \"<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/external/{loan_external_id}/repayments")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"transactionAmount\": 123,\n \"transactionDate\": \"<string>\",\n \"transactionAt\": \"<string>\",\n \"idempotencyKey\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/repayments")
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 \"transactionAmount\": 123,\n \"transactionDate\": \"<string>\",\n \"transactionAt\": \"<string>\",\n \"idempotencyKey\": \"<string>\"\n}"
response = http.request(request)
puts response.read_body{
"loanStatus": "<string>",
"totalOutstanding": 123,
"availableCreditLimit": 123
}Records a standard repayment against an active loan. The amount is
allocated to interest then principal according to the loan’s
processing rules.
Two equivalent forms — prefer the externalId form for partner integrations.
The position fields appear only on this POST response. Ledger reads
(list, get-one, and the
Path parameters
string
required
The loan’s externalId. On the
/v1/loans/{loan_id}/repayments form,
this is the numeric LMS id instead.Request body
number
required
Must be greater than 0.
string
Optional value date of the payment, in ISO
yyyy-MM-dd. Books the
repayment as of the day the money actually moved on your side rather
than the day you call this API — the reconciliation case where a
wallet debit succeeds but settling with us is delayed (a failure,
timeout, or a next-day retry). Defaults to the current date when
omitted. Must be on or after the loan’s disbursement date and
not in the future. Backdating across a later transaction already
recorded on the loan may be rejected by the ledger.string
Optional exact instant the money moved on your side, RFC3339 (e.g.
2026-08-08T13:15:00+03:00). Also fixes transactionDate (which must
agree if both are sent). Send it on every settlement. Penalties are
posted at the loan’s exact dueDateTime, so a date alone cannot tell a
payment made at 13:15 from one made at 13:25 on the same day: with
transactionAt the service can prove the customer paid before the
penalty instant and waive a penalty that was posted while your settle
call was in flight. See
Reconciling delayed settlements.string
Optional dedupe token — typically your wallet transaction reference
for this payment (max 100 characters). If a repayment carrying this key
has already been recorded on the loan, the original transaction is
returned unchanged: the retry neither double-posts nor re-runs penalty
reconciliation. Strongly recommended on every repayment so timeouts
and retries are safe to replay. Use a value unique per payment, and see
Timeouts and retries.
Examples
# By loan externalId (recommended)
curl -sf -X POST "$BASE/v1/loans/external/loan-ext-12345/repayments" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "transactionAmount": 11100 }'
# Backdated value date + idempotency key — settle as of the day the
# wallet was debited, safe to retry. Recommended shape for every payment.
curl -sf -X POST "$BASE/v1/loans/external/loan-ext-12345/repayments" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "transactionAmount": 11100, "transactionDate": "2026-06-01", "idempotencyKey": "wallet-txn-abc123" }'
# Same effect, by LMS id
curl -sf -X POST "$BASE/v1/loans/501/repayments" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{ "transactionAmount": 11100 }'
Response
200 OK returns the repayment object showing
how the amount was allocated, plus the loan’s post-repayment position
so you can decide what to do next without follow-up reads. The
idempotencyKey you posted is echoed back (and on every later read of
the transaction).
{
"id": "7821",
"type": "Repayment",
"date": "2026-05-25",
"amount": 11100,
"principal": 10000,
"interest": 1100,
"idempotencyKey": "wallet-txn-abc123",
"status": "posted",
"loanStatus": "Closed (obligations met)",
"totalOutstanding": 0,
"availableCreditLimit": 15000
}
string
The loan’s status after this payment —
Active,
Closed (obligations met), or Overpaid.number
What is still owed on this loan after the payment.
0 means the
loan is settled.number
What the customer can borrow now, across all their loans — the same
figure as
GET /v1/credit-scorecard. Per the
integration rule, a customer with any outstanding balance has 0
available (so after a partial repayment this is always 0); only a
customer whose last balance this payment settled sees their full
finalCreditLimit. On the rare occasion the scoring service cannot be
reached, the field is omitted — fall back to the scorecard endpoint.repaymentHistory embedded in
the loan detail) return pure transaction objects; on an idempotent replay
the position reflects the loan at the time of the retry, which is what
“remaining balance” means then.
Errors
| Code | When |
|---|---|
not_found | No loan with that id or externalId |
failed_precondition | Loan is not in an active state |
invalid_argument | Missing or non-positive transactionAmount; transactionDate not ISO yyyy-MM-dd, in the future, or before disbursement; idempotencyKey longer than 100 characters |

