FOR ENGINEERS

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

POST/api/auth/register-organization
cURL
curl -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

POST/api/customers
cURL
curl -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

POST/api/loans/bulk-sync

externalLoanId 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
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

POST/api/mandates

Use an Idempotency-Key header to safely retry mandate creation.

cURL
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

POST/api/recovery/cases/escalate
cURL
curl -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 recovery

LenderProfile, 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.status

PENDING → AWAITING_ACTIVATION → ACTIVE → (EXPIRED | REVOKED), or FAILED at any point before ACTIVE.

Only an ACTIVE mandate can be charged.

Instalment.status

PENDING → (PARTIALLY_PAID →) PAID, or OVERDUE once the due date passes unpaid, or WAIVED if written off.

DebitAttempt.status

PENDING → PROCESSING → SUCCESSFUL | FAILED | REVERSED | REFUNDED.

Each attempt is immutable once resolved — a retry creates a new row.

RecoveryCase.status

OPEN → 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.

ORG_MANAGELENDERS_MANAGEPORTFOLIOS_MANAGEUSERS_MANAGEROLES_MANAGELOANS_MANAGECUSTOMERS_MANAGEMANDATES_MANAGESALARY_PREDICTIONS_MANAGECOLLECTION_POLICY_MANAGEDEBITS_ATTEMPTRECOVERY_CASES_VIEWRECOVERY_CASES_MANAGECALLS_PLACEMESSAGES_SENDMESSAGE_TEMPLATES_MANAGEPROMISES_TO_PAY_MANAGEDISPUTES_MANAGEHARDSHIP_MANAGECONTACT_POLICY_MANAGERECOVERY_WORKFLOWS_MANAGERULES_MANAGERULES_APPROVERULES_PUBLISHAUDIT_LOG_VIEWWALLET_MANAGE
POST
/api/auth/register-organizationCreates a tenant, owner account, and initial JWT pair. The one call every integration starts with.
REQUEST BODY
organizationName*stringYour business's display name.
industryType*enume.g. MICROFINANCE_BANK.
ownerFullName*stringFull name of the first (owner) account.
ownerEmail*stringMust be a valid, unique email address.
password*stringMinimum 10 characters.
Sample request body
{
  "organizationName": "<organizationName>",
  "industryType": "<industryType>",
  "ownerFullName": "<ownerFullName>",
  "ownerEmail": "<ownerEmail>",
  "password": "<password>"
}
POST
/api/auth/loginSigns a user in and returns a JWT access/refresh pair.
REQUEST BODY
email*stringAccount email.
password*stringAccount password.
Sample request body
{
  "email": "<email>",
  "password": "<password>"
}
POST
/api/auth/refreshExchanges a refresh token for a new access/refresh pair.
REQUEST BODY
refreshToken*stringA previously issued refresh token.
Sample request body
{
  "refreshToken": "<refreshToken>"
}
POST
/api/auth/verify-email?token=...Verifies the emailed token.
QUERY PARAMETERS
token*stringToken from the verification email link.
GET
/api/usersLists users for the current tenant.
POST
/api/usersCreates a staff user under your organization.
REQUEST BODY
email*stringMust be a valid, unique email address.
fullName*stringDisplay name.
password*stringMinimum 10 characters.
roleIds*UUID[]At least one role id, from GET /api/roles.
Sample request body
{
  "email": "<email>",
  "fullName": "<fullName>",
  "password": "<password>",
  "roleIds": ["11111111-1111-1111-1111-111111111111"]
}
PUT
/api/users/{id}/rolesReplaces a user's role assignments.
REQUEST BODY
roleIds*UUID[]The full new set of role ids — this replaces, not appends.
Sample request body
{
  "roleIds": ["11111111-1111-1111-1111-111111111111"]
}
PUT
/api/users/{id}/activeActivates or deactivates a user.
QUERY PARAMETERS
active*booleantrue to activate, false to deactivate.
GET
/api/rolesLists roles.
GET
/api/roles/permissionsLists every permission the platform recognizes, for building a custom role.
POST
/api/rolesCreates a custom role.
REQUEST BODY
name*stringRole display name.
descriptionstringOptional free text.
permissionsenum[]Permission constants from GET /api/roles/permissions.
Sample request body
{
  "name": "<name>",
  "description": "<description>",
  "permissions": "<permissions>"
}
PUT
/api/roles/{id}Updates a role's name, description, and permission set.
REQUEST BODY
name*stringRole display name.
descriptionstringOptional free text.
permissionsenum[]Full replacement permission set.
Sample request body
{
  "name": "<name>",
  "description": "<description>",
  "permissions": "<permissions>"
}
DELETE
/api/roles/{id}Deletes a custom role.

