RemitLend can deliver real-time event notifications to external services via webhooks. This guide covers everything an external integrator needs to subscribe, receive, and verify webhook deliveries.
- Creating a Subscription
- Supported Event Types
- Payload Examples
- Delivery & Retry Semantics
- Circuit Breaker
- Verifying HMAC Signatures
- Subscriber Response Requirements
Endpoint: POST /api/webhooks/subscriptions
Headers:
Content-Type: application/json
Authorization: Bearer <your-jwt-token>
Request body:
{
"url": "https://your-service.com/webhooks/remitlend",
"events": ["LoanApproved", "LoanRepaid", "LoanDefaulted"],
"description": "My loan tracking service (optional)"
}| Field | Type | Description |
|---|---|---|
url |
string | HTTPS endpoint that will receive POST requests |
events |
string[] | Array of event types |
description |
string | Optional human-readable label |
Response (201):
{
"success": true,
"data": {
"id": "sub_abc123",
"url": "https://your-service.com/webhooks/remitlend",
"events": ["LoanApproved", "LoanRepaid", "LoanDefaulted"],
"active": true,
"createdAt": "2026-05-28T12:00:00.000Z"
}
}After creation the subscription is immediately active. No verification handshake is required.
| Method | Endpoint | Description |
|---|---|---|
| GET | /api/webhooks/subscriptions |
List all subscriptions |
| GET | /api/webhooks/subscriptions/:id |
Get a single subscription |
| PUT | /api/webhooks/subscriptions/:id |
Update events / URL |
| DELETE | /api/webhooks/subscriptions/:id |
Delete a subscription |
The events below are what the webhook subscription system
(backend/src/services/webhookService.ts) can actually dispatch — its
SUPPORTED_WEBHOOK_EVENT_TYPES list is the source of truth. These are raw
Soroban contract event names, distinct from the friendly, snake_case
notification types (loan_approved, repayment_due, etc.) used by the
in-app /api/notifications REST API — the two are separate systems.
| Event | Description |
|---|---|
LoanRequested |
A borrower requested a new loan |
LoanApproved |
A borrower's loan has been approved |
LoanRepaid |
A repayment was received and confirmed |
LoanDefaulted |
A loan has been marked as defaulted |
CollateralLiquidated |
Collateral has been liquidated after default |
CollateralReturned |
Collateral was returned to the borrower |
CollateralDeposited |
Collateral was deposited against a loan |
CollateralReleased |
Collateral was released back to the borrower |
LateFeeCharged |
A late fee was charged on an overdue loan |
LoanExtended |
A loan's term was extended |
LoanCancelled |
A loan request was cancelled |
LoanRejected |
A loan request was rejected |
LoanRefinanced |
A loan was refinanced |
InterestRateUpdated |
The interest rate configuration changed |
DefaultTermUpdated |
The default-term configuration changed |
TermLimitsUpdated |
Loan term limits configuration changed |
LateFeeRateUpdated |
The late-fee rate configuration changed |
GracePeriodUpdated |
The grace-period configuration changed |
DefaultWindowUpdated |
The default-window configuration changed |
MaxLoanAmountUpdated |
The maximum loan amount configuration changed |
MinRepaymentUpdated |
The minimum repayment configuration changed |
MaxLoansPerBorrower |
The max-loans-per-borrower configuration changed |
MinRateBpsUpdated |
The minimum interest rate (bps) configuration changed |
MaxRateBpsUpdated |
The maximum interest rate (bps) configuration changed |
RateOracleUpdated |
The rate oracle configuration changed |
MinScoreUpdated |
The minimum credit score configuration changed |
Deposit |
A pool deposit occurred |
Withdraw |
A pool withdrawal occurred |
YieldDistributed |
Yield was distributed to pool depositors |
EmergencyWithdraw |
An emergency withdrawal occurred |
DepositCapUpdated |
The pool deposit cap configuration changed |
WithdrawalCooldownUpdated |
The withdrawal cooldown configuration changed |
NFTMinted |
A borrower/score NFT was minted |
ScoreUpdated |
A borrower's credit score changed |
NFTSeized |
A borrower/score NFT was seized |
NFTBurned |
A borrower/score NFT was burned |
ProposalCreated |
A governance proposal was created |
ProposalApproved |
A governance proposal was approved |
ProposalFinalized |
A governance proposal was finalized |
ProposalCancelled |
A governance proposal was cancelled |
ColDep |
Collateral was deposited against a loan (short form of CollateralDeposited) |
ColRel |
Collateral was released back to the borrower (short form of CollateralReleased) |
LoanApprv |
A borrower's loan has been approved (short contract event name) |
LoanLiquidated |
Collateral has been liquidated after default (alternate name for CollateralLiquidated) |
Kept for backward compatibility with existing subscribers. These are
resolved to one of the current event names above by
EVENT_TYPE_ALIASES in backend/src/services/eventIndexer.ts before
dispatch, except where noted below. New integrations should prefer the
current event names listed above where an equivalent exists.
| Event | Description |
|---|---|
Mint |
Legacy alias — resolves to NFTMinted |
AdmRemint |
Legacy alias — resolves to NFTMinted |
ScoreUpd |
Legacy alias — resolves to ScoreUpdated |
Seized |
Legacy alias — resolves to NFTSeized |
NftBurned |
Legacy alias — resolves to NFTBurned |
GovProp |
Legacy alias — resolves to ProposalCreated |
GovAppr |
Legacy alias — resolves to ProposalApproved |
GovFin |
Legacy alias — resolves to ProposalFinalized |
GovCncl |
Legacy alias — resolves to ProposalCancelled |
GovEmerg |
Legacy alias — resolves to ProposalCancelled |
GovExp |
Legacy alias — resolves to ProposalCancelled |
ScoreDecr |
Legacy event type for a score decrease — not currently aliased to another event type |
HashUpd |
Legacy event type for a content-hash update — not currently aliased to another event type |
Transfer |
Legacy event type for an NFT/asset transfer — not currently aliased to another event type |
MntAuth |
Legacy event type for a minting-authority change — not currently aliased to another event type |
MntRev |
Legacy event type for a minting-authority revocation — not currently aliased to another event type |
Paused |
Legacy event type for a contract pause — not currently aliased to another event type |
Unpaused |
Legacy event type for a contract unpause — not currently aliased to another event type |
PoolPaused |
Legacy event type for a pool pause — not currently aliased to another event type |
PoolUnpaused |
Legacy event type for a pool unpause — not currently aliased to another event type |
Every delivery is a JSON POST with the following envelope:
{
"event": "<event_type>",
"id": "<unique_delivery_id>",
"timestamp": "2026-05-28T12:00:00.000Z",
"data": { }
}{
"event": "LoanApproved",
"id": "evt_loan_42",
"timestamp": "2026-05-28T12:00:00.000Z",
"data": {
"loanId": 42,
"borrower": "GABCDEF...",
"amount": "5000",
"termMonths": 12
}
}{
"event": "LoanRepaid",
"id": "evt_repay_99",
"timestamp": "2026-05-28T12:05:00.000Z",
"data": {
"loanId": 42,
"borrower": "GABCDEF...",
"amount": "450",
"txHash": "a1b2c3d4..."
}
}{
"event": "LoanDefaulted",
"id": "evt_default_7",
"timestamp": "2026-05-28T12:10:00.000Z",
"data": {
"loanId": 42,
"borrower": "GABCDEF...",
"outstandingAmount": "3200"
}
}{
"event": "CollateralLiquidated",
"id": "evt_liq_3",
"timestamp": "2026-05-28T12:15:00.000Z",
"data": {
"loanId": 42,
"borrower": "GABCDEF...",
"collateralSeized": true,
"borrowerRefund": "150"
}
}{
"event": "LateFeeCharged",
"id": "evt_due_21",
"timestamp": "2026-05-28T12:00:00.000Z",
"data": {
"loanId": 42,
"borrower": "GABCDEF...",
"dueDate": "2026-06-01",
"amount": "450"
}
}{
"event": "ScoreUpdated",
"id": "evt_score_15",
"timestamp": "2026-05-28T12:00:00.000Z",
"data": {
"userId": "GABCDEF...",
"previousScore": 650,
"newScore": 665,
"reason": "on-time repayment"
}
}- Delivery method: HTTP POST to the subscriber URL.
- Timeout: The endpoint must respond within 10 seconds.
- Retry policy: Deliveries are retried with exponential backoff:
- Retry 1: 10 seconds
- Retry 2: 30 seconds
- Retry 3: 1 minute
- Retry 4: 5 minutes
- Retry 5: 15 minutes
- Retry 6: 30 minutes
- Retry 7: 1 hour
- Max attempts: 8 total (1 initial + 7 retries).
- Delivery window: Events older than 24 hours are not retried.
- Ordering: Webhooks are delivered on a best-effort basis and may not arrive in the exact order events occurred.
If a subscriber endpoint fails to respond with a 2xx status for 5 consecutive deliveries, the subscription is automatically deactivated to avoid wasting resources.
While deactivated:
- No further events are sent to the subscriber.
- The subscription status changes to
deactivated. - You can re-activate the subscription by calling
PUT /api/webhooks/subscriptions/:idwith{ "active": true }.
Each delivery includes an X-RemitLend-Signature header containing an
HMAC-SHA256 signature of the raw request body.
Header format:
X-RemitLend-Signature: sha256=<hex-encoded-hmac>
The value is sha256= followed by the lowercase hex-encoded HMAC-SHA256
digest computed over the raw request body (no timestamp prefix).
import { createHmac, timingSafeEqual } from "node:crypto";
function verifyWebhookSignature(
rawBody: string,
signatureHeader: string,
secret: string,
): boolean {
const expected =
"sha256=" +
createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader ?? "");
if (a.length !== b.length) return false;
return timingSafeEqual(a, b);
}
⚠️ Important: Always usetimingSafeEqual(or your language's constant-time comparison) when verifying the signature to prevent timing attacks.
The signing secret is the per-subscription secret returned in the response when you register the webhook subscription (see Creating a Subscription). It is not a global environment variable. Store it securely on your server and use it to verify each incoming delivery.
See also: docs/wiki/webhook-signatures.md for additional language examples.
| Code | Meaning |
|---|---|
| 2xx | Delivery accepted — no retry |
| 4xx | Request rejected — permanent failure (no retry) |
| 5xx | Server error — will be retried |
| Timeout | Treated as a failure — will be retried |
- Respond within 10 seconds. Slow responses are counted as failures.
- Returning any 2xx status (200, 201, 202, 204) acknowledges delivery.
Contact the RemitLend team or open an issue on GitHub for integration support.