Authentication
Every request to the Work Benefit API must include a valid API key in the X-Api-Key header. Keys are created in the developer portal and are scoped to specific operations.
curl https://api-v2.workbenefits.app/api/public/v1/companies \
    -H "X-Api-Key: wb_live_xxxxxxxxxxxxxxxxxxxxxxxx"

API keys are tied to a single environment — Sandbox or Production. A Sandbox key cannot call production endpoints, and vice versa.

Requests using revoked, expired, or invalid keys return 401 Unauthorized. Requests where the key's scopes do not include the required permission return 403 Forbidden.

Scopes & Permissions

Each API key is assigned one or more scopes at creation time. Scopes are permanent — to change them you must revoke the key and create a new one.

ScopeGrants access to
COMPANY_READList and retrieve companies
COMPANY_WRITECreate and update companies
EMPLOYEE_READList and retrieve employees
EMPLOYEE_WRITERegister employees, manage company links
BENEFIT_READList company benefit enrollments
BENEFIT_WRITEEnroll and update company benefits
EWA_READView EWA settings, requests, and employee data
EWA_WRITEApprove/reject requests, update EWA settings
PAYMENT_READView payment history and balances
PAYMENT_WRITEInitialize repayments and EWA transfers

Environments

EnvironmentKey prefixBase URL
Sandboxwb_test_https://api-v2.workbenefits.app
Productionwb_live_https://api-v2.workbenefits.app

Sandbox and Production share the same base URL. The key prefix determines which environment your request runs in. Sandbox data is isolated from production data.

Ownership

You act on behalf of the company admins and employees you onboard. You can only access companies you created and employees you registered — requests for anything else return 404, exactly as if the record did not exist.

Pagination

List endpoints accept page (default 1) and pageSize (default 20, max 100) query parameters and return a paginated envelope inside data:

{
    "status": "success",
    "message": "...",
    "data": {
        "total": 57,
        "page": 1,
        "pageSize": 20,
        "totalPages": 3,
        "data": [ ... ]
    }
}

Hosted Flows

Some steps must be completed by the employee directly and are never handled by your integration. For these, the API returns a short-lived hosted url — open it in a browser or web view for the employee.

FlowStarted by
PIN setupPOST /employees/:employeeId/pin/initialize
EWA withdrawalPOST /employees/:employeeId/payments/ewa/transfer/initialize
The employee's PIN is only ever entered on the hosted page. Never collect or send a PIN through the API.

Response Format

All responses are JSON. Successful responses follow this envelope:

{
    "status": "success",
    "message": "Human-readable message",
    "data": { ... } // or an array
}

Rate Limits

Requests are limited to 100 per minute per client IP address, across all endpoints. The window is a fixed 60 seconds. Sandbox and Production keys share the same limit.

Every response includes rate limit headers so you can track usage:

HeaderMeaning
RateLimit-PolicyThe policy in force, e.g. 100;w=60 (100 requests per 60 seconds)
RateLimit-LimitMaximum requests allowed in the current window
RateLimit-RemainingRequests left in the current window
RateLimit-ResetSeconds until the window resets

When the limit is exceeded the API returns 429 Too Many Requests:

{
    "status": "error",
    "message": "Too many requests"
}
Back off until RateLimit-Reset seconds have passed before retrying. Avoid tight polling loops — for example, when waiting on a repayment or withdrawal, poll no more than every few seconds.

Error Codes

StatusMeaning
400Bad request — validation failed or business rule violated
401Missing, invalid, or expired API key
403API key exists but lacks the required scope, or developer account is not active
404Resource not found, or belongs to another developer
409Conflict — duplicate record or violated unique constraint
429Rate limit exceeded — see Rate Limits
500Unexpected server error

Error responses include a message field describing what went wrong.

{
    "status": "error",
    "message": "API key does not have the COMPANY_READ scope"
}
Companies
Create and manage the companies linked to your developer account, and act on their behalf for benefits, Earned Wage Access and payments. All company endpoints share the base path /api/public/v1/companies.
You can only access companies created with your developer account. Requests for any other company — or for a benefit, EWA request, employee or transaction that belongs to another company — return 404, exactly as if the resource did not exist.

List endpoints are paginated with page and pageSize (default 20, max 100) and return this envelope inside data:

{
    "status": "success",
    "message": "…",
    "data": {
        "total": 57,
        "page": 1,
        "pageSize": 20,
        "totalPages": 3,
        "data": [ ... ]
    }
}
POST /api/public/v1/companies
Required scope: COMPANY_WRITE
Creates a company, links it to your developer account, and provisions its first company admin with the Owner role. A temporary password is generated and emailed to the admin as an invitation — it is never returned in the response. The company starts as PENDING until Work Benefit reviews it.
Request body
{
    "name": "Acme Corp",
    "emailDomain": "acmecorp.com",
    "isEmailDomainEnforced": true,
    "admin": {
        "firstName": "Jane",
        "lastName": "Doe",
        "email": "jane@acmecorp.com",
        "phoneNumber": "+233201234567",
        "position": "HR Manager"
    }
}
FieldTypeNotes
namestringrequired1–200 characters
emailDomainstringrequiredUp to 100 characters, stored lowercase, e.g. acmecorp.com
isEmailDomainEnforcedbooleanrequiredIf true, employees must use an email on this domain to link to the company
admin.firstNamestringrequired1–100 characters
admin.lastNamestringrequired1–100 characters
admin.emailstringrequiredValid email, stored lowercase. Must not already belong to a company admin
admin.phoneNumberstringoptionalInclude the country code, e.g. +233201234567. Must not already belong to a company admin
admin.positionstringoptionalUp to 100 characters. Defaults to HR

Profile details (industry, pay cycle, website, etc.) are not accepted here — set them afterwards with Update Company.

Response 201
{
    "status": "success",
    "message": "Company created successfully",
    "data": {
        "company": {
            "id": 42,
            "name": "Acme Corp",
            "code": "A1B2C3D4",
            "emailDomain": "acmecorp.com",
            "isEmailDomainEnforced": true,
            "status": "PENDING",
            "createdAt": "2026-04-17T10:00:00.000Z",
            "updatedAt": "2026-04-17T10:00:00.000Z",
            "profile": null
        },
        "admin": { "email": "jane@acmecorp.com", "firstName": "Jane", "lastName": "Doe" }
    }
}
StatusWhen
400Validation failed
409A company admin with this email or phone number already exists
GET /api/public/v1/companies
Required scope: COMPANY_READ
Returns a paginated list of your companies, newest first. Each item is a summary — use Get Company for the full profile, locations and counts.
Query paramTypeNotes
pageintegerPage number, starting at 1. Default 1
pageSizeintegerItems per page, 1–100. Default 20
searchstringMatches part of the company name (max 200 characters)
statusstringPENDING, ACCEPTED or REJECTED
industrySectorstringOne of the industry sector values listed under Update Company
from / toISO dateOnly companies created on/after from and on/before to
Response 200
{
    "status": "success",
    "message": "Companies fetched successfully",
    "data": {
        "total": 1,
        "page": 1,
        "pageSize": 20,
        "totalPages": 1,
        "data": [{
            "id": 42,
            "name": "Acme Corp",
            "code": "A1B2C3D4",
            "emailDomain": "acmecorp.com",
            "isEmailDomainEnforced": true,
            "status": "PENDING",
            "createdAt": "2026-04-17T10:00:00.000Z",
            "updatedAt": "2026-04-17T10:00:00.000Z",
            "profile": {
                "industrySector": "TECHNOLOGY",
                "employeeCountRange": "FIFTY_ONE_TO_TWO_HUNDRED",
                "hasPhysicalOffice": true,
                "payrollSystem": "Sage",
                "incorporationCountry": "Ghana",
                "website": "https://acmecorp.com",
                "linkedIn": null,
                "facebook": null
            }
        }]
    }
}

profile is null until a profile field has been set. totalPages is 0 when nothing matches.

GET /api/public/v1/companies/:id
Required scope: COMPANY_READ
Returns a single company with its full profile, locations (primary first) and employee/benefit counts.
Response 200
{
    "status": "success",
    "message": "Company fetched successfully",
    "data": {
        "id": 42,
        "name": "Acme Corp",
        "code": "A1B2C3D4",
        "emailDomain": "acmecorp.com",
        "isEmailDomainEnforced": true,
        "status": "PENDING",
        "createdAt": "2026-04-17T10:00:00.000Z",
        "updatedAt": "2026-04-17T10:00:00.000Z",
        "profile": {
            "industrySector": "TECHNOLOGY",
            "employeeCountRange": "FIFTY_ONE_TO_TWO_HUNDRED",
            "hasPhysicalOffice": true,
            "payrollSystem": "Sage",
            "incorporationCountry": "Ghana",
            "incorporationCertUrl": null,
            "logoUrl": null,
            "website": "https://acmecorp.com",
            "linkedIn": null,
            "facebook": null,
            "workingDayStartDay": 1,
            "workingDayEndDay": 31,
            "cutoffDay": 25,
            "repaymentDay": 31,
            "maxSalaryExposurePercentage": 60,
            "contactAdminId": null
        },
        "locations": [
            { "id": 1, "location": "Airport City, Accra", "ghanaPostGps": "GA-123-4567", "isVerified": true, "isPrimary": true }
        ],
        "_count": { "employeeLinks": 120, "companyBenefits": 3 }
    }
}

profile is null until a profile field has been set; locations is [] when none exist.

PATCH /api/public/v1/companies/:id
Required scope: COMPANY_WRITE
Updates company fields and/or its profile. Send at least one field; everything else is left unchanged. The first update that includes profile creates the profile. code and status cannot be changed.
Request body
{
    "name": "Acme Corporation",
    "profile": {
        "industrySector": "TECHNOLOGY",
        "employeeCountRange": "FIFTY_ONE_TO_TWO_HUNDRED",
        "payrollSystem": "Sage",
        "cutoffDay": 20,
        "repaymentDay": 28,
        "maxSalaryExposurePercentage": 50,
        "contactAdminId": 7
    }
}
FieldTypeNotes
namestring1–200 characters
emailDomainstringUp to 100 characters, stored lowercase. Applies to future employee links only
isEmailDomainEnforcedbooleanApplies to future employee links only
profileobjectAt least one field when present
profile.industrySectorstringSee values below
profile.employeeCountRangestringSee values below
profile.hasPhysicalOfficeboolean
profile.payrollSystemstringUp to 100 characters
profile.incorporationCountrystringUp to 100 characters
profile.websitestringURL, up to 500 characters
profile.linkedInstringURL, up to 500 characters
profile.facebookstringURL, up to 500 characters
profile.logoUrlstringURL, up to 500 characters
profile.incorporationCertUrlstringURL, up to 500 characters
profile.workingDayStartDayinteger1–31. First day of the pay cycle (default 1)
profile.workingDayEndDayinteger1–31. Last day of the pay cycle (default 31)
profile.cutoffDayinteger1–31. Monthly cutoff for EWA/payroll (default 25)
profile.repaymentDayinteger1–31. Day the company repays each month (default 31)
profile.maxSalaryExposurePercentageinteger0–100. Cap on benefit repayments plus earned wage as a share of salary (default 60)
profile.contactAdminIdintegerMust be the id of an admin of this company
Industry sector values
AGRICULTURE CONSTRUCTION EDUCATION ENERGY FINANCE HEALTHCARE HOSPITALITY MANUFACTURING MEDIA MINING RETAIL TECHNOLOGY TELECOMMUNICATIONS TRANSPORTATION OTHER
Employee count range values
ONE_TO_FIVE SIX_TO_TWENTY TWENTY_ONE_TO_FIFTY FIFTY_ONE_TO_TWO_HUNDRED TWO_HUNDRED_PLUS
Response 200