No request body — path parameters only.

Customers

POST
/api/customersCreates a borrower/customer profile.
REQUEST BODY
fullName*stringBorrower's full name.
phoneNumber*stringNigerian mobile number, used for SMS/WhatsApp/voice outreach.
emailstringOptional email address.
employerNamestringUsed alongside salary observations for pay-date prediction.
employmentTypestringe.g. SALARIED.
preferredChannelenumSMS, WHATSAPP, VOICE, or EMAIL.
Sample request body
{
  "fullName": "<fullName>",
  "phoneNumber": "<phoneNumber>",
  "email": "<email>",
  "employerName": "<employerName>",
  "employmentType": "<employmentType>",
  "preferredChannel": "<preferredChannel>"
}
GET
/api/customers/{id}Fetches one customer.
GET
/api/customers?page=0&size=20Lists customers with pagination.
POST
/api/customers/{customerId}/salary-observationsRecords a salary observation used as an input to pay-date prediction.
REQUEST BODY
source*enumWhere this observation came from (e.g. bank statement, employer confirmation).
observedDate*dateThe date this salary payment was observed.
amountMinornumberAmount in kobo, if known.
notestringFree-text context.
Sample request body
{
  "source": "<source>",
  "observedDate": "2026-08-25",
  "amountMinor": 1000,
  "note": "<note>"
}
GET
/api/customers/{customerId}/salary-prediction/latestReturns the latest salary/pay-date prediction.
POST
/api/customers/{customerId}/salary-prediction/recomputeForces a prediction refresh from currently recorded observations.

No request body — path parameters only.

GET
/api/customers/{customerId}/salary-observationsLists recorded salary observations.

Loans & portfolios

POST
/api/loans/bulk-syncImports or upserts loans from a JSON array. The main sync endpoint for a nightly job or a disbursement-triggered call.
REQUEST BODY
externalLoanId*stringYour own loan reference. Sending the same value again updates that loan instead of creating a duplicate.
lenderProfileId*UUIDFrom GET /api/lender-profiles.
portfolioIdUUIDFrom 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.
portfolioNamestringCreated 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*stringMatched/created against your Customers.
customerPhone*stringNigerian mobile number.
customerEmailstringOptional.
principalNaira*numberOriginal loan amount, in whole Naira.
outstandingBalanceNaira*numberCurrent balance, in whole Naira.
disbursedAt*datetimeISO-8601 disbursement timestamp.
instalments*object[]At least one: { instalmentNumber, dueDate, amountDueNaira }.
Sample request body
[
  {
    "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 }
    ]
  }
]
POST
/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.
GET
/api/loans/{id}Fetches one loan.
GET
/api/loansLists loans with pagination.
POST
/api/portfoliosCreates a portfolio (a named grouping of loans).
REQUEST BODY
lenderProfileId*UUIDThe lender profile this portfolio belongs to.
name*stringe.g. "Salary Loans Q3".
descriptionstringOptional free text.
loanProductstringOptional product label.
Sample request body
{
  "lenderProfileId": "11111111-1111-1111-1111-111111111111",
  "name": "<name>",
  "description": "<description>",
  "loanProduct": "<loanProduct>"
}
GET
/api/portfoliosLists portfolios.
POST
/api/lender-profilesCreates a lender profile — the regulated entity that is the legal creditor of record.
REQUEST BODY
name*stringLegal/trading name.
licenseTypestringe.g. microfinance bank licence class.
licenseNumberstringRegulator-issued licence number.
regulatorNamestringe.g. Central Bank of Nigeria.
Sample request body
{
  "name": "<name>",
  "licenseType": "<licenseType>",
  "licenseNumber": "<licenseNumber>",
  "regulatorName": "<regulatorName>"
}
GET
/api/lender-profilesLists lender profiles.
PUT
/api/lender-profiles/{id}/activeEnables or disables a lender profile.
QUERY PARAMETERS
active*booleantrue to enable, false to disable.

Mandates & collections

POST
/api/mandatesInitiates a direct debit mandate. Accepts an Idempotency-Key header — reuse the same key to safely retry.
REQUEST BODY
customerId*UUIDThe borrower authorizing this mandate.
loanIdUUIDThe loan this mandate will collect against.
customerEmail*stringUsed in the consent flow.
customerPhone*stringUsed in the consent flow.
customerAddress*stringRequired by the mandate provider.
lenderProfileId*UUIDThe lender this mandate is for — determines which provider's bank list bankCode must come from.
bankCode*stringFrom GET /api/mandates/banks?lenderProfileId=... for this same lender.
bankName*stringHuman-readable bank name.
accountNumber*string10-digit NUBAN account number.
maxAmountNaira*numberThe ceiling this mandate can ever be charged per debit.
frequency*enumDAILY, WEEKLY, BI_WEEKLY, or MONTHLY.
startDate*dateFirst date this mandate may be used.
endDate*dateLast date this mandate may be used.
Sample request body
{
  "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"
}
POST
/api/mandates/{id}/activateManual override to force-activate a mandate (e.g. the customer confirmed activation by phone) when automatic polling isn't usable.
REQUEST BODY
reason*stringRequired for the audit trail — why this was manually activated.
Sample request body
{
  "reason": "<reason>"
}
POST
/api/mandates/{id}/revokeRevokes an active mandate.
REQUEST BODY
reason*stringRequired for the audit trail.
Sample request body
{
  "reason": "<reason>"
}
GET
/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.
QUERY PARAMETERS
lenderProfileId*UUIDDetermines which provider's bank list is returned.
GET
/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.
QUERY PARAMETERS
lenderProfileId*UUIDMust match the lender you'll submit the mandate under.
bankCode*stringFrom GET /api/mandates/banks for this same lenderProfileId.
accountNumber*string10-digit NUBAN account number.
GET
/api/mandates/{id}Fetches one mandate — check status here before relying on it for collections.
GET
/api/mandatesLists mandates.
GET
/api/public/mandates/{id}/statusPublic status check for customer self-service (no auth required).
POST
/api/mandates/{mandateId}/account-statementUploads a bank statement (multipart/form-data) for name-match validation against the mandate's account holder.
GET
/api/mandates/{mandateId}/account-statementRetrieves statement upload metadata and match result.
GET
/api/collections/dashboardSummary metrics for collections.
GET
/api/collections/queueQueue of instalments ready for collection work.
GET
/api/collections/debit-calendarScheduled debit calendar.
GET
/api/collections/cases/{instalmentId}Detailed collection case view for one instalment.
POST
/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.

POST
/api/instalments/{instalmentId}/debit-attemptsTriggers a debit attempt for this instalment against its active mandate.

No request body — path parameters only.

GET
/api/instalments/{instalmentId}/debit-attemptsLists debit attempts for one instalment.
GET
/api/debit-attempts/{id}Fetches one debit attempt.
GET
/api/debit-attemptsLists debit attempts using filters/pagination.
POST
/api/collection-policiesCreates a collection policy — the rules governing notice timing and retry behavior.
REQUEST BODY
portfolioIdUUIDScope this policy to one portfolio, or omit for organization-wide.
name*stringPolicy display name.
descriptionstringOptional free text.
noticeHoursRequired*integerMinimum hours between notice and debit attempt.
maxAttemptsPerInstalment*integerRetry ceiling before a policy stops attempting an instalment.
retryIntervalHours*integerHours to wait between retries.
Sample request body
{
  "portfolioId": "11111111-1111-1111-1111-111111111111",
  "name": "<name>",
  "description": "<description>",
  "noticeHoursRequired": 1,
  "maxAttemptsPerInstalment": 1,
  "retryIntervalHours": 1
}
PUT
/api/collection-policies/{id}/activeActivates or deactivates a collection policy.
QUERY PARAMETERS
active*booleantrue to activate, false to deactivate.
GET
/api/collection-policiesLists collection policies.

Recovery

GET
/api/recovery/dashboardHigh-level recovery KPIs.
POST
/api/recovery/cases/escalateMoves a loan into recovery.
REQUEST BODY
loanId*UUIDThe loan to escalate.
reason*stringWhy this loan is being escalated — becomes part of the case record.
Sample request body
{
  "loanId": "11111111-1111-1111-1111-111111111111",
  "reason": "<reason>"
}
GET
/api/recovery/cases/queueLists active recovery cases.
GET
/api/recovery/cases/{id}Returns the basic recovery case view.
GET
/api/recovery/cases/{id}/detailReturns full case detail (contacts, promises, disputes) for the case workspace.
PUT
/api/recovery/cases/{id}/assignAssigns an agent to the case.
QUERY PARAMETERS
agentId*UUIDThe staff user id to assign.
PUT
/api/recovery/cases/{id}/statusUpdates case status.
QUERY PARAMETERS
status*enumOPEN, IN_PROGRESS, PTP_PENDING, RESOLVED, ESCALATED, or CLOSED.
POST
/api/recovery/cases/{id}/contactsLogs a contact attempt or outcome against a case.
REQUEST BODY
channel*enume.g. CALL, SMS, WHATSAPP.
direction*enumINBOUND or OUTBOUND.
outcome*enume.g. ANSWERED, NO_ANSWER, PROMISED_TO_PAY.
notesstringFree-text detail.
Sample request body
{
  "channel": "<channel>",
  "direction": "<direction>",
  "outcome": "<outcome>",
  "notes": "<notes>"
}
GET
/api/recovery/cases/{id}/contactsReturns contact history.
GET
/api/recovery/cases/{id}/eligibilityEvaluates whether this case can be contacted right now under the active contact policy (quiet hours, frequency caps).
POST
/api/recovery/cases/{caseId}/messagesSends a message to the case's borrower using a saved template.
REQUEST BODY
templateId*UUIDFrom GET /api/message-templates.
Sample request body
{
  "templateId": "11111111-1111-1111-1111-111111111111"
}
GET
/api/recovery/cases/{caseId}/messagesLists case messages.
POST
/api/recovery/cases/{caseId}/callsPlaces an outbound recovery call for this case. Accepts an optional Idempotency-Key header.

No request body — path parameters only.

GET
/api/recovery/cases/{caseId}/callsLists calls for a case.
GET
/api/calls/{id}Fetches one call, including outcome and transcript summary once available.
POST
/api/recovery/cases/{caseId}/promises-to-payRecords a promise to pay against a case.
REQUEST BODY
promisedAmountNaira*numberAmount the borrower committed to pay.
promisedDate*dateDate the borrower committed to pay by.
notesstringOptional context.
Sample request body
{
  "promisedAmountNaira": 1000,
  "promisedDate": "2026-08-25",
  "notes": "<notes>"
}
GET
/api/recovery/cases/{caseId}/promises-to-payLists promises to pay for one case.
GET
/api/promises-to-payLists promises to pay across all cases.
POST
/api/promises-to-pay/{id}/keepMarks a promise as kept.

No request body — path parameters only.

POST
/api/promises-to-pay/{id}/breakMarks a promise as broken.

No request body — path parameters only.

POST
/api/promises-to-pay/{id}/cancelCancels a promise.

No request body — path parameters only.

POST
/api/disputesRaises a dispute against a loan.
REQUEST BODY
loanId*UUIDThe disputed loan.
recoveryCaseIdUUIDIf the loan is already in recovery.
description*stringWhat the borrower is disputing.
Sample request body
{
  "loanId": "11111111-1111-1111-1111-111111111111",
  "recoveryCaseId": "11111111-1111-1111-1111-111111111111",
  "description": "<description>"
}
POST
/api/disputes/{id}/resolveResolves a dispute in the borrower's favor or as settled.
REQUEST BODY
resolutionNotes*stringHow this was resolved.
Sample request body
{
  "resolutionNotes": "<resolutionNotes>"
}
POST
/api/disputes/{id}/rejectRejects a dispute.
REQUEST BODY
resolutionNotes*stringWhy this was rejected.
Sample request body
{
  "resolutionNotes": "<resolutionNotes>"
}
GET
/api/disputesLists disputes.
POST
/api/hardship-casesOpens a hardship case for a borrower.
REQUEST BODY
loanId*UUIDThe affected loan.
recoveryCaseIdUUIDIf the loan is already in recovery.
description*stringThe borrower's hardship circumstances.
Sample request body
{
  "loanId": "11111111-1111-1111-1111-111111111111",
  "recoveryCaseId": "11111111-1111-1111-1111-111111111111",
  "description": "<description>"
}
POST
/api/hardship-cases/{id}/approveApproves a hardship request.
REQUEST BODY
resolutionNotes*stringApproval terms/notes.
Sample request body
{
  "resolutionNotes": "<resolutionNotes>"
}
POST
/api/hardship-cases/{id}/denyDenies a hardship request.
REQUEST BODY
resolutionNotes*stringReason for denial.
Sample request body
{
  "resolutionNotes": "<resolutionNotes>"
}
POST
/api/hardship-cases/{id}/resolveResolves the hardship case.
REQUEST BODY
resolutionNotes*stringResolution notes.
Sample request body
{
  "resolutionNotes": "<resolutionNotes>"
}
GET
/api/hardship-casesLists hardship cases.
POST
/api/recovery-workflowsCreates a recovery workflow.
REQUEST BODY
name*stringWorkflow display name.
descriptionstringOptional free text.
stepsJson*stringJSON-encoded step definition — build it in the CreditPulse workflow builder UI and copy it here, rather than hand-authoring.
Sample request body
{
  "name": "<name>",
  "description": "<description>",
  "stepsJson": "<stepsJson>"
}
PUT
/api/recovery-workflows/{id}Updates a workflow.
REQUEST BODY
name*stringWorkflow display name.
descriptionstringOptional free text.
stepsJson*stringJSON-encoded step definition.
Sample request body
{
  "name": "<name>",
  "description": "<description>",
  "stepsJson": "<stepsJson>"
}
PUT
/api/recovery-workflows/{id}/activeActivates or deactivates a workflow.
QUERY PARAMETERS
active*booleantrue to activate, false to deactivate.
GET
/api/recovery-workflowsLists workflows.
GET
/api/recovery-workflows/{id}Fetches one workflow.
POST
/api/contact-policiesCreates a contact policy — quiet hours and contact-frequency limits.
REQUEST BODY
portfolioIdUUIDScope to one portfolio, or omit for organization-wide.
quietHoursStart*timee.g. 21:00 — no outreach after this time.
quietHoursEnd*timee.g. 08:00 — no outreach before this time.
maxContactsPerDay*integerHard ceiling per borrower per day.
maxContactsPerWeek*integerHard ceiling per borrower per week.
minHoursBetweenContacts*integerMinimum spacing between two contact attempts.
Sample request body
{
  "portfolioId": "11111111-1111-1111-1111-111111111111",
  "quietHoursStart": "21:00",
  "quietHoursEnd": "21:00",
  "maxContactsPerDay": 1,
  "maxContactsPerWeek": 1,
  "minHoursBetweenContacts": 1
}
PUT
/api/contact-policies/{id}/activeActivates or deactivates a contact policy.
QUERY PARAMETERS
active*booleantrue to activate, false to deactivate.
GET
/api/contact-policiesLists contact policies.
POST
/api/message-templatesCreates a message template.
REQUEST BODY
name*stringTemplate display name.
channel*enumSMS, WHATSAPP, or VOICE.
body*stringTemplate text, with placeholders your recovery workflow fills in.
Sample request body
{
  "name": "<name>",
  "channel": "<channel>",
  "body": "<body>"
}
GET
/api/message-templatesLists templates.
PUT
/api/message-templates/{id}/activeActivates or deactivates a template.
QUERY PARAMETERS
active*booleantrue to activate, false to deactivate.

Rules & reports

POST
/api/rulesCreates a new rule group as a draft (version 1).
REQUEST BODY
name*stringRule display name.
descriptionstringOptional free text.
module*enumWhich part of the platform this rule governs (e.g. COLLECTIONS, RECOVERY).
jurisdictionstringe.g. "NG" — for your own record-keeping of where this rule applies.
scopestringOptional scoping label.
conditionsJson*stringJSON-encoded condition logic.
actionJson*stringJSON-encoded action to take when conditions match.
priority*integerEvaluation order relative to other rules in the same module.
effectiveStartDate*dateWhen this rule starts applying.
effectiveEndDatedateOptional expiry date.
sourceReferencestringThe legal/policy source this rule's numbers came from — recommended for anything compliance-adjacent.
Sample request body
{
  "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>"
}
POST
/api/rules/groups/{ruleGroupId}/versionsCreates a new draft version for an existing rule group.
REQUEST BODY
(same fields as POST /api/rules)*—A new version replaces the group's active configuration once published.
Sample request body
{
  "(same fields as POST /api/rules)": "<(same fields as POST /api/rules)>"
}
POST
/api/rules/{id}/simulateRuns this draft rule against current data to preview its effect before publishing.

No request body — path parameters only.

POST
/api/rules/{id}/approveApproves a simulated draft rule.
QUERY PARAMETERS
approverUserId*UUIDThe approving user — recorded on the rule for audit purposes.
POST
/api/rules/{id}/publishPublishes an approved rule, making it live.

No request body — path parameters only.

POST
/api/rules/{id}/retireRetires a published rule.

No request body — path parameters only.

GET
/api/rules/{id}/conflictsReturns other rules that overlap with this one's scope/priority.
GET
/api/rulesLists rules.
GET
/api/rules/groups/{ruleGroupId}/versionsLists version history for a rule group.
GET
/api/rules/{id}Fetches one rule.
GET
/api/reports/portfolioPortfolio report snapshot.
GET
/api/reports/collections-performance?from=2026-08-01&to=2026-08-31Collections performance report for a date range.
QUERY PARAMETERS
from*dateRange start.
to*dateRange end.
GET
/api/reports/agent-performanceAgent performance report.
GET
/api/audit-eventsAudit trail listing — every automated and manual action, tied to the rule that authorized it.
GET
/api/organizationCurrent tenant organization record.
PATCH
/api/organizationUpdates organization settings.
REQUEST BODY
name*stringOrganization display name.
industryType*enume.g. MICROFINANCE_BANK.
currency*stringe.g. NGN.
timezone*stringe.g. Africa/Lagos.
Sample request body
{
  "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.

GET
/api/walletCurrent wallet balance.
GET
/api/wallet/transactionsPaginated wallet debit/credit history.
POST
/api/wallet/topupStarts a top-up and returns a payment authorization URL to redirect your user to.
REQUEST BODY
amountNaira*numberMinimum ₦100.
Sample request body
{
  "amountNaira": 1000
}
POST
/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

StatusWhen it happensdata shape
400Request body failed field validationOne entry per invalid field
400Business-rule violation (invalid state for the action)null, reason in message
401Missing, expired, or invalid credentialsnull
403Authenticated but missing the required permissionnull
404Resource doesn't exist in your tenantnull
409Conflicts with existing state (e.g. duplicate externalLoanId)null
500Unexpected server-side failure — treat as retryablenull
Non-technical stakeholder on your team? Point them to the Integration Guide instead — same ground, no JSON.