API Reference
A comprehensive REST API for lenders integrating their own core banking or loan management system with CreditPulse's Collections and Recovery platform.
Every endpoint returns the same envelope: { success, message, data }. Protected endpoints expect Authorization: Bearer <jwt>.
Getting started
1. Register an organization
/api/auth/register-organizationcurl -X POST http://localhost:8080/api/auth/register-organization \
-H "Content-Type: application/json" \
-d '{
"organizationName": "Precision Finance",
"industryType": "MICROFINANCE_BANK",
"ownerFullName": "Ada Okafor",
"ownerEmail": "ada@precisionfinance.ng",
"password": "StrongPass123!"
}'2. Create a customer
/api/customerscurl -X POST http://localhost:8080/api/customers \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"fullName": "Chinedu Eze",
"phoneNumber": "+2348012345678",
"employerName": "Lagos Logistics Ltd",
"employmentType": "SALARIED"
}'3. Import loans
/api/loans/bulk-syncexternalLoanId is your own loan reference — sending the same one again upserts the loan instead of duplicating it, which is what makes this safe to call from a nightly sync job.
curl -X POST http://localhost:8080/api/loans/bulk-sync \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '[{
"externalLoanId": "LN-2026-0001",
"lenderProfileId": "33333333-...",
"customerFullName": "Chinedu Eze",
"principalNaira": 250000,
"outstandingBalanceNaira": 180000,
"disbursedAt": "2026-07-01T09:00:00Z",
"instalments": [
{ "instalmentNumber": 1, "dueDate": "2026-08-25", "amountDueNaira": 60000 }
]
}]'4. Initiate a debit mandate
/api/mandatesUse an Idempotency-Key header to safely retry mandate creation.
curl -X POST http://localhost:8080/api/mandates \
-H "Authorization: Bearer <jwt>" \
-H "Idempotency-Key: mandate-init-001" \
-H "Content-Type: application/json" \
-d '{
"customerId": "44444444-...",
"loanId": "55555555-...",
"bankCode": "058",
"accountNumber": "0123456789",
"maxAmountNaira": 60000,
"frequency": "MONTHLY",
"startDate": "2026-08-25",
"endDate": "2027-01-25"
}'5. Escalate to recovery
/api/recovery/cases/escalatecurl -X POST http://localhost:8080/api/recovery/cases/escalate \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"loanId": "55555555-...",
"reason": "Customer missed debit window twice and needs assisted recovery."
}'Data model at a glance
Organization (your tenant)
└─ LenderProfile the regulated lender who is the legal creditor of record
└─ Portfolio a named grouping of loans
└─ Customer a borrower
└─ Loan principal, outstanding balance, disbursement date
├─ Instalment one due payment on a loan's schedule
│ └─ DebitAttempt one attempt to collect an instalment
└─ Mandate the standing consent to debit a bank account
└─ RecoveryCase opened when a loan needs assisted recoveryLenderProfile, not Organization, is the creditor of record — CreditPulse never holds, pools, or settles customer money. Every tenant-scoped record carries a tenant_id enforced from your JWT, never a request field.
Status lifecycles
Mandate.statusPENDING → AWAITING_ACTIVATION → ACTIVE → (EXPIRED | REVOKED), or FAILED at any point before ACTIVE.
Only an ACTIVE mandate can be charged.
Instalment.statusPENDING → (PARTIALLY_PAID →) PAID, or OVERDUE once the due date passes unpaid, or WAIVED if written off.
DebitAttempt.statusPENDING → PROCESSING → SUCCESSFUL | FAILED | REVERSED | REFUNDED.
Each attempt is immutable once resolved — a retry creates a new row.
RecoveryCase.statusOPEN → IN_PROGRESS → (PTP_PENDING →) RESOLVED | ESCALATED | CLOSED.
Auth & access control
Every endpoint besides the ones below requires a bearer token, and most also require a specific permission — CreditPulse uses role-based access control, not "any authenticated user can call anything."
Public endpoints (no token required)
- POST /api/auth/register-organization
- POST /api/auth/login
- POST /api/auth/refresh
- POST /api/auth/verify-email?token=...
- POST /api/demo-requests
- GET /api/public/mandates/{id}/status
- GET /api/docs, GET /api/docs/markdown
Permission catalog
Assign these to roles via POST /api/roles. If your integration calls the API as a background job, create a dedicated service user with only the permissions its path needs, rather than reusing an owner/admin token.
/api/auth/register-organizationCreates a tenant, owner account, and initial JWT pair. The one call every integration starts with.organizationName* | string | Your business's display name. |
industryType* | enum | e.g. MICROFINANCE_BANK. |
ownerFullName* | string | Full name of the first (owner) account. |
ownerEmail* | string | Must be a valid, unique email address. |
password* | string | Minimum 10 characters. |
{
"organizationName": "<organizationName>",
"industryType": "<industryType>",
"ownerFullName": "<ownerFullName>",
"ownerEmail": "<ownerEmail>",
"password": "<password>"
}/api/auth/loginSigns a user in and returns a JWT access/refresh pair.email* | string | Account email. |
password* | string | Account password. |
{
"email": "<email>",
"password": "<password>"
}/api/auth/refreshExchanges a refresh token for a new access/refresh pair.refreshToken* | string | A previously issued refresh token. |
{
"refreshToken": "<refreshToken>"
}/api/auth/verify-email?token=...Verifies the emailed token.token* | string | Token from the verification email link. |
/api/usersLists users for the current tenant./api/usersCreates a staff user under your organization.email* | string | Must be a valid, unique email address. |
fullName* | string | Display name. |
password* | string | Minimum 10 characters. |
roleIds* | UUID[] | At least one role id, from GET /api/roles. |
{
"email": "<email>",
"fullName": "<fullName>",
"password": "<password>",
"roleIds": ["11111111-1111-1111-1111-111111111111"]
}/api/users/{id}/rolesReplaces a user's role assignments.roleIds* | UUID[] | The full new set of role ids — this replaces, not appends. |
{
"roleIds": ["11111111-1111-1111-1111-111111111111"]
}/api/users/{id}/activeActivates or deactivates a user.active* | boolean | true to activate, false to deactivate. |
/api/rolesLists roles./api/roles/permissionsLists every permission the platform recognizes, for building a custom role./api/rolesCreates a custom role.name* | string | Role display name. |
description | string | Optional free text. |
permissions | enum[] | Permission constants from GET /api/roles/permissions. |
{
"name": "<name>",
"description": "<description>",
"permissions": "<permissions>"
}/api/roles/{id}Updates a role's name, description, and permission set.name* | string | Role display name. |
description | string | Optional free text. |
permissions | enum[] | Full replacement permission set. |
{
"name": "<name>",
"description": "<description>",
"permissions": "<permissions>"
}/api/roles/{id}Deletes a custom role.No request body — path parameters only.
Customers
/api/customersCreates a borrower/customer profile.fullName* | string | Borrower's full name. |
phoneNumber* | string | Nigerian mobile number, used for SMS/WhatsApp/voice outreach. |
email | string | Optional email address. |
employerName | string | Used alongside salary observations for pay-date prediction. |
employmentType | string | e.g. SALARIED. |
preferredChannel | enum | SMS, WHATSAPP, VOICE, or EMAIL. |
{
"fullName": "<fullName>",
"phoneNumber": "<phoneNumber>",
"email": "<email>",
"employerName": "<employerName>",
"employmentType": "<employmentType>",
"preferredChannel": "<preferredChannel>"
}/api/customers/{id}Fetches one customer./api/customers?page=0&size=20Lists customers with pagination./api/customers/{customerId}/salary-observationsRecords a salary observation used as an input to pay-date prediction.source* | enum | Where this observation came from (e.g. bank statement, employer confirmation). |
observedDate* | date | The date this salary payment was observed. |
amountMinor | number | Amount in kobo, if known. |
note | string | Free-text context. |
{
"source": "<source>",
"observedDate": "2026-08-25",
"amountMinor": 1000,
"note": "<note>"
}/api/customers/{customerId}/salary-prediction/latestReturns the latest salary/pay-date prediction./api/customers/{customerId}/salary-prediction/recomputeForces a prediction refresh from currently recorded observations.No request body — path parameters only.
/api/customers/{customerId}/salary-observationsLists recorded salary observations.Loans & portfolios
/api/loans/bulk-syncImports or upserts loans from a JSON array. The main sync endpoint for a nightly job or a disbursement-triggered call.externalLoanId* | string | Your own loan reference. Sending the same value again updates that loan instead of creating a duplicate. |
lenderProfileId* | UUID | From GET /api/lender-profiles. |
portfolioId | UUID | From GET /api/portfolios. Preferred over portfolioName — must already exist, and the loan fails with a clear per-row error if it doesn't, instead of silently creating a new portfolio from a typo. Takes precedence if both are given. |
portfolioName | string | Created automatically if it doesn't already exist. Provide this only if you don't want to look up an id first; either portfolioId or portfolioName is required. |
customerFullName* | string | Matched/created against your Customers. |
customerPhone* | string | Nigerian mobile number. |
customerEmail | string | Optional. |
principalNaira* | number | Original loan amount, in whole Naira. |
outstandingBalanceNaira* | number | Current balance, in whole Naira. |
disbursedAt* | datetime | ISO-8601 disbursement timestamp. |
instalments* | object[] | At least one: { instalmentNumber, dueDate, amountDueNaira }. |
[
{
"externalLoanId": "LN-2026-0001",
"lenderProfileId": "33333333-3333-3333-3333-333333333333",
"portfolioId": "44444444-4444-4444-4444-444444444444",
"customerFullName": "Chinedu Eze",
"customerPhone": "+2348012345678",
"customerEmail": "chinedu@example.com",
"principalNaira": 250000,
"outstandingBalanceNaira": 180000,
"disbursedAt": "2026-07-01T09:00:00Z",
"instalments": [
{ "instalmentNumber": 1, "dueDate": "2026-08-25", "amountDueNaira": 60000 }
]
}
]/api/loans/import-csvImports loans from a CSV file upload (multipart/form-data) — an alternative to bulk-sync for a one-off manual import, using the same fields as column headers./api/loans/{id}Fetches one loan./api/loansLists loans with pagination./api/portfoliosCreates a portfolio (a named grouping of loans).lenderProfileId* | UUID | The lender profile this portfolio belongs to. |
name* | string | e.g. "Salary Loans Q3". |
description | string | Optional free text. |
loanProduct | string | Optional product label. |
{
"lenderProfileId": "11111111-1111-1111-1111-111111111111",
"name": "<name>",
"description": "<description>",
"loanProduct": "<loanProduct>"
}/api/portfoliosLists portfolios./api/lender-profilesCreates a lender profile — the regulated entity that is the legal creditor of record.name* | string | Legal/trading name. |
licenseType | string | e.g. microfinance bank licence class. |
licenseNumber | string | Regulator-issued licence number. |
regulatorName | string | e.g. Central Bank of Nigeria. |
{
"name": "<name>",
"licenseType": "<licenseType>",
"licenseNumber": "<licenseNumber>",
"regulatorName": "<regulatorName>"
}/api/lender-profilesLists lender profiles./api/lender-profiles/{id}/activeEnables or disables a lender profile.active* | boolean | true to enable, false to disable. |
Mandates & collections
/api/mandatesInitiates a direct debit mandate. Accepts an Idempotency-Key header — reuse the same key to safely retry.customerId* | UUID | The borrower authorizing this mandate. |
loanId | UUID | The loan this mandate will collect against. |
customerEmail* | string | Used in the consent flow. |
customerPhone* | string | Used in the consent flow. |
customerAddress* | string | Required by the mandate provider. |
lenderProfileId* | UUID | The lender this mandate is for — determines which provider's bank list bankCode must come from. |
bankCode* | string | From GET /api/mandates/banks?lenderProfileId=... for this same lender. |
bankName* | string | Human-readable bank name. |
accountNumber* | string | 10-digit NUBAN account number. |
maxAmountNaira* | number | The ceiling this mandate can ever be charged per debit. |
frequency* | enum | DAILY, WEEKLY, BI_WEEKLY, or MONTHLY. |
startDate* | date | First date this mandate may be used. |
endDate* | date | Last date this mandate may be used. |
{
"customerId": "11111111-1111-1111-1111-111111111111",
"loanId": "11111111-1111-1111-1111-111111111111",
"customerEmail": "<customerEmail>",
"customerPhone": "<customerPhone>",
"customerAddress": "<customerAddress>",
"lenderProfileId": "11111111-1111-1111-1111-111111111111",
"bankCode": "<bankCode>",
"bankName": "<bankName>",
"accountNumber": "<accountNumber>",
"maxAmountNaira": 1000,
"frequency": "<frequency>",
"startDate": "2026-08-25",
"endDate": "2026-08-25"
}/api/mandates/{id}/activateManual override to force-activate a mandate (e.g. the customer confirmed activation by phone) when automatic polling isn't usable.reason* | string | Required for the audit trail — why this was manually activated. |
{
"reason": "<reason>"
}/api/mandates/{id}/revokeRevokes an active mandate.reason* | string | Required for the audit trail. |
{
"reason": "<reason>"
}/api/mandates/banks?lenderProfileId=...Lists the supported banks and their codes for the given lender's mandate provider — bank codes are provider-specific, so always fetch this scoped to the lenderProfileId you'll submit the mandate under.lenderProfileId* | UUID | Determines which provider's bank list is returned. |
/api/mandates/resolve-account?lenderProfileId=...&bankCode=058&accountNumber=0123456789Resolves the account holder's name before mandate setup, so you can confirm it matches your borrower.lenderProfileId* | UUID | Must match the lender you'll submit the mandate under. |
bankCode* | string | From GET /api/mandates/banks for this same lenderProfileId. |
accountNumber* | string | 10-digit NUBAN account number. |
/api/mandates/{id}Fetches one mandate — check status here before relying on it for collections./api/mandatesLists mandates./api/public/mandates/{id}/statusPublic status check for customer self-service (no auth required)./api/mandates/{mandateId}/account-statementUploads a bank statement (multipart/form-data) for name-match validation against the mandate's account holder./api/mandates/{mandateId}/account-statementRetrieves statement upload metadata and match result./api/collections/dashboardSummary metrics for collections./api/collections/queueQueue of instalments ready for collection work./api/collections/debit-calendarScheduled debit calendar./api/collections/cases/{instalmentId}Detailed collection case view for one instalment./api/instalments/{instalmentId}/notice-sentMarks the required pre-debit notice as sent for this instalment's current cycle. A debit cannot be attempted until this is recorded.No request body — path parameters only.
/api/instalments/{instalmentId}/debit-attemptsTriggers a debit attempt for this instalment against its active mandate.No request body — path parameters only.
/api/instalments/{instalmentId}/debit-attemptsLists debit attempts for one instalment./api/debit-attempts/{id}Fetches one debit attempt./api/debit-attemptsLists debit attempts using filters/pagination./api/collection-policiesCreates a collection policy — the rules governing notice timing and retry behavior.portfolioId | UUID | Scope this policy to one portfolio, or omit for organization-wide. |
name* | string | Policy display name. |
description | string | Optional free text. |
noticeHoursRequired* | integer | Minimum hours between notice and debit attempt. |
maxAttemptsPerInstalment* | integer | Retry ceiling before a policy stops attempting an instalment. |
retryIntervalHours* | integer | Hours to wait between retries. |
{
"portfolioId": "11111111-1111-1111-1111-111111111111",
"name": "<name>",
"description": "<description>",
"noticeHoursRequired": 1,
"maxAttemptsPerInstalment": 1,
"retryIntervalHours": 1
}/api/collection-policies/{id}/activeActivates or deactivates a collection policy.active* | boolean | true to activate, false to deactivate. |
/api/collection-policiesLists collection policies.Recovery
/api/recovery/dashboardHigh-level recovery KPIs./api/recovery/cases/escalateMoves a loan into recovery.loanId* | UUID | The loan to escalate. |
reason* | string | Why this loan is being escalated — becomes part of the case record. |
{
"loanId": "11111111-1111-1111-1111-111111111111",
"reason": "<reason>"
}/api/recovery/cases/queueLists active recovery cases./api/recovery/cases/{id}Returns the basic recovery case view./api/recovery/cases/{id}/detailReturns full case detail (contacts, promises, disputes) for the case workspace./api/recovery/cases/{id}/assignAssigns an agent to the case.agentId* | UUID | The staff user id to assign. |
/api/recovery/cases/{id}/statusUpdates case status.status* | enum | OPEN, IN_PROGRESS, PTP_PENDING, RESOLVED, ESCALATED, or CLOSED. |
/api/recovery/cases/{id}/contactsLogs a contact attempt or outcome against a case.channel* | enum | e.g. CALL, SMS, WHATSAPP. |
direction* | enum | INBOUND or OUTBOUND. |
outcome* | enum | e.g. ANSWERED, NO_ANSWER, PROMISED_TO_PAY. |
notes | string | Free-text detail. |
{
"channel": "<channel>",
"direction": "<direction>",
"outcome": "<outcome>",
"notes": "<notes>"
}/api/recovery/cases/{id}/contactsReturns contact history./api/recovery/cases/{id}/eligibilityEvaluates whether this case can be contacted right now under the active contact policy (quiet hours, frequency caps)./api/recovery/cases/{caseId}/messagesSends a message to the case's borrower using a saved template.templateId* | UUID | From GET /api/message-templates. |
{
"templateId": "11111111-1111-1111-1111-111111111111"
}/api/recovery/cases/{caseId}/messagesLists case messages./api/recovery/cases/{caseId}/callsPlaces an outbound recovery call for this case. Accepts an optional Idempotency-Key header.No request body — path parameters only.
/api/recovery/cases/{caseId}/callsLists calls for a case./api/calls/{id}Fetches one call, including outcome and transcript summary once available./api/recovery/cases/{caseId}/promises-to-payRecords a promise to pay against a case.promisedAmountNaira* | number | Amount the borrower committed to pay. |
promisedDate* | date | Date the borrower committed to pay by. |
notes | string | Optional context. |
{
"promisedAmountNaira": 1000,
"promisedDate": "2026-08-25",
"notes": "<notes>"
}/api/recovery/cases/{caseId}/promises-to-payLists promises to pay for one case./api/promises-to-payLists promises to pay across all cases./api/promises-to-pay/{id}/keepMarks a promise as kept.No request body — path parameters only.
/api/promises-to-pay/{id}/breakMarks a promise as broken.No request body — path parameters only.
/api/promises-to-pay/{id}/cancelCancels a promise.No request body — path parameters only.
/api/disputesRaises a dispute against a loan.loanId* | UUID | The disputed loan. |
recoveryCaseId | UUID | If the loan is already in recovery. |
description* | string | What the borrower is disputing. |
{
"loanId": "11111111-1111-1111-1111-111111111111",
"recoveryCaseId": "11111111-1111-1111-1111-111111111111",
"description": "<description>"
}/api/disputes/{id}/resolveResolves a dispute in the borrower's favor or as settled.resolutionNotes* | string | How this was resolved. |
{
"resolutionNotes": "<resolutionNotes>"
}/api/disputes/{id}/rejectRejects a dispute.resolutionNotes* | string | Why this was rejected. |
{
"resolutionNotes": "<resolutionNotes>"
}/api/disputesLists disputes./api/hardship-casesOpens a hardship case for a borrower.loanId* | UUID | The affected loan. |
recoveryCaseId | UUID | If the loan is already in recovery. |
description* | string | The borrower's hardship circumstances. |
{
"loanId": "11111111-1111-1111-1111-111111111111",
"recoveryCaseId": "11111111-1111-1111-1111-111111111111",
"description": "<description>"
}/api/hardship-cases/{id}/approveApproves a hardship request.resolutionNotes* | string | Approval terms/notes. |
{
"resolutionNotes": "<resolutionNotes>"
}/api/hardship-cases/{id}/denyDenies a hardship request.resolutionNotes* | string | Reason for denial. |
{
"resolutionNotes": "<resolutionNotes>"
}/api/hardship-cases/{id}/resolveResolves the hardship case.resolutionNotes* | string | Resolution notes. |
{
"resolutionNotes": "<resolutionNotes>"
}/api/hardship-casesLists hardship cases./api/recovery-workflowsCreates a recovery workflow.name* | string | Workflow display name. |
description | string | Optional free text. |
stepsJson* | string | JSON-encoded step definition — build it in the CreditPulse workflow builder UI and copy it here, rather than hand-authoring. |
{
"name": "<name>",
"description": "<description>",
"stepsJson": "<stepsJson>"
}/api/recovery-workflows/{id}Updates a workflow.name* | string | Workflow display name. |
description | string | Optional free text. |
stepsJson* | string | JSON-encoded step definition. |
{
"name": "<name>",
"description": "<description>",
"stepsJson": "<stepsJson>"
}/api/recovery-workflows/{id}/activeActivates or deactivates a workflow.active* | boolean | true to activate, false to deactivate. |
/api/recovery-workflowsLists workflows./api/recovery-workflows/{id}Fetches one workflow./api/contact-policiesCreates a contact policy — quiet hours and contact-frequency limits.portfolioId | UUID | Scope to one portfolio, or omit for organization-wide. |
quietHoursStart* | time | e.g. 21:00 — no outreach after this time. |
quietHoursEnd* | time | e.g. 08:00 — no outreach before this time. |
maxContactsPerDay* | integer | Hard ceiling per borrower per day. |
maxContactsPerWeek* | integer | Hard ceiling per borrower per week. |
minHoursBetweenContacts* | integer | Minimum spacing between two contact attempts. |
{
"portfolioId": "11111111-1111-1111-1111-111111111111",
"quietHoursStart": "21:00",
"quietHoursEnd": "21:00",
"maxContactsPerDay": 1,
"maxContactsPerWeek": 1,
"minHoursBetweenContacts": 1
}/api/contact-policies/{id}/activeActivates or deactivates a contact policy.active* | boolean | true to activate, false to deactivate. |
/api/contact-policiesLists contact policies./api/message-templatesCreates a message template.name* | string | Template display name. |
channel* | enum | SMS, WHATSAPP, or VOICE. |
body* | string | Template text, with placeholders your recovery workflow fills in. |
{
"name": "<name>",
"channel": "<channel>",
"body": "<body>"
}/api/message-templatesLists templates./api/message-templates/{id}/activeActivates or deactivates a template.active* | boolean | true to activate, false to deactivate. |
Rules & reports
/api/rulesCreates a new rule group as a draft (version 1).name* | string | Rule display name. |
description | string | Optional free text. |
module* | enum | Which part of the platform this rule governs (e.g. COLLECTIONS, RECOVERY). |
jurisdiction | string | e.g. "NG" — for your own record-keeping of where this rule applies. |
scope | string | Optional scoping label. |
conditionsJson* | string | JSON-encoded condition logic. |
actionJson* | string | JSON-encoded action to take when conditions match. |
priority* | integer | Evaluation order relative to other rules in the same module. |
effectiveStartDate* | date | When this rule starts applying. |
effectiveEndDate | date | Optional expiry date. |
sourceReference | string | The legal/policy source this rule's numbers came from — recommended for anything compliance-adjacent. |
{
"name": "<name>",
"description": "<description>",
"module": "<module>",
"jurisdiction": "<jurisdiction>",
"scope": "<scope>",
"conditionsJson": "<conditionsJson>",
"actionJson": "<actionJson>",
"priority": 1,
"effectiveStartDate": "2026-08-25",
"effectiveEndDate": "2026-08-25",
"sourceReference": "<sourceReference>"
}/api/rules/groups/{ruleGroupId}/versionsCreates a new draft version for an existing rule group.(same fields as POST /api/rules)* | — | A new version replaces the group's active configuration once published. |
{
"(same fields as POST /api/rules)": "<(same fields as POST /api/rules)>"
}/api/rules/{id}/simulateRuns this draft rule against current data to preview its effect before publishing.No request body — path parameters only.
/api/rules/{id}/approveApproves a simulated draft rule.approverUserId* | UUID | The approving user — recorded on the rule for audit purposes. |
/api/rules/{id}/publishPublishes an approved rule, making it live.No request body — path parameters only.
/api/rules/{id}/retireRetires a published rule.No request body — path parameters only.
/api/rules/{id}/conflictsReturns other rules that overlap with this one's scope/priority./api/rulesLists rules./api/rules/groups/{ruleGroupId}/versionsLists version history for a rule group./api/rules/{id}Fetches one rule./api/reports/portfolioPortfolio report snapshot./api/reports/collections-performance?from=2026-08-01&to=2026-08-31Collections performance report for a date range.from* | date | Range start. |
to* | date | Range end. |
/api/reports/agent-performanceAgent performance report./api/audit-eventsAudit trail listing — every automated and manual action, tied to the rule that authorized it./api/organizationCurrent tenant organization record./api/organizationUpdates organization settings.name* | string | Organization display name. |
industryType* | enum | e.g. MICROFINANCE_BANK. |
currency* | string | e.g. NGN. |
timezone* | string | e.g. Africa/Lagos. |
{
"name": "<name>",
"industryType": "<industryType>",
"currency": "<currency>",
"timezone": "<timezone>"
}Wallet & billing
Every SMS, WhatsApp message, and voice-call minute your recovery workflows send is metered against a prepaid wallet balance, at the platform's published per-unit cost. Most integrators only need the balance and top-up routes.
/api/walletCurrent wallet balance./api/wallet/transactionsPaginated wallet debit/credit history./api/wallet/topupStarts a top-up and returns a payment authorization URL to redirect your user to.amountNaira* | number | Minimum ₦100. |
{
"amountNaira": 1000
}/api/wallet/topup/{id}/confirmConfirms a top-up after the customer completes payment at the authorization URL.No request body — path parameters only.
Real-time updates
CreditPulse currently receives webhooks; it does not yet send them. It has inbound webhook routes from its own payment and communications providers — that's how CreditPulse finds out a mandate activated or a call ended — but those exist between CreditPulse and its vendors, not between CreditPulse and you. There is no outbound mechanism today for CreditPulse to push an event (mandate activated, debit succeeded, case resolved) to your system the moment it happens.
Until that exists, poll the relevant GET endpoint on an interval matched to how time-sensitive that state is to you. If real-time push is a hard requirement, raise it before go-live — it changes what needs to be built, not just how you poll.
Errors & status codes
| Status | When it happens | data shape |
|---|---|---|
| 400 | Request body failed field validation | One entry per invalid field |
| 400 | Business-rule violation (invalid state for the action) | null, reason in message |
| 401 | Missing, expired, or invalid credentials | null |
| 403 | Authenticated but missing the required permission | null |
| 404 | Resource doesn't exist in your tenant | null |
| 409 | Conflicts with existing state (e.g. duplicate externalLoanId) | null |
| 500 | Unexpected server-side failure — treat as retryable | null |