Returns the updated company in the same shape as Get Company.

{
    "status": "success",
    "message": "Company updated successfully",
    "data": {
        "id": 42,
        "name": "Acme Corporation",
        "code": "A1B2C3D4",
        "emailDomain": "acmecorp.com",
        "isEmailDomainEnforced": true,
        "status": "PENDING",
        "createdAt": "2026-04-17T10:00:00.000Z",
        "updatedAt": "2026-04-17T10:15:00.000Z",
        "profile": {
            "industrySector": "TECHNOLOGY",
            "employeeCountRange": "FIFTY_ONE_TO_TWO_HUNDRED",
            "hasPhysicalOffice": true,
            "payrollSystem": "Sage",
            "incorporationCountry": "Ghana",
            "incorporationCertUrl": null,
            "logoUrl": null,
            "website": "https://acmecorp.com",
            "linkedIn": null,
            "facebook": null,
            "workingDayStartDay": 1,
            "workingDayEndDay": 31,
            "cutoffDay": 20,
            "repaymentDay": 28,
            "maxSalaryExposurePercentage": 50,
            "contactAdminId": 7
        },
        "locations": [
            { "id": 1, "location": "Airport City, Accra", "ghanaPostGps": "GA-123-4567", "isVerified": true, "isPrimary": true }
        ],
        "_count": { "employeeLinks": 120, "companyBenefits": 3 }
    }
}
StatusWhen
400Validation failed, empty body, or contactAdminId is not an admin of this company
404Company not found

Benefits — /api/public/v1/companies/:companyId/benefits

A company benefit attaches a benefit from the catalogue (GET /api/public/v1/benefits) to a company. It has its own id, distinct from the catalogue benefitId: pass benefitId when adding a benefit, and the company benefit id when updating one.

GET /api/public/v1/companies/:companyId/benefits
Required scope: BENEFIT_READ
Returns all benefits attached to the company, with benefit details and the number of enrolled employees. This list is not paginated.
Response 200
{
    "status": "success",
    "message": "Company benefits fetched successfully",
    "data": [{
        "id": 3,
        "status": "ACTIVE",
        "paymentOption": "PAYROLL_DEDUCTION",
        "createdAt": "2026-04-01T09:00:00.000Z",
        "updatedAt": "2026-04-17T10:00:00.000Z",
        "benefit": {
            "id": 1,
            "name": "Flexible Rent Payment Options",
            "code": "RENT",
            "description": "Rent Financing helps individuals take control of their housing costs…",
            "tag": "POPULAR",
            "fixedPaymentOption": null
        },
        "enrolledCount": 45
    }]
}

benefit.tag is POPULAR, NEW or null. benefit.fixedPaymentOption is null, or the payment option the benefit is locked to (PAYROLL_DEDUCTION / EMPLOYEE_SELF_PAYMENT).

POST /api/public/v1/companies/:companyId/benefits
Required scope: BENEFIT_WRITE
Adds a catalogue benefit to the company. It starts as ACTIVE (or WAITLISTED if the benefit is not yet generally available) with payment option PAYROLL_DEDUCTION. When active, all approved employees are enrolled and notified.
Request body
{
    "benefitId": 1
}
FieldTypeNotes
benefitIdintegerrequiredCatalogue benefit id
Response 201
{
    "status": "success",
    "message": "Company benefit created successfully",
    "data": {
        "id": 3,
        "companyId": 42,
        "benefitId": 1,
        "status": "ACTIVE",
        "paymentOption": "PAYROLL_DEDUCTION",
        "createdAt": "2026-04-17T10:00:00.000Z",
        "updatedAt": "2026-04-17T10:00:00.000Z"
    }
}
StatusWhen
404Company or catalogue benefit not found
409The company already has this benefit
PATCH /api/public/v1/companies/:companyId/benefits/:id
Required scope: BENEFIT_WRITE
Changes the status and/or payment option of a company benefit. :id is the company benefit id from List Benefits. Send at least one field. Activating a benefit enrols all approved employees and notifies them.
Request body
{
    "status": "ACTIVE",
    "paymentOption": "EMPLOYEE_SELF_PAYMENT"
}
FieldTypeNotes
statusstringACTIVE or INACTIVE
paymentOptionstringPAYROLL_DEDUCTION or EMPLOYEE_SELF_PAYMENT. Rejected with 400 if the benefit's fixedPaymentOption is set
Benefit statusDescription
ACTIVELive — enrolled employees can use it
INACTIVEDisabled
WAITLISTEDSet automatically when the benefit is not yet available; employees are not enrolled yet
SUSPENDEDSet by Work Benefit only — cannot be set or changed through the API
Payment optionDescription
PAYROLL_DEDUCTIONCompany repays and deducts from employee salary
EMPLOYEE_SELF_PAYMENTEmployee pays directly
Response 200
{
    "status": "success",
    "message": "Company benefit updated successfully",
    "data": {
        "id": 3,
        "status": "ACTIVE",
        "paymentOption": "EMPLOYEE_SELF_PAYMENT",
        "createdAt": "2026-04-01T09:00:00.000Z",
        "updatedAt": "2026-04-17T10:15:00.000Z",
        "benefit": {
            "id": 1,
            "name": "Flexible Rent Payment Options",
            "code": "RENT",
            "description": "Rent Financing helps individuals take control of their housing costs…",
            "tag": "POPULAR",
            "fixedPaymentOption": null
        },
        "enrolledCount": 45
    }
}
Setting status to INACTIVE on a WAITLISTED benefit removes it from the company. The response is still 200 and returns the removed record (id, companyId, benefitId, status, paymentOption, timestamps).
StatusWhen
400Validation failed, or payment option change on a fixed-option benefit
403The benefit is SUSPENDED
404Company or company benefit not found

Earned Wage Access — /api/public/v1/companies/:companyId/ewa

GET /api/public/v1/companies/:companyId/ewa/stats
Required scope: EWA_READ
Returns headline EWA figures. pendingApprovals counts all pending requests; the other values cover the current pay cycle (based on the company's workingDayStartDay / workingDayEndDay).
Response 200
{
    "status": "success",
    "message": "EWA company stats fetched successfully",
    "data": {
        "pendingApprovals": 3,
        "approvedThisMonth": 12,
        "declinedThisMonth": 1,
        "totalWithdrawalsThisMonth": 4500
    }
}
GET /api/public/v1/companies/:companyId/ewa/settings
Required scope: EWA_READ
Returns the company's EWA auto-approval rules. If none have been saved yet, the defaults below are created and returned.
Response 200
{
    "status": "success",
    "message": "EWA company settings fetched successfully",
    "data": {
        "id": 5,
        "companyId": 42,
        "enabledAutoApprovalForAllEmployees": false,
        "hasPercentageOfAccruedWage": false,
        "hasPermanentOrConfirmedEmployeeStatus": false,
        "hasRequestedLessThanMaxAmountPerCycle": false,
        "hasMadeLessThanMaxRequestsPerMonth": false,
        "hasPendingPaymentFromPriorCycles": false,
        "enabledBetweenTimePeriods": false,
        "percentageOfAccruedWage": 50,
        "maxAmountPerCycle": "100000",
        "smartAutoApprovalStartTime": "08:00",
        "smartAutoApprovalEndTime": "17:00",
        "smartAutoApprovalDays": "WEEKDAYS"
    }
}
PATCH /api/public/v1/companies/:companyId/ewa/settings
Required scope: EWA_WRITE
Updates the company's EWA auto-approval rules. Send at least one field; others keep their current value. Each has… / enabled… toggle switches on the matching limit, which can be set independently.
Request body
{
    "enabledAutoApprovalForAllEmployees": true,
    "hasPercentageOfAccruedWage": true,
    "percentageOfAccruedWage": 40,
    "enabledBetweenTimePeriods": true,
    "smartAutoApprovalStartTime": "09:00",
    "smartAutoApprovalEndTime": "16:30",
    "smartAutoApprovalDays": "WEEKDAYS"
}
FieldTypeDefaultNotes
enabledAutoApprovalForAllEmployeesbooleanfalseMaster switch for auto-approval
hasPercentageOfAccruedWagebooleanfalseEnforce percentageOfAccruedWage
percentageOfAccruedWageinteger500–100. Max share of accrued wage per request
hasPermanentOrConfirmedEmployeeStatusbooleanfalseOnly auto-approve permanent/confirmed employees
hasRequestedLessThanMaxAmountPerCyclebooleanfalseEnforce maxAmountPerCycle
maxAmountPerCyclenumber100000≥ 0. Max total withdrawn per pay cycle
hasMadeLessThanMaxRequestsPerMonthbooleanfalseEnforce each employee's maxRequestsPerMonth
hasPendingPaymentFromPriorCyclesbooleanfalseBlock auto-approval while prior cycles are unpaid
enabledBetweenTimePeriodsbooleanfalseOnly auto-approve within the time window below
smartAutoApprovalStartTimestring08:0024-hour HH:MM
smartAutoApprovalEndTimestring17:0024-hour HH:MM
smartAutoApprovalDaysstringWEEKDAYSWEEKDAYS, WEEKENDS or EVERYDAY
Response 200
{
    "status": "success",
    "message": "EWA company settings updated successfully",
    "data": {
        "id": 5,
        "companyId": 42,
        "enabledAutoApprovalForAllEmployees": true,
        "hasPercentageOfAccruedWage": true,
        "hasPermanentOrConfirmedEmployeeStatus": false,
        "hasRequestedLessThanMaxAmountPerCycle": false,
        "hasMadeLessThanMaxRequestsPerMonth": false,
        "hasPendingPaymentFromPriorCycles": false,
        "enabledBetweenTimePeriods": true,
        "percentageOfAccruedWage": 40,
        "maxAmountPerCycle": "100000",
        "smartAutoApprovalStartTime": "09:00",
        "smartAutoApprovalEndTime": "16:30",
        "smartAutoApprovalDays": "WEEKDAYS"
    }
}
GET /api/public/v1/companies/:companyId/ewa/requests
Required scope: EWA_READ
Paginated list of EWA requests from the company's employees, newest first.
Query paramTypeNotes
statusstringPENDING, APPROVED or REJECTED
searchstringMatches employee first or last name
dateRangestringtoday, this_week, this_month or this_year
from / toISO dateExplicit request-date range, used when dateRange is not given
pageintegerPage number, starting at 1. Default 1
pageSizeintegerItems per page, 1–100. Default 20
Response 200
{
    "status": "success",
    "message": "EWA requests fetched successfully",
    "data": {
        "total": 25,
        "page": 1,
        "pageSize": 20,
        "totalPages": 2,
        "data": [{
            "id": 7,
            "reference": "EWA-20260417-8F3A1C",
            "linkId": 5,
            "status": "PENDING",
            "employeeName": "Kwame Mensah",
            "earnedWage": 1200,
            "amount": 400,
            "percentageOfWage": 33.33,
            "requestDate": "2026-04-17T09:00:00.000Z"
        }]
    }
}

earnedWage is the employee's estimated earned wage so far this cycle (0 if their salary is unknown); percentageOfWage is the request as a share of it (null if unknown). linkId identifies the employee for Update Employee.

GET /api/public/v1/companies/:companyId/ewa/requests/:requestId
Required scope: EWA_READ
Returns one EWA request with a rule-check summary and the employee's last 5 approved withdrawals (excluding this one).
Response 200
{
    "status": "success",
    "message": "EWA request details fetched successfully",
    "data": {
        "id": 7,
        "reference": "EWA-20260417-8F3A1C",
        "status": "PENDING",
        "employeeName": "Kwame Mensah",
        "requestDate": "2026-04-17T09:00:00.000Z",
        "summary": {
            "estimatedEarnedSalary": 1200,
            "requestAmount": 400,
            "percentageOfAccrued": 33.33,
            "maxWithdrawalPercentage": 50,
            "withinRuleLimit": true
        },
        "withdrawalHistory": [
            { "amount": 200, "requestDate": "2026-03-10T08:00:00.000Z" }
        ]
    }
}

withinRuleLimit compares percentageOfAccrued with the employee's maxWithdrawalPercentage; it is null when either is unknown, as are estimatedEarnedSalary and percentageOfAccrued when the salary is unknown.

PATCH /api/public/v1/companies/:companyId/ewa/requests/:requestId/approve
Required scope: EWA_WRITE
Approves a PENDING EWA request and notifies the employee. No request body.
Response 200
{
    "status": "success",
    "message": "EWA request approved successfully",
    "data": {
        "id": 7,
        "reference": "EWA-20260417-8F3A1C",
        "companyLinkId": 5,
        "amount": "400",
        "serviceCharge": "8",
        "status": "APPROVED",
        "requestDate": "2026-04-17T09:00:00.000Z",
        "cutOffDate": "2026-04-25T00:00:00.000Z",
        "repaymentDate": "2026-04-30T00:00:00.000Z",
        "approvedById": null,
        "createdAt": "2026-04-17T09:00:00.000Z",
        "updatedAt": "2026-04-17T10:05:00.000Z"
    }
}

amount and serviceCharge are decimal strings. approvedById is always null for requests approved through the API.

StatusWhen
400The request is not PENDING (e.g. Request is already approved)
404Company or request not found
PATCH /api/public/v1/companies/:companyId/ewa/requests/:requestId/reject
Required scope: EWA_WRITE
Rejects a PENDING EWA request and notifies the employee. No request body. Returns 400 if the request has already been processed.
Response 200
{
    "status": "success",
    "message": "EWA request rejected successfully",
    "data": {
        "id": 7,
        "reference": "EWA-20260417-8F3A1C",
        "companyLinkId": 5,
        "amount": "400",
        "serviceCharge": "8",
        "status": "REJECTED",
        "requestDate": "2026-04-17T09:00:00.000Z",
        "cutOffDate": "2026-04-25T00:00:00.000Z",
        "repaymentDate": "2026-04-30T00:00:00.000Z",
        "approvedById": null,
        "createdAt": "2026-04-17T09:00:00.000Z",
        "updatedAt": "2026-04-17T10:06:00.000Z"
    }
}
GET /api/public/v1/companies/:companyId/ewa/employees
Required scope: EWA_READ
Paginated list of the company's approved employees with their EWA settings and enrolment status. Sorted by first name unless sortBy is given.
Query paramTypeNotes
searchstringMatches employee first or last name
sortBystringname, maxWithdrawalPercentage or approvalMode
sortOrderstringasc (default) or desc
pageintegerPage number, starting at 1. Default 1
pageSizeintegerItems per page, 1–100. Default 20
Response 200
{
    "status": "success",
    "message": "EWA employees fetched successfully",
    "data": {
        "total": 120,
        "page": 1,
        "pageSize": 20,
        "totalPages": 6,
        "data": [{
            "employeeName": "Kwame Mensah",
            "maxWithdrawalPercentage": 50,
            "approvalMode": "MANUAL",
            "maxRequestsPerMonth": 5,
            "enrolled": true,
            "linkId": 5
        }]
    }
}

Settings fields are null until they have been set for that employee. enrolled is true when the employee's EWA benefit is active.

PATCH /api/public/v1/companies/:companyId/ewa/employees/:linkId
Required scope: EWA_WRITE
Updates one employee's EWA settings and/or enrolment. :linkId comes from List Employees or List Requests. Send at least one field.
Request body
{
    "enrolled": true,
    "maxWithdrawalPercentage": 40,
    "approvalMode": "AUTOMATIC",
    "maxRequestsPerMonth": 3
}
FieldTypeNotes
enrolledbooleanTurns the employee's EWA benefit on or off. A suspended EWA benefit is not changed
maxWithdrawalPercentageinteger0–50. Max share of accrued wage the employee may withdraw
approvalModestringMANUAL or AUTOMATIC
maxRequestsPerMonthinteger1–50
Response 200
{
    "status": "success",
    "message": "EWA employee settings updated successfully",
    "data": {
        "maxWithdrawalPercentage": 40,
        "approvalMode": "AUTOMATIC",
        "maxRequestsPerMonth": 3,
        "enrolled": true
    }
}
StatusWhen
400Validation failed or empty body
404Company or employee not found

Payments — /api/public/v1/companies/:companyId/payments

Two separate balances are exposed. …/owed endpoints show what the company owes for payroll-deduction benefits (EWA, Rent & Pay Monthly, Shop & Pay Monthly). …/employees/… endpoints show what self-paying employees owe for their own rent/shop instalments.
GET /api/public/v1/companies/:companyId/payments/history
Required scope: PAYMENT_READ
Paginated, newest-first feed combining approved EWA withdrawals and the company's repayment transactions.
Query paramTypeNotes
pageintegerPage number, starting at 1. Default 1
pageSizeintegerItems per page, 1–100. Default 20
searchstringMatches employee name (EWA entries) or company name (repayments)
from / toISO dateDate range
Response 200
{
    "status": "success",
    "message": "Payment history retrieved",
    "data": {
        "total": 50,
        "page": 1,
        "pageSize": 20,
        "totalPages": 3,
        "data": [
            { "reference": "EWA-20260417-8F3A1C", "date": "2026-04-17T09:00:00.000Z", "name": "Kwame Mensah", "amount": 400, "benefitType": "Earned Wage Access", "paymentMethod": null, "status": "APPROVED" },
            { "reference": "cpr-20260415-3f9c2a7b1d4e6f80", "date": "2026-04-15T14:00:00.000Z", "name": "Acme Corp", "amount": 1000, "benefitType": null, "paymentMethod": "Paystack", "status": "COMPLETED" }
        ]
    }
}
GET /api/public/v1/companies/:companyId/payments/owed
Required scope: PAYMENT_READ
What the company still owes, grouped by month and split by benefit. Fully paid months are left out unless you filter with status=PAID.
Query paramTypeNotes
statusstringUNPAID, PARTIAL or PAID
searchstringMatches the month name, e.g. March
from / toISO dateMatched against each month's due date
Response 200
{
    "status": "success",
    "message": "Owed balance retrieved",
    "data": {
        "breakdown": [{
            "month": "March",
            "year": 2026,
            "ewaOwed": 2400,
            "ewaPaid": 800,
            "rentOwed": 600,
            "rentPaid": 0,
            "shopOwed": 200,
            "shopPaid": 0,
            "dueDate": "2026-03-31T00:00:00.000Z",
            "owed": 3200,
            "paid": 800,
            "status": "PARTIAL"
        }],
        "totals": { "owed": 3200, "paid": 800 }
    }
}

owed is the remaining balance for the month (sum of ewaOwed, rentOwed, shopOwed); paid is what has been paid so far.

GET /api/public/v1/companies/:companyId/payments/owed/:year/:month
Required scope: PAYMENT_READ
Per-employee breakdown of what the company owes for one month. :year is e.g. 2026, :month is 1–12. Employees appear once per benefit type.
Response 200
{
    "status": "success",
    "message": "Owed balance retrieved",
    "data": {
        "month": 3,
        "year": 2026,
        "employees": [
            { "employeeId": 101, "name": "Kwame Mensah", "owed": 400, "type": "Earned Wage Access", "dueDate": "2026-03-31T00:00:00.000Z" },
            { "employeeId": 102, "name": "Ama Owusu", "owed": 600, "type": "Rent & Pay Monthly", "dueDate": "2026-03-28T00:00:00.000Z" }
        ],
        "totals": { "owed": 1000, "paid": 800 }
    }
}

type is Earned Wage Access, Rent & Pay Monthly or Shop & Pay Monthly. totals.paid is the amount the company has already repaid for that month.

POST /api/public/v1/companies/:companyId/payments/repayment
Required scope: PAYMENT_WRITE
Starts a Paystack payment for the company to repay outstanding EWA for one or more months. Send the payer to authorization_url, then call Verify Repayment when they return to your callbackUrl. The Paystack receipt goes to the company's primary admin email.
Request body — single month
{
    "month": 3,
    "year": 2026,
    "amount": 1000,
    "callbackUrl": "https://yourapp.com/callback"
}
Request body — multiple months
{
    "months": [
        { "month": 3, "year": 2026 },
        { "month": 4, "year": 2026 }
    ],
    "amount": 2500,
    "callbackUrl": "https://yourapp.com/callback"
}
FieldTypeNotes
monthintegerrequired1–12 (single-month form)
yearintegerrequired≥ 2000 (single-month form)
monthsarrayrequiredAt least one { month, year } (multiple-month form, instead of month/year)
amountnumberrequired> 0
callbackUrlstringrequiredURL Paystack redirects to after payment

The amount is applied to the listed months oldest first, up to what is still owed on each. Anything left over is recorded as an overpayment credit and returned as overpayment.

Response 201
{
    "status": "success",
    "message": "Repayment initialized",
    "data": {
        "authorization_url": "https://checkout.paystack.com/abc123",
        "access_code": "abc123",
        "reference": "cpr-20260417-3f9c2a7b1d4e6f80",
        "amount": 1000,
        "transactionId": 55,
        "allocations": [
            { "year": 2026, "month": 3, "amount": 1000, "benefitId": 4 }
        ]
    }
}
StatusWhen
400Validation failed, the company has no outstanding EWA balance, or Paystack could not start the payment
404Company not found, or the company has no admin
GET /api/public/v1/companies/:companyId/payments/repayment/:reference/verify
Required scope: PAYMENT_READ
Checks the payment with Paystack and settles the repayment as completed or failed. A reference can only be settled once — calling again afterwards returns 400.
Response 200 — success
{
    "status": "success",
    "message": "Repayment verified",
    "data": {
        "success": true,
        "reference": "cpr-20260417-3f9c2a7b1d4e6f80",
        "amount": 1000,
        "months": [
            { "month": 3, "year": 2026 }
        ]
    }
}
Response 200 — payment failed or abandoned
{
    "status": "success",
    "message": "Repayment verified",
    "data": {
        "success": false,
        "reference": "cpr-20260417-3f9c2a7b1d4e6f80"
    }
}
Response 200 — still in progress
{
    "status": "success",
    "message": "Repayment verified",
    "data": {
        "success": false,
        "pending": true,
        "reference": "cpr-20260417-3f9c2a7b1d4e6f80",
        "status": "ongoing"
    }
}

When pending is true nothing has been settled yet; status is the Paystack status. Call verify again later.

StatusWhen
400Transaction has already been processed
404Transaction not found
GET /api/public/v1/companies/:companyId/payments/:reference
Required scope: PAYMENT_READ
Returns one company transaction or approved EWA withdrawal by reference. direction is CREDIT for repayments and DEBIT for EWA withdrawals.
Response 200
{
    "status": "success",
    "message": "Transaction details retrieved",
    "data": {
        "reference": "cpr-20260415-3f9c2a7b1d4e6f80",
        "amount": 1000,
        "direction": "CREDIT",
        "status": "COMPLETED",
        "date": "2026-04-15T14:00:00.000Z",
        "name": "Acme Corp",
        "benefitType": "Earned Wage Access",
        "paymentMethod": "Company Benefit epayment"
    }
}

Transaction status is PENDING, COMPLETED or FAILED; EWA withdrawals return APPROVED. For EWA withdrawals paymentMethod is null.

StatusWhen
404No transaction with this reference for the company
GET /api/public/v1/companies/:companyId/payments/employees/amount-owed
Required scope: PAYMENT_READ
Paginated per-employee summary of outstanding self-paid rent instalments due up to the end of the current month.
Query paramTypeNotes
pageintegerPage number, starting at 1. Default 1
pageSizeintegerItems per page, 1–100. Default 20
searchstringMatches employee name
Response 200
{
    "status": "success",
    "message": "Employee amount owed retrieved",
    "data": {
        "total": 1,
        "page": 1,
        "pageSize": 20,
        "totalPages": 1,
        "data": [{
            "employeeId": 101,
            "name": "Kwame Mensah",
            "amountOwed": 1500,
            "monthsOwed": [
                "March",
                "April"
            ],
            "benefit": "Flexible Rent Payment Options",
            "status": "UNPAID"
        }],
        "totals": { "owed": 1500, "paid": 0 }
    }
}

status is UNPAID if any month has no payment yet, otherwise PARTIALLY_PAID. totals.owed covers every matching employee, not just the current page.

GET /api/public/v1/companies/:companyId/payments/employees/owed
Required scope: PAYMENT_READ
What self-paying employees still owe for rent and shop instalments, grouped by month. Fully paid months are left out.
Query paramTypeNotes
statusstringUNPAID, PARTIAL or PAID — filters individual instalments
searchstringMatches the month name
from / toISO dateMatched against the instalment due date
Response 200
{
    "status": "success",
    "message": "Employee owed balance retrieved",
    "data": {
        "breakdown": [
            { "month": "April", "year": 2026, "owed": 1500, "totalPaid": 200, "status": "PARTIAL", "dueDate": "2026-04-30T00:00:00.000Z" }
        ],
        "totals": { "owed": 1500, "paid": 200 }
    }
}
GET /api/public/v1/companies/:companyId/payments/employees/owed/:year/:month
Required scope: PAYMENT_READ
Per-employee breakdown of self-paid rent and shop instalments for one month. :year is e.g. 2026, :month is 1–12. Employees appear once per benefit type.
Response 200
{
    "status": "success",
    "message": "Employee owed balance retrieved",
    "data": {
        "month": 4,
        "year": 2026,
        "employees": [
            { "employeeId": 101, "name": "Kwame Mensah", "type": "Rent & Pay Monthly", "owed": 800, "paid": 200, "status": "PARTIAL" }
        ],
        "totals": {
            "owed": 800,
            "paid": 200,
            "rentOwed": 800,
            "rentPaid": 200,
            "shopOwed": 0,
            "shopPaid": 0
        }
    }
}
Employees
Register employees, verify their phone number, collect a selfie, and link them to your companies. Each employee belongs to the developer that registered them — you can only see and act on your own employees; any other employee ID returns 404. All employee endpoints share the base path /api/public/v1/employees unless noted otherwise.

Onboarding flow

Onboarding is not fully automated — it needs the employee. Verification codes (4-digit OTPs) are sent directly to the employee's phone by SMS and to their work email inbox. You cannot read these codes: the employee receives them and gives them back to you (for example, through your own UI), and you submit them to the API. Build a step into your flow to collect these codes from the employee.

StepEndpointNotes
1. RegisterPOST /employeesCreates the employee and sends a phone OTP by SMS. Optionally links a company in the same call (also sends a work-email OTP).
2. Verify phonePOST /employees/:id/verify-phoneSubmit the SMS code the employee received.
3. Upload selfiePOST /employees/:id/selfieRequired for KYC. Only allowed after the phone is verified.
4. Link companyPOST /employees/:id/companiesSkip if you linked a company at registration. Sends a work-email OTP.
5. Verify work emailPOST /employees/:id/verify-work-emailSubmit the code the employee received at their work email.
Codes are 4 digits, expire after 10 minutes, and allow 3 attempts. If a code is lost or expires, use Resend Phone OTP or Resend Work Email OTP.

Phone numbers are accepted in international format with the country code, e.g. +233558371654. Local Ghana numbers such as 0558371654 are also accepted. Numbers are validated, stored and returned in E.164 format (+233558371654). An invalid number returns 400.

POST /api/public/v1/employees
Required scope: EMPLOYEE_WRITE
Registers a new employee under your developer account and sends a 4-digit phone verification code to the employee by SMS. The account stays unverified until you submit that code to Verify Phone. If you also pass companyId and workEmail, the employee is linked to that company in the same call and a work-email code is sent.
Request body
{
    "firstName": "Kwame",
    "lastName": "Mensah",
    "phoneNumber": "+233201234567",
    "companyId": 42, // optional, with workEmail
    "workEmail": "kwame@acmecorp.com" // optional, with companyId
}
FieldTypeNotes
firstNamestringrequired1–100 characters
lastNamestringrequired1–100 characters
phoneNumberstringrequiredValid phone number with country code (see phone format). Must be unique across the platform.
companyIdintegeroptionalA company owned by your developer account. Must be sent together with workEmail.
workEmailstringoptionalValid email, lowercased. Must be sent together with companyId. Must use the company's domain if the company enforces its email domain.
gender and dateOfBirth are not accepted at registration — set them afterwards with Update Profile. Unknown fields are rejected with 400.
Response 201
{
    "status": "success",
    "message": "Employee registered successfully",
    "data": {
        "id": 101, "firstName": "Kwame", "lastName": "Mensah",
        "phoneNumber": "+233201234567",
        "gender": null, "dateOfBirth": null,
        "createdAt": "2026-09-25T10:00:00.000Z",
        "companyLinks": [{
            "id": 5, "workEmail": "kwame@acmecorp.com", "emailVerified": false,
            "status": "PENDING", "createdAt": "2026-09-25T10:00:00.000Z",
            "company": { "id": 42, "name": "Acme Corp", "code": "A1B2C3D4", "status": "ACCEPTED" }
        }]
    }
}

companyLinks is an empty array when no company was linked. Errors: 400 validation or work-email domain mismatch, 404 company not found, 409 phone number already registered.

GET /api/public/v1/employees
Required scope: EMPLOYEE_READ
Paginated list of employees registered by your developer account, newest first.
Query paramNotes
searchMatches first name, last name or phone number. Max 200 characters.
page / pageSizeDefaults: 1 / 20 (max 100)
Response 200
{
    "status": "success",
    "message": "Employees fetched successfully",
    "data": {
        "total": 1, "page": 1, "pageSize": 20, "totalPages": 1,
        "data": [{
            "id": 101, "firstName": "Kwame", "lastName": "Mensah",
            "phoneNumber": "+233201234567", "gender": "MALE",
            "dateOfBirth": "1995-06-15T00:00:00.000Z", "createdAt": "2026-09-25T10:00:00.000Z"
        }]
    }
}
GET /api/public/v1/employees/:id
Required scope: EMPLOYEE_READ
Returns a single employee with their company links (work email, verification state, link status and company summary). Returns 404 if the employee was not registered by your developer account.
Response 200
{
    "status": "success",
    "message": "Employee fetched successfully",
    "data": {
        "id": 101, "firstName": "Kwame", "lastName": "Mensah",
        "phoneNumber": "+233201234567", "gender": "MALE",
        "dateOfBirth": "1995-06-15T00:00:00.000Z",
        "createdAt": "2026-09-25T10:00:00.000Z",
        "companyLinks": [{
            "id": 5, "workEmail": "kwame@acmecorp.com", "emailVerified": true,
            "status": "APPROVED", "createdAt": "2026-09-25T10:00:00.000Z",
            "company": { "id": 42, "name": "Acme Corp", "code": "A1B2C3D4", "status": "ACCEPTED" }
        }]
    }
}
POST /api/public/v1/employees/:id/verify-phone
Required scope: EMPLOYEE_WRITE
Verifies the employee's phone number with the 4-digit code sent by SMS at registration (or by Resend Phone OTP). The employee must relay the code to you.
Request body
{ "code": "1234" }
FieldTypeNotes
codestringrequiredExactly 4 characters
Response 200
{ "status": "success", "message": "Phone number verified successfully" }

Errors: 409 if the code is invalid or expired, or the phone number is already verified. Each wrong code uses up one of the 3 attempts.

POST /api/public/v1/employees/:id/resend-phone-otp
Required scope: EMPLOYEE_WRITE
Sends a new phone verification code to the employee. Any earlier code stops working. No request body.
Response 200
{
    "status": "success",
    "message": "Verification code resent successfully",
    "data": { "phoneNumber": "+233201234567" }
}

Errors: 409 if the phone number is already verified.

POST /api/public/v1/employees/:id/companies
Required scope: EMPLOYEE_WRITE
Links the employee to one of your companies. Creates a PENDING link and emails a 4-digit code to the workEmail. The employee relays the code to you for Verify Work Email.
Request body
{ "companyId": 42, "workEmail": "kwame@acmecorp.com" }
FieldTypeNotes
companyIdintegerrequiredA company owned by your developer account
workEmailstringrequiredValid email, lowercased. Must use the company's domain if the company enforces its email domain.
Linking ruleResult
An employee can have only one active company link. An existing approved, suspended, on-hold, or verified-pending link must be revoked first.409
A link that was rejected by the same company cannot be attempted again.409
A work email already verified and active at the company cannot be reused.409
The company enforces its email domain and workEmail does not match it.400
Unverified pending links, and any earlier link to the same company, are replaced automatically.—
Response 201
{
    "status": "success",
    "message": "Employee linked to company successfully",
    "data": {
        "id": 5, "companyId": 42, "workEmail": "kwame@acmecorp.com",
        "status": "PENDING", "otpSent": true, "createdAt": "2026-09-25T10:05:00.000Z"
    }
}
POST /api/public/v1/employees/:id/verify-work-email
Required scope: EMPLOYEE_WRITE
Verifies the work email with the code the employee received. If the work email's domain matches the company's domain, the link is automatically APPROVED and the company's active benefits become available to the employee. Otherwise (only possible when the company does not enforce its domain) the link stays PENDING until a company admin approves it. Company admins are notified either way.
Request body
{ "companyId": 42, "code": "1234" }
FieldTypeNotes
companyIdintegerrequiredThe company the employee is linking to
codestringrequiredExactly 4 characters
Response 200
{
    "status": "success",
    "message": "Work email verified successfully",
    "data": { "id": 5, "companyId": 42, "workEmail": "kwame@acmecorp.com", "status": "APPROVED", "createdAt": "2026-09-25T10:05:00.000Z" }
}
If the code expires (10 minutes) or all attempts are used, the pending link is deleted and you must start again with Link to Company.

Errors: 400 domain mismatch on a company that enforces its domain, 404 no link to this company, 409 invalid or expired code, or email already verified.

POST /api/public/v1/employees/:id/resend-work-email-otp
Required scope: EMPLOYEE_WRITE
Sends a new work-email verification code for a pending company link. Any earlier code stops working. Only works while the current code is still valid — if it has already expired, the link is deleted and you must re-link.
Request body
{ "companyId": 42 }
FieldTypeNotes
companyIdintegerrequiredThe company of the pending link
Response 200
{
    "status": "success",
    "message": "Work email verification code resent successfully",
    "data": { "companyId": 42, "workEmail": "kwame@acmecorp.com" }
}

Errors: 404 no link to this company, 409 email already verified, link no longer pending, or code expired.

Link statusDescription
PENDINGWork email not yet verified, or verified and awaiting company admin approval (check emailVerified)
APPROVEDActive — the employee has access to the company's benefits
REJECTEDRejected by a company admin. The employee cannot re-link to the same company.
SUSPENDEDTemporarily suspended by the company
HOLDPlaced on hold by the company
REVOKEDLink ended. The employee can link to a new company.

Profile — /api/public/v1/employees/:employeeId/profile

PATCH /api/public/v1/employees/:employeeId/profile
Required scope: EMPLOYEE_WRITE
Updates the employee's basic details. This is where gender and dateOfBirth are set. Send only the fields you want to change; at least one field is required.
Request body
{
    "firstName": "Kwame", "lastName": "Asante",
    "dateOfBirth": "1995-06-15", "gender": "MALE"
}
FieldTypeNotes
firstNamestringoptionalNon-empty
lastNamestringoptionalNon-empty
dateOfBirthstringoptionalISO 8601 date, e.g. 1995-06-15
genderstringoptionalMALE or FEMALE
Response 200
{
    "status": "success",
    "message": "Profile updated successfully",
    "data": {
        "id": 101, "firstName": "Kwame", "lastName": "Asante",
        "phoneNumber": "+233201234567", "isPhoneVerified": true, "isActive": true,
        "dateOfBirth": "1995-06-15T00:00:00.000Z", "gender": "MALE",
        "selfie": "https://...",
        "personalEmail": { "email": "kwame.personal@gmail.com", "isVerified": true },
        "salary": { "amount": "5000.00", "isVerified": false },
        "createdAt": "2026-09-25T10:00:00.000Z"
    }
}

selfie, personalEmail and salary are null until set. salary.amount is a decimal string.

POST /api/public/v1/employees/:employeeId/profile/email/request
Required scope: EMPLOYEE_WRITE
Sets (or replaces) the employee's personal email and sends a 4-digit code to it. The personal email is separate from the work email used for company linking.
Request body
{ "email": "kwame.personal@gmail.com" }
FieldTypeNotes
emailstringrequiredValid email address
Response 200
{
    "status": "success",
    "message": "Verification code sent to email",
    "data": { "email": "kwame.personal@gmail.com" }
}
Personal-email and work-email verification share one active code per employee. Requesting a personal-email code cancels any pending work-email code (and vice versa), so finish one verification before starting the other.

Errors: 409 if this email is already verified for the employee, or is in use by another employee.

POST /api/public/v1/employees/:employeeId/profile/email/verify
Required scope: EMPLOYEE_WRITE
Verifies the personal email with the code sent by Request Email Verification. Codes expire after 10 minutes and allow 3 attempts.
Request body
{ "code": "1234" }
FieldTypeNotes
codestringrequiredExactly 4 characters
Response 200
{
    "status": "success",
    "message": "Email verified successfully",
    "data": { "email": "kwame.personal@gmail.com" }
}

Errors: 409 if there is no pending verification, or the code is invalid or expired.

Selfie — /api/public/v1/employees/:employeeId/selfie

POST /api/public/v1/employees/:employeeId/selfie
Required scope: EMPLOYEE_WRITE
Uploads the employee's selfie for KYC. Send as multipart/form-data with the image in a file field named selfie. The image is checked for face quality before it is saved; uploading again replaces the previous selfie.
FieldTypeNotes
selfiefilerequiredJPEG or PNG image, max 5 MB. Must show exactly one clear, well-lit face.
Only allowed after the employee's phone is verified (Verify Phone). Otherwise returns 400.
curl -X POST https://api-v2.workbenefits.app/api/public/v1/employees/101/selfie \
    -H "X-Api-Key: wb_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
    -F "selfie=@/path/to/selfie.jpg"
Response 200
{
    "status": "success",
    "message": "Selfie uploaded successfully",
    "data": { "selfie": "https://...", "previousSelfie": null }
}

Errors: 400 if no image is sent, the phone is not verified, or the image fails the face check (no face, more than one face, too blurry, too dark or too bright — the message says which). 415 for an unsupported file type.

Emergency Contact — /api/public/v1/employees/:employeeId/emergency-contact

GET /api/public/v1/employees/:employeeId/emergency-contact
Required scope: EMPLOYEE_READ
Returns the employee's emergency contact. Returns 404 if none has been saved yet.
Response 200
{
    "status": "success",
    "message": "Emergency contact fetched successfully",
    "data": {
        "id": 3, "firstName": "Abena", "lastName": "Mensah",
        "relationship": "Sister", "phoneNumber": "+233201112233",
        "residentialAddress": "123 Osu Street, Accra", "createdAt": "2026-09-25T09:00:00.000Z"
    }
}
POST /api/public/v1/employees/:employeeId/emergency-contact
Required scope: EMPLOYEE_WRITE
Creates or replaces the employee's emergency contact. An employee has at most one emergency contact — calling this again overwrites it.
Request body
{
    "firstName": "Abena", "lastName": "Mensah",
    "relationship": "Sister", "phoneNumber": "+233201112233",
    "residentialAddress": "123 Osu Street, Accra"
}
FieldTypeNotes
firstNamestringrequired
lastNamestringrequired
relationshipstringrequiredFree text, e.g. Sister
phoneNumberstringrequiredValid phone number with country code (see phone format)
residentialAddressstringrequired
Response 201
{
    "status": "success",
    "message": "Emergency contact saved successfully",
    "data": { "id": 3, "firstName": "Abena", "lastName": "Mensah", "relationship": "Sister", "phoneNumber": "+233201112233", "residentialAddress": "123 Osu Street, Accra", "createdAt": "2026-09-25T09:00:00.000Z" }
}

Identity Verification — /api/public/v1/employees/:employeeId/identity-verification

GET /api/public/v1/employees/:employeeId/identity-verification
Required scope: EMPLOYEE_READ
Returns the employee's identity documents, oldest first. An employee has at most one document per type, so this is a short, non-paginated array (empty if none submitted).
Response 200
{
    "status": "success",
    "message": "Identity verifications fetched successfully",
    "data": [{
        "id": 2,
        "documentType": "GHANA_CARD", "documentId": "GHA-123456789-0",
        "frontImage": "https://...", "backImage": "https://...",
        "createdAt": "2026-09-25T09:00:00.000Z"
    }]
}
POST /api/public/v1/employees/:employeeId/identity-verification
Required scope: EMPLOYEE_WRITE
Submits an identity document for the employee. Send as multipart/form-data. If a document of the same documentType already exists, it is replaced (new images and document number); otherwise a new record is created.
FieldTypeNotes
documentTypetextrequiredGHANA_CARD or PASSPORT
documentIdtextrequiredThe document number, max 100 characters
frontImagefilerequiredFront of the document. Image (JPEG, PNG, WebP, GIF) or PDF, max 5 MB.
backImagefileoptionalBack of the document (e.g. for a Ghana Card). Same format and size limits.
curl -X POST https://api-v2.workbenefits.app/api/public/v1/employees/101/identity-verification \
    -H "X-Api-Key: wb_live_xxxxxxxxxxxxxxxxxxxxxxxx" \
    -F "documentType=GHANA_CARD" \
    -F "documentId=GHA-123456789-0" \
    -F "frontImage=@/path/to/front.jpg" \
    -F "backImage=@/path/to/back.jpg"
Response 201
{
    "status": "success",
    "message": "Identity document uploaded successfully",
    "data": {
        "id": 2,
        "documentType": "GHANA_CARD", "documentId": "GHA-123456789-0",
        "frontImage": "https://...", "backImage": "https://...",
        "createdAt": "2026-09-25T09:00:00.000Z"
    }
}

backImage is null when not supplied. Errors: 400 invalid documentType, missing documentId or missing frontImage; 415 unsupported file type.

Earned Wage Access — /api/public/v1/employees/:employeeId/ewa

GET /api/public/v1/employees/:employeeId/ewa
Required scope: EWA_READ
Returns the employee's earned wage summary for the current pay cycle: how much they have earned so far, how much they can still request, how much approved EWA is ready to withdraw, and the cycle's cut-off and repayment dates.
Response 200
{
    "status": "success",
    "message": "Earned wage details fetched successfully",
    "data": {
        "earnedWage": 2450,
        "availableToUse": 850,
        "maxAmountAllowed": 1250,
        "availableToWithdraw": 400,
        "serviceChargePercentage": 0.049,
        "maximumWithdrawalPercentage": 25,
        "totalRequestedAmount": 400,
        "totalRequests": 1,
        "linkId": 5,
        "repaymentDate": "2026-09-30T00:00:00.000Z",
        "cutoffDate": "2026-09-25T00:00:00.000Z",
        "message": "You can access up to GHS 850.00 of your earned salary at no interest fee"
    }
}
FieldDescription
earnedWageSalary earned so far in the current pay cycle
availableToUseAmount the employee can still request this cycle (the upper limit for Submit Request)
maxAmountAllowedCycle cap, based on the employee's withdrawal limit and the company's salary-exposure limit
availableToWithdrawApproved EWA balance that can be paid out now (the upper limit for a withdrawal)
serviceChargePercentageService charge as a fraction of the requested amount (0.049 = 4.9%)
totalRequestedAmount / totalRequestsPending and approved requests in the current cycle
cutoffDate / repaymentDateLast day to request in this cycle / date the advance is recovered from salary
If the employee is not yet linked to a company, the availability amounts are 0, linkId, cutoffDate and repaymentDate are null, and the response also includes minimumRequestAmount. Returns 400 if no salary has been set (see Update Income).
GET /api/public/v1/employees/:employeeId/ewa/requests
Required scope: EWA_READ
Paginated list of the employee's EWA requests, newest first.
Query paramNotes
statusoptionalPENDING, APPROVED or REJECTED
pageoptionalInteger ≥ 1. Default 1
pageSizeoptionalInteger 1–100. Default 20
Response 200
{
    "status": "success",
    "message": "EWA requests fetched successfully",
    "data": {
        "total": 3,
        "page": 1,
        "pageSize": 20,
        "totalPages": 1,
        "data": [
            {
                "id": 7,
                "reference": "EWA-A1B2C3D4",
                "status": "APPROVED",
                "amount": 400,
                "serviceCharge": 19.6,
                "requestDate": "2026-09-10T09:00:00.000Z",
                "cutOffDate": "2026-09-25T00:00:00.000Z",
                "repaymentDate": "2026-09-30T00:00:00.000Z",
                "companyName": "Acme Corp"
            }
        ]
    }
}
POST /api/public/v1/employees/:employeeId/ewa/requests
Required scope: EWA_WRITE
Submits an earned wage request for the current pay cycle. If the company's auto-approval rules are met the request is approved immediately; otherwise it stays PENDING until a company admin reviews it. Approved amounts become available to withdraw via Initialize Withdrawal.
Request body
{ "amount": 400 }
FieldTypeNotes
amountnumberrequired≥ 1 and ≤ availableToUse from EWA Details
Response 201
{
    "status": "success",
    "message": "Earned wage request submitted successfully",
    "data": {
        "id": 7,
        "reference": "EWA-A1B2C3D4",
        "companyLinkId": 5,
        "amount": "400",
        "serviceCharge": "19.6",
        "status": "PENDING",
        "requestDate": "2026-09-10T09:00:00.000Z",
        "cutOffDate": "2026-09-25T00:00:00.000Z",
        "repaymentDate": "2026-09-30T00:00:00.000Z",
        "approvedById": null,
        "createdAt": "2026-09-10T09:00:00.000Z",
        "updatedAt": "2026-09-10T09:00:00.000Z",
        "autoApproved": false
    }
}
autoApproved: true means the request was approved immediately and status is APPROVED. In this response amount and serviceCharge are decimal strings.
ErrorWhen
400The employee already has a PENDING request, the cycle's cut-off date has passed, or amount exceeds availableToUse
403Employee is deactivated, EWA is not active for the company, the employee is not enrolled in EWA, or their salary is not yet verified
404Employee not found for this developer, or not linked to an active company
GET /api/public/v1/employees/:employeeId/ewa/recipients
Required scope: EWA_READ
Lists every bank / mobile money account the employee has withdrawn to, newest first. Recipients are created automatically when a withdrawal is completed. accountName is looked up live and is null if it cannot be resolved.
Response 200
{
    "status": "success",
    "message": "Recipients fetched successfully",
    "data": [
        {
            "id": 12,
            "employeeId": 101,
            "transferType": "BANK",
            "accountNumber": "1234567890",
            "bankCode": "030100",
            "recipientCode": "RCP_abc123",
            "isSaved": true,
            "createdAt": "2026-09-10T10:00:00.000Z",
            "accountName": "KWAME MENSAH"
        }
    ]
}
transferType is BANK or MOBILE_MONEY (manually processed payouts appear as MANUAL_BANK / MANUAL_MOBILE_MONEY). For mobile money, accountNumber is the phone number and bankCode the network (MTN, VOD, ATL).
GET /api/public/v1/employees/:employeeId/ewa/recipients/saved
Required scope: EWA_READ
Same as List Recipients, filtered to recipients with isSaved: true.
PATCH /api/public/v1/employees/:employeeId/ewa/recipients/:recipientId/save
Required scope: EWA_WRITE
Marks a recipient as saved (or unsaved) so it can be offered as a favourite in your UI.
Request body
{ "isSaved": true }
FieldTypeNotes
isSavedbooleanrequired
Response 200
{
    "status": "success",
    "message": "Recipient updated successfully",
    "data": { "id": 12, "employeeId": 101, "transferType": "BANK", "accountNumber": "1234567890", "bankCode": "030100", "recipientCode": "RCP_abc123", "isSaved": true, "createdAt": "2026-09-10T10:00:00.000Z" }
}
Returns 404 if the recipient does not belong to this employee. accountName is not included in this response.

Payments — /api/public/v1/employees/:employeeId/payments

Hosted sessions (withdrawals & PIN)
The employee's 4-digit transaction PIN is never sent through the API. PIN setup and withdrawals are completed by the employee on a secure page hosted by WorkBenefit. Your integration starts the action and sends the employee to the returned url; the PIN entered on that page is what authorises the withdrawal.
StepWhat happens
1. InitializeCall Initialize PIN Setup or Initialize Withdrawal. The response contains url and expiresAt.
2. Send the employeeRedirect the employee's browser to url, or open it in a new tab / in-app browser / WebView. Do not fetch it server-side.
3. Employee completesPIN setup: the employee enters and confirms a new PIN. Withdrawal: the employee chooses a bank (and account number) or mobile money network (and phone number), then enters their PIN.
4. ReturnIf you passed redirectUrl, the page sends the employee there on success. For withdrawals, ?reference=<ref>&status=success is appended. Without redirectUrl, a success screen is shown on the hosted page.
Each session URL is single-use and expires 30 minutes after creation (expiresAt). Opening a used or expired link shows an error page — create a new session. Errors such as a wrong PIN are shown on the page and the employee can retry until the session is used or expires. Five wrong PINs lock the PIN for 15 minutes.
The redirectUrl is a plain browser redirect with no signature. Confirm the outcome from your server with PIN Status or Transaction History (match the withdrawal reference) rather than trusting the query string.
GET /api/public/v1/employees/:employeeId/payments/history
Required scope: PAYMENT_READ
Returns the employee's money movements grouped into three lists: approved EWA requests, withdrawals (payouts), and external benefit payments (rent and shop).
Query paramNotes
typeoptionalrequests, withdrawals, rent or shop. Omit to return all groups. rent and shop both return the external list (each item tagged with benefitType)
pageoptionalInteger ≥ 1. Default 1
pageSizeoptionalInteger 1–100. Default 20
Response 200
{
    "status": "success",
    "message": "Transaction history retrieved",
    "data": {
        "requests": [
            { "id": 7, "reference": "EWA-A1B2C3D4", "status": "APPROVED", "amount": "400", "serviceCharge": "19.6", "requestDate": "2026-09-10T09:00:00.000Z", "cutOffDate": "2026-09-25T00:00:00.000Z", "repaymentDate": "2026-09-30T00:00:00.000Z" }
        ],
        "withdrawals": [
            { "id": 20, "reference": "WD-9F8E7D6C", "status": "COMPLETED", "transferType": "BANK", "amount": "200", "transferFee": "7", "createdAt": "2026-09-11T10:00:00.000Z" }
        ],
        "external": [
            { "id": 5521, "paymentPurpose": "RENT", "paymentChannel": "MOMO", "amountPaid": 1500, "transactionReference": "TXN-55210", "datePaymentWasMade": "2026-09-01", "paymentMonth": "September", "paymentTitle": "Rent payment", "benefitType": "RENT" }
        ],
        "total": 3,
        "page": 1,
        "pageSize": 20
    }
}
This endpoint does not use the standard paginated envelope. page / pageSize apply to requests and withdrawals separately; external is always returned in full. total is the combined count across all three groups. Withdrawal status is PROCESSING, COMPLETED or FAILED. Decimal amounts in requests and withdrawals are strings.
GET /api/public/v1/employees/:employeeId/payments/ewa/transfer/details
Required scope: PAYMENT_READ
Previews the fee for a withdrawal so you can show the employee what they will receive before starting it.
Query paramNotes
accountTyperequiredBANK or MOBILE_MONEY
amountrequiredPositive number
Response 200
{
    "status": "success",
    "message": "Transfer details retrieved",
    "data": { "fee": 7, "requestedAmount": 200, "receivedAmount": 193, "minimumAmount": 10 }
}
Bank transfers have a flat fee of GHS 7; mobile money transfers are free. receivedAmount = requestedAmount − fee. The minimum withdrawal is GHS 10.
POST /api/public/v1/employees/:employeeId/payments/ewa/transfer/initialize
Required scope: PAYMENT_WRITE
Creates a hosted withdrawal session for approved EWA funds. Send the employee to the returned url, where they choose the destination account and confirm with their PIN (see Hosted sessions). No money moves until the employee confirms on the hosted page.
Request body
{
    "amount": 200,
    "accountType": "BANK",
    "redirectUrl": "https://yourapp.com/withdrawals/done"
}
FieldTypeNotes
amountnumberrequired≥ 1, at most 2 decimal places (e.g. 150.75). Fixed for the session
accountTypestringrequiredBANK or MOBILE_MONEY. Decides which destination form the hosted page shows
redirectUrlstringoptionalValid URI. Where the employee is sent after a successful withdrawal, with reference and status=success appended
Response 201
{
    "status": "success",
    "message": "Withdrawal session created",
    "data": {
        "url": "https://api-v2.workbenefits.app/hosted/withdrawal/3f9c1e...b7a2",
        "expiresAt": "2026-09-25T10:30:00.000Z"
    }
}
The employee must already have a PIN — check PIN Status first and run PIN setup if hasPin is false. When the employee confirms, the withdrawal is checked against the GHS 10 minimum, the employee's availableToWithdraw balance and a daily limit of GHS 2,500; any failure is shown on the hosted page.
ErrorWhen
400Validation failed
403Employee account is deactivated
404Employee not found for this developer

PIN — /api/public/v1/employees/:employeeId/pin

GET /api/public/v1/employees/:employeeId/pin/status
Required scope: EMPLOYEE_READ
Returns whether the employee has an active 4-digit transaction PIN. A PIN is required to complete withdrawals.
Response 200
{
    "status": "success",
    "message": "PIN status fetched successfully",
    "data": { "hasPin": false }
}
POST /api/public/v1/employees/:employeeId/pin/initialize
Required scope: EMPLOYEE_WRITE
Creates a hosted PIN setup session. Send the employee to the returned url to choose and confirm their PIN (see Hosted sessions). The PIN itself is never sent to or returned by the API.
Request body
{ "redirectUrl": "https://yourapp.com/pin/done" }
FieldTypeNotes
redirectUrlstringoptionalValid URI. Where the employee is sent after the PIN is created. The body may be empty
Response 201
{
    "status": "success",
    "message": "PIN setup session created",
    "data": {
        "url": "https://api-v2.workbenefits.app/hosted/pin/8a41d0...c93e",
        "expiresAt": "2026-09-25T10:30:00.000Z"
    }
}
PIN setup is for employees without a PIN. If the employee already has one, the hosted page shows an error — check PIN Status first.

Income — /api/public/v1/employees/:employeeId/income

GET /api/public/v1/employees/:employeeId/income
Required scope: EMPLOYEE_READ
Returns the employee's declared monthly salary and whether it has been verified. data is null if no salary has been set.
Response 200
{
    "status": "success",
    "message": "Income fetched successfully",
    "data": { "amount": "5000", "isVerified": false }
}
amount is a decimal string.
PUT /api/public/v1/employees/:employeeId/income
Required scope: EMPLOYEE_WRITE
Sets or updates the employee's monthly salary. A salary that has already been verified cannot be changed.
Request body
{ "salary": 5000 }
FieldTypeNotes
salarynumberrequired800 – 1,000,000
Response 200
{
    "status": "success",
    "message": "Income updated successfully",
    "data": { "amount": "5000", "isVerified": false, "previousAmount": 4500 }
}
previousAmount is null when no salary was set before. Returns 409 if the salary is already verified.
GET /api/public/v1/employees/:employeeId/income/verification
Required scope: EMPLOYEE_READ
Returns the employee's income verification: the latest approved verification if there is one, otherwise the pending submission. Includes active documents, admin requests for more information (rfes) and the document verification fee status.
Response 200
{
    "status": "success",
    "message": "Income verification fetched successfully",
    "data": {
        "id": 3,
        "employeeId": 101,
        "verificationType": "STANDARD",
        "status": "PENDING",
        "incomeAmount": "5000",
        "verifiedAmount": "0",
        "approvedById": null,
        "approvedAt": null,
        "isUpdatedAfterRFE": false,
        "createdAt": "2026-09-01T09:00:00.000Z",
        "updatedAt": "2026-09-01T09:00:00.000Z",
        "deletedAt": null,
        "documents": [
            { "id": 14, "documentType": "BANK_STATEMENT", "bank": "GCB", "name": "statement-aug.pdf", "url": "https://…/statement-aug.pdf", "isActive": true, "createdAt": "2026-09-01T09:00:00.000Z" }
        ],
        "rfes": [
            { "id": 2, "adminComment": "Please upload your latest payslip", "commentedAt": "2026-09-03T12:00:00.000Z" }
        ],
        "controllerDetail": null,
        "documentVerificationFee": "unpaid",
        "salaryReview": null
    }
}
FieldDescription
statusPENDING, APPROVED or REJECTED
incomeAmount / verifiedAmountDeclared monthly income / amount confirmed by review (decimal strings)
documents[].documentTypeBANK_STATEMENT, MOMO_STATEMENT, PAY_SLIP, OFFER_LETTER, BUSINESS_REGISTRATION or OTHER
documentVerificationFeepaid, unpaid or waived
salaryReviewWhen an approved verification exists and a newer submission is pending, the pending submission (same shape); otherwise null
If the employee has never submitted, only controllerDetail: null, documentVerificationFee and salaryReview: null are returned.
POST /api/public/v1/employees/:employeeId/income/verification
Required scope: EMPLOYEE_WRITE
Submits the employee's monthly income with supporting documents for review. Files are sent as base64 strings. Only one pending submission is allowed at a time.
Request body
{
    "monthlyIncome": 5000,
    "bank": [
        { "name": "statement-aug.pdf", "bankName": "GCB", "password": null, "file": "JVBERi0xLjcK…" }
    ],
    "momo": [],
    "otherFiles": [
        { "type": "PAY_SLIP", "file": "JVBERi0xLjcK…" }
    ]
}
FieldTypeNotes
monthlyIncomenumberrequired≥ 0
bankarrayoptionalBank statements. Each item: name (string, required — include the file extension, e.g. .pdf), file (base64 string, required), bankName (string, optional), password (string, optional — for password-protected PDFs)
momoarrayoptionalMobile money statements. Same item shape as bank
otherFilesarrayoptionalSupporting documents. Each item: type (required — PAY_SLIP, OFFER_LETTER, BUSINESS_REGISTRATION or OTHER), file (base64 string, required)
Response 201
{
    "status": "success",
    "message": "Income verification submitted successfully",
    "data": {
        "id": 3,
        "employeeId": 101,
        "verificationType": "STANDARD",
        "status": "PENDING",
        "incomeAmount": "5000",
        "verifiedAmount": "0",
        "approvedById": null,
        "approvedAt": null,
        "isUpdatedAfterRFE": false,
        "createdAt": "2026-09-01T09:00:00.000Z",
        "updatedAt": "2026-09-01T09:00:00.000Z",
        "deletedAt": null,
        "documents": [
            { "id": 14, "documentType": "BANK_STATEMENT", "bank": "GCB", "name": "statement-aug.pdf", "url": "https://…/statement-aug.pdf", "isActive": true, "createdAt": "2026-09-01T09:00:00.000Z" },
            { "id": 15, "documentType": "PAY_SLIP", "bank": null, "name": null, "url": "https://…/payslip.pdf", "isActive": true, "createdAt": "2026-09-01T09:00:00.000Z" }
        ],
        "controllerDetail": null
    }
}
Returns 409 if a pending verification already exists — use Update Verification instead.
PATCH /api/public/v1/employees/:employeeId/income/verification
Required scope: EMPLOYEE_WRITE
Updates the employee's pending income verification, for example to answer an admin request for more information. All fields are optional; send only what changes.
Request body
{
    "monthlyIncome": 5200,
    "otherFiles": [
        { "type": "PAY_SLIP", "file": "JVBERi0xLjcK…" }
    ]
}
FieldTypeNotes
monthlyIncomenumberoptional≥ 0
bankarrayoptionalReplaces all current bank statements. Item shape as in Submit Verification
momoarrayoptionalReplaces all current mobile money statements
otherFilesarrayoptionalReplaces current documents of the same type (other types are kept)
Response 200
{
    "status": "success",
    "message": "Income verification updated successfully",
    "data": {
        "id": 3,
        "employeeId": 101,
        "verificationType": "STANDARD",
        "status": "PENDING",
        "incomeAmount": "5200",
        "verifiedAmount": "0",
        "approvedById": null,
        "approvedAt": null,
        "isUpdatedAfterRFE": true,
        "createdAt": "2026-09-01T09:00:00.000Z",
        "updatedAt": "2026-09-04T11:00:00.000Z",
        "deletedAt": null,
        "documents": [
            { "id": 14, "documentType": "BANK_STATEMENT", "bank": "GCB", "name": "statement-aug.pdf", "url": "https://…/statement-aug.pdf", "isActive": true, "createdAt": "2026-09-01T09:00:00.000Z" },
            { "id": 16, "documentType": "PAY_SLIP", "bank": null, "name": null, "url": "https://…/payslip-sep.pdf", "isActive": true, "createdAt": "2026-09-04T11:00:00.000Z" }
        ]
    }
}
Returns 404 if there is no pending verification. documents lists only the active documents after the update.

Rent — /api/public/v1/employees/:employeeId/rent

Rent financing lets an employee have their rent paid up front and repay it in monthly instalments. You manage the whole application on the employee's behalf. Every endpoint below only works for employees that belong to your developer account; any other employee returns 404.

Each application has a stage that moves forward as the employee completes each step:

StepWhat you doStage afterwards
1. CheckEstimate with Calculate and Rent Limit, then run Check Qualification.—
2. ApplyCreate Application. Add or correct landlord and property details with Update Application.CREATED_APPLICATION
3. Inspection feeInspection Payment — the employee pays on a checkout page. WorkBenefit then inspects the property and proposes a payment plan.PAID_PHYSICAL_INSPECTION
4. Sign agreementShare the agreement (Agreement Link or Email), then Sign Agreement.AGREED_TO_PAYMENT_PLAN
5. Move-in depositDeposit Payment — the employee pays the move-in deposit on a checkout page.PAID_INITIAL_DEPOSIT
6. RepayShow the Payment Plan, collect instalments with Pay Rent, and review Payment History.COMPLETED_RENT_PAYMENT once the balance is fully paid
Payment endpoints return a checkout_url. Send the employee to it to pay. When they finish, they land on a WorkBenefit confirmation page and the application's stage updates. Read the application again with Get Application to see the new stage. Stages can also change from WorkBenefit's side (for example after the inspection), so always read the current stage before deciding the next step.
GET /api/public/v1/employees/:employeeId/rent/physical-inspection-fees
Required scope: BENEFIT_READ
Returns the physical inspection fee amounts in GHS. The express fee applies when the employee asks for express inspection in Inspection Payment.
Response 200
{
    "status": "success",
    "message": "Physical inspection fees fetched successfully",
    "data": { "standardFee": 130, "expressFee": 250 }
}
GET /api/public/v1/employees/:employeeId/rent/calculate
Required scope: BENEFIT_READ
Estimates the total repayment and monthly instalment for a rent amount. A premium of premiumRate × paybackDuration is added to the amount. Values are rounded to whole numbers. This is an estimate only; it does not check the employee's eligibility.
Query paramTypeNotes
amountnumberrequiredTotal rent amount to finance (GHS), ≥ 1
paybackDurationintegerrequiredRepayment period in months, ≥ 1
premiumRatenumberoptionalMonthly premium rate, ≥ 0. Defaults to 0.025 (2.5% per month)
Example
GET /api/public/v1/employees/101/rent/calculate?amount=12000&paybackDuration=6
Response 200
{
    "status": "success",
    "message": "Rent calculated successfully",
    "data": { "totalPayback": 13800, "monthlyRent": 2300 }
}
GET /api/public/v1/employees/:employeeId/rent/limit
Required scope: BENEFIT_READ
Returns the most the employee can finance, based on their salary on file. The limit assumes the maximum 12-month payback period at the default premium rate. Returns 404 if the employee has no salary on file.
Response 200
{
    "status": "success",
    "message": "Rent limit fetched successfully",
    "data": { "maxMonthlyPayment": 1950, "maxTotalPayback": 18000, "maxPaybackPeriod": 12, "premuimRate": 0.025 }
}
The response field is spelled premuimRate. This matches what the API returns.
GET /api/public/v1/employees/:employeeId/rent/qualify
Required scope: BENEFIT_READ
Checks, before you create an application, whether a rent amount over a payback period qualifies for the employee. The check uses the employee's income and employment details, so it is more precise than Rent Limit. Returns 404 if the employee has no salary on file.
Query paramTypeNotes
amountnumberrequiredTotal rent amount to finance (GHS), ≥ 1
paybackDurationintegerrequiredRepayment period in months, ≥ 1
Response 200 — qualifies
{
    "status": "success",
    "message": "Rent qualification checked successfully",
    "data": { "results": "P" }
}
Response 200 — does not qualify
{
    "status": "success",
    "message": "Rent qualification checked successfully",
    "data": { "results": "F", "message": "Requested amount exceeds the affordable limit" }
}
results is "P" (pass) or "F" (fail). A fail includes a message with the reason. Both outcomes return HTTP 200.
POST /api/public/v1/employees/:employeeId/rent/applications
Required scope: BENEFIT_WRITE
Creates a rent application for the employee. The employee's profile, ID documents and income documents on file are sent with the application.
FieldTypeNotes
monthlyRentBudgetnumberrequiredMonthly rent (GHS), ≥ 1
durationOfRentintegerrequiredLength of the tenancy in months, ≥ 1
rentPaybackPeriodintegerrequiredMonths to repay, ≥ 1
fullNameOfLandlordstringoptional
phoneNumberOfLandlordstringoptionalPhone number with country code, e.g. +233201234567
propertyGpsAddressstringoptionalGhana GPS address, e.g. GA-123-4567
The employee must already have an emergency contact and a salary on file (404 otherwise), and a monthly salary of at least GHS 1,500 (400 otherwise).
Request body
{
    "monthlyRentBudget": 1500,
    "durationOfRent": 12,
    "rentPaybackPeriod": 6,
    "fullNameOfLandlord": "John Landlord",
    "phoneNumberOfLandlord": "+233201234567",
    "propertyGpsAddress": "GA-123-4567"
}
Response 201
Returns the full application record from the rent provider. Its fields are in snake_case. Keep id: it is the applicationId for every later call. A shortened example:
{
    "status": "success",
    "message": "Rent application created successfully",
    "data": {
        "id": 5821,
        "client_information_id": 3310,
        "application_date": "2026-09-25",
        "monthly_rent_value": "1500",
        "rent_duration": 12,
        "payback_duration": 6,
        "name_of_landlord": "John Landlord",
        "phone_number_of_landlord": "+233201234567",
        "gps_address": "GA-123-4567",
        "tenant_has_signed": "no",
        "payment_schedules": [],
        …
    }
}
GET /api/public/v1/employees/:employeeId/rent/applications
Required scope: BENEFIT_READ
Returns all of the employee's rent applications as a plain array. This list is not paginated. Returns 404 if the employee has never applied.
Response 200
{
    "status": "success",
    "message": "Rent applications fetched successfully",
    "data": [
        { "applicationId": 5821, "stage": "PAID_PHYSICAL_INSPECTION", "propertyGpsAddress": "GA-123-4567", "nameOfLandlord": "John Landlord", "phoneOfLandlord": "+233201234567", "locationOfLandlord": "East Legon, Accra", "hasPaymentPlan": true, "hasSigned": "no", "applicationDate": "2026-09-25", "monthlyPayment": 1725, "paybackDuration": 6, "rentDuration": 12, "monthlyRentValue": 1500 }
    ]
}
FieldNotes
applicationIdUse as :applicationId in the endpoints below
stageCREATED_APPLICATION, PAID_PHYSICAL_INSPECTION, AGREED_TO_PAYMENT_PLAN, PAID_INITIAL_DEPOSIT or COMPLETED_RENT_PAYMENT
hasPaymentPlantrue once a payment plan has been proposed
hasSigned"yes" once the tenancy agreement is signed, otherwise "no"
monthlyPaymentMonthly repayment (GHS), or null until a plan is set
monthlyRentValueMonthly rent (GHS), or null
This list may be up to a few minutes behind. Use Get Application when you need one application's current details.
GET /api/public/v1/employees/:employeeId/rent/applications/:applicationId
Required scope: BENEFIT_READ
Returns one application with its stage, payment schedule and move-in deposit breakdown. paymentSchedule is empty until WorkBenefit proposes a plan. Its items use the rent provider's snake_case fields.
Response 200
{
    "status": "success",
    "message": "Rent application fetched successfully",
    "data": { "applicationId": 5821, "propertyGpsAddress": "GA-123-4567", "nameOfLandlord": "John Landlord", "phoneOfLandlord": "+233201234567", "locationOfLandlord": "East Legon, Accra", "stage": "AGREED_TO_PAYMENT_PLAN", "paymentSchedule": [{ "id": 90211, "rent_month": "October", "expected_payment_date": "2026-10-28", "amount_due": "1725", "amount_paid": "0", "amount_remaining": "1725", "amount_fined": "0", "payment_status_words": "Unpaid" }], "moveInDepositBreakdown": { "monthlyRent": 1725, "refundableRentSecurity": 1500, "serviceFee": 150, "extraPayment": 0, "proRatedFirstMonth": 0, "totalMoveInDeposit": 3375 } }
}
FieldNotes
moveInDepositBreakdown.totalMoveInDepositAmount the employee pays with Deposit Payment
moveInDepositBreakdown.*All amounts in GHS; 0 until a plan is set
PATCH /api/public/v1/employees/:employeeId/rent/applications/:applicationId
Required scope: BENEFIT_WRITE
Updates an application's rent terms, landlord or property details. All fields are optional; send only the ones you want to change.
FieldTypeNotes
monthlyRentBudgetnumberoptional≥ 1
durationOfRentintegeroptionalMonths, ≥ 1
rentPaybackPeriodintegeroptionalMonths, ≥ 1
fullNameOfLandlordstringoptional
phoneNumberOfLandlordstring | nulloptionalPhone number with country code; null or "" clears it
locationOfLandlordstringoptional
locationOfPropertystring | nulloptional
propertyGpsAddressstring | nulloptionalGhana GPS address
Request body
{
    "fullNameOfLandlord": "John Landlord",
    "locationOfLandlord": "East Legon, Accra"
}
Response 200
{
    "status": "success",
    "message": "Rent application updated successfully",
    "data": { "applicationId": 5821, "propertyGpsAddress": "GA-123-4567", "nameOfLandlord": "John Landlord", "phoneOfLandlord": "+233201234567", "locationOfLandlord": "East Legon, Accra", "stage": "CREATED_APPLICATION" }
}
POST /api/public/v1/employees/:employeeId/rent/applications/:applicationId/initialize-physical-inspection-payment
Required scope: BENEFIT_WRITE
Starts payment of the physical inspection fee: GHS 130 standard, or GHS 250 express. You can also send the employee's preferred inspection date. Send the employee to checkout_url to pay. Once the payment is confirmed, the stage moves to PAID_PHYSICAL_INSPECTION.
FieldTypeNotes
expressServiceForPhysicalInspectionbooleanoptionaltrue for express inspection (higher fee). Default: standard
dateAndTimeForVerificationstringoptionalPreferred inspection date, format YYYY-MM-DD (date only)
Request body
{
    "expressServiceForPhysicalInspection": false,
    "dateAndTimeForVerification": "2026-10-02"
}
Response 201
{
    "status": "success",
    "message": "Payment initialized successfully",
    "data": { "checkout_url": "https://checkout.paystack.com/abc123xyz" }
}
GET /api/public/v1/employees/:employeeId/rent/applications/:applicationId/generate-tenancy-agreement-link
Required scope: BENEFIT_READ
Returns a link to a WorkBenefit-hosted page showing the tenancy agreement. The page needs no API key. Share it with the employee so they can read the agreement before signing.
Response 200
{
    "status": "success",
    "message": "Tenancy agreement link generated successfully",
    "data": { "link": "https://api-v2.workbenefits.app/api/v2/employee/rent/tenancy-agreement/eyJhbGciOi..." }
}
The link contains an access token. Anyone who has it can view the agreement, so share it only with the employee.
POST /api/public/v1/employees/:employeeId/rent/applications/:applicationId/send-tenancy-agreement-email
Required scope: BENEFIT_WRITE
Emails the tenancy agreement details to an address you choose. The email is sent in the background.
FieldTypeNotes
emailstringrequiredValid email address
Request body
{
    "email": "kwame.mensah@example.com"
}
Response 200
{
    "status": "success",
    "message": "Tenancy agreement email sent successfully",
    "data": null
}
PATCH /api/public/v1/employees/:employeeId/rent/applications/:applicationId/sign-tenancy-agreement
Required scope: BENEFIT_WRITE
Records that the employee has signed the tenancy agreement and accepted the payment plan. The stage moves to AGREED_TO_PAYMENT_PLAN. No request body.
Response 200
{
    "status": "success",
    "message": "Tenancy agreement signed successfully",
    "data": { "message": "Tenancy agreement signed successfully" }
}
POST /api/public/v1/employees/:employeeId/rent/applications/:applicationId/initialize-initial-deposit-payment
Required scope: BENEFIT_WRITE
Starts payment of the move-in deposit. The amount is the application's totalMoveInDeposit (see Get Application). Send the employee to checkout_url to pay. Once the payment is confirmed, the stage moves to PAID_INITIAL_DEPOSIT and repayments begin. No request body.
Returns 400 if the deposit amount has not been set yet, which means WorkBenefit has not proposed a payment plan.
Response 201
{
    "status": "success",
    "message": "Initial deposit payment initialized successfully",
    "data": { "checkout_url": "https://checkout.paystack.com/def456uvw" }
}
GET /api/public/v1/employees/:employeeId/rent/applications/:applicationId/payment-plan
Required scope: BENEFIT_READ
Returns the monthly repayment schedule, one item per month. Amounts are in GHS.
Response 200
{
    "status": "success",
    "message": "Payment plan fetched successfully",
    "data": [
        { "applicationId": 5821, "amount": 1725, "fines": 0, "amountPaid": 1725, "totalAmountDue": 0, "paymentStatus": "Paid", "rentMonth": "October", "rentYear": 2026, "expectedPaymentDate": "2026-10-28", "isLate": false, "daysDelayed": 0 },
        { "applicationId": 5821, "amount": 1725, "fines": 0, "amountPaid": 0, "totalAmountDue": 1725, "paymentStatus": "Unpaid", "rentMonth": "November", "rentYear": 2026, "expectedPaymentDate": "2026-11-28", "isLate": false, "daysDelayed": 0 }
    ]
}
FieldNotes
amountInstalment due for the month
finesLate fines added to the month
totalAmountDueWhat is still owed for the month, including fines
paymentStatuse.g. Paid, Partial, Unpaid
isLatetrue when fines have been added
daysDelayedDays past the expected payment date
POST /api/public/v1/employees/:employeeId/rent/applications/:applicationId/pay
Required scope: BENEFIT_WRITE
Starts a repayment for any amount. Send the employee to checkout_url to pay. The amount can cover part of an instalment, one instalment or several.
FieldTypeNotes
amountnumberrequiredAmount to pay (GHS), ≥ 1
Request body
{
    "amount": 1725
}
Response 201
{
    "status": "success",
    "message": "Rent payment initialized successfully",
    "data": { "checkout_url": "https://checkout.paystack.com/ghi789rst" }
}
GET /api/public/v1/employees/:employeeId/rent/applications/:applicationId/payments
Required scope: BENEFIT_READ
Returns every payment made on the application, including the inspection fee, move-in deposit and repayments, newest first. This list is not paginated.
Response 200
{
    "status": "success",
    "message": "Payment history fetched successfully",
    "data": [
        { "id": 7712, "paymentPurpose": "RENT", "paymentChannel": "mobile_money", "amountPaid": 1725, "transactionReference": "T482915736201", "datePaymentWasMade": "2026-10-27", "paymentMonth": "October", "paymentTitle": "Monthly Rent Payment" },
        { "id": 7650, "paymentPurpose": "INITIAL_DEPOSIT", "paymentChannel": "card", "amountPaid": 3375, "transactionReference": "T482901118845", "datePaymentWasMade": "2026-09-30", "paymentMonth": null, "paymentTitle": "Initial Deposit" }
    ]
}
paymentPurpose is PHYSICAL_VERIFICATION_FEE, INITIAL_DEPOSIT or RENT. paymentMonth is set only for rent repayments.
Reference Data
Read-only lookup lists to populate choices in your UI. These endpoints need a valid API key but no specific scope. Responses are plain arrays, not paginated.
GET /api/public/v1/banks/income-verification
Required scope: Any valid API key
Returns the names of banks supported for income verification. Use one of these values as bankName on bank statements when you submit income verification.
Response 200
{
    "status": "success",
    "message": "Banks fetched successfully",
    "data": [
        "Absa Bank Ghana LTD",
        "Access Bank (Ghana) Plc",
        "Agricultural Development Bank Plc",
        ...,
        "Zenith Bank (Ghana) Limited"
    ]
}
GET /api/public/v1/benefits
Required scope: Any valid API key
Returns every benefit offered on the platform. Use the id when enrolling a company in a benefit.
Response 200
{
    "status": "success",
    "message": "Benefits fetched successfully",
    "data": [{
        "id": 4,
        "name": "Earned Wage Access",
        "code": "EWA",
        "description": "Real-time access to a portion of earned but unpaid wages before payday...",
        "readMore": "",
        "status": "ACTIVE",
        "tag": null,
        "fixedPaymentOption": "EMPLOYEE_SELF_PAYMENT"
    }]
}
FieldNotes
codeStable identifier, e.g. RENT, BNPL, CAR_FINANCING, EWA, GROUP_LIFE_INSURANCE, GROUP_HEALTH_INSURANCE
statusACTIVE, INACTIVE, WAITLIST or ARCHIVED
tagPOPULAR, NEW or null
fixedPaymentOptionIf set, the only repayment option for this benefit: PAYROLL_DEDUCTION or EMPLOYEE_SELF_PAYMENT. null means the company can choose.