Roll back a loan
curl --request POST \
--url https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/rollback \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"note": "<string>"
}
'import requests
url = "https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/rollback"
payload = { "note": "<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({note: '<string>'})
};
fetch('https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/rollback', 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}/rollback",
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([
'note' => '<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}/rollback"
payload := strings.NewReader("{\n \"note\": \"<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}/rollback")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"note\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/rollback")
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 \"note\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyLoans
Roll back a loan
Tear down a loan whose wallet disbursement failed — one call instead of three.
POST
/
v1
/
loans
/
external
/
{loan_external_id}
/
rollback
Roll back a loan
curl --request POST \
--url https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/rollback \
--header 'Authorization: Bearer <token>' \
--header 'Content-Type: application/json' \
--data '
{
"note": "<string>"
}
'import requests
url = "https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/rollback"
payload = { "note": "<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({note: '<string>'})
};
fetch('https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/rollback', 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}/rollback",
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([
'note' => '<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}/rollback"
payload := strings.NewReader("{\n \"note\": \"<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}/rollback")
.header("Authorization", "Bearer <token>")
.header("Content-Type", "application/json")
.body("{\n \"note\": \"<string>\"\n}")
.asString();require 'uri'
require 'net/http'
url = URI("https://api-staging.bsa.ai/v1/loans/external/{loan_external_id}/rollback")
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 \"note\": \"<string>\"\n}"
response = http.request(request)
puts response.read_bodyRemoves a loan that should never have existed — the exception flow for
a failed wallet disbursement. One call performs the full teardown
internally (
So the partner-side recovery logic collapses to: call rollback until
you get
Rolling back also cancels the loan’s pending overdue penalty in the same
call — nothing fires for a loan that was torn down.
The body is optional; POST with no body is valid.
After a successful rollback the loan no longer appears in queries —
undo-disbursal → undo-approval → delete) and returns
a single response, so your exception handler doesn’t need to chain
three synchronous requests.
The three individual endpoints remain available; rollback is the
consolidated form built on top of them.
Resumable by design
Each teardown step is durable in the LMS the moment it succeeds. If a step fails (or your call times out mid-sequence), the loan is left at a well-defined interim state — and calling rollback again resumes from that state instead of failing on steps that already ran:| Loan status when called | Steps performed |
|---|---|
Active | undo-disbursal → undo-approval → delete |
Approved | undo-approval → delete |
Submitted and pending approval | delete |
Closed (obligations met), Closed (written off), Overpaid | rejected with 409 aborted — settled loans can’t be rolled back |
200. There is no partial-failure state you need to
disambiguate yourself — but if you want to inspect anyway,
GET /v1/loans/external/{loan_external_id} reports the interim status
dynamically after every step (Active → Approved →
Submitted and pending approval → 404 once deleted).
A loan with recorded repayments is rejected with
409 aborted
(rollback_has_repayments). Rollback assumes the loan should never
have existed; once a customer has paid against it, that premise no
longer holds and erasing the payment must be an explicit decision. Have
the repayment(s) reversed by the operator team first, then roll back.Path parameters
string
required
The loan’s externalId. On the
/v1/loans/{loan_id}/rollback form,
this is the numeric LMS id instead.Request body
string
Optional. Reason for the rollback — recorded against both undo steps
in the LMS audit log.
Examples
# By loan externalId (recommended)
curl -sf -X POST "$BASE/v1/loans/external/loan-ext-12345/rollback" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"note": "Wallet credit failed"}'
# Same effect, by LMS id
curl -sf -X POST "$BASE/v1/loans/501/rollback" \
-H "Authorization: Bearer $TOKEN" \
-H "Content-Type: application/json" \
-d '{"note": "Wallet credit failed"}'
Response
200 OK with the steps that ran on this call. A rollback resumed from
an interim state reports only the remaining steps it executed.
{
"rolledBack": true,
"stepsPerformed": ["undo-disbursal", "undo-approval", "delete"]
}
GET /v1/loans/external/{loan_external_id} returns 404. The LMS
retains the full activity history (creation, approval, disbursal,
every reversal, deletion) in its audit log.
Errors
| Code | When |
|---|---|
not_found | No loan with that id or externalId (including a loan already fully rolled back) |
aborted | Loan is settled (Closed / Overpaid; loan_not_rollbackable), or has recorded repayments (rollback_has_repayments) — rollback is not applicable in either case |
| other | A teardown step failed mid-sequence. The message names the step that halted and the steps already completed; completed steps are durable — call rollback again to resume |

