All API calls in these guides use the Work Benefit Public API at https://api-v2.workbenefits.app/api/public/v1 and require an X-Api-Key header on every request.
Keys starting with wb_test_ are sandbox keys and keys starting with wb_live_ are production keys. Both use the same base URL. Sandbox requests are routed to the sandbox environment automatically.
Every successful response has the shape { "status": "success", "message": "...", "data": ... }. Errors return { "status": "error", "message": "..." } with the matching HTTP status. List endpoints return data as { total, page, pageSize, totalPages, data: [...] }.
COMPANY_WRITE or EWA_READ). A call without the required scope returns 403. You can only see and act on companies and employees created with your own developer account; anything else returns 404.Company status lifecycle
Every company starts as PENDING and must be reviewed by the Work Benefit team. You can keep configuring the company, enrolling benefits and onboarding employees while it is pending, but employees cannot request EWA until the status reaches ACCEPTED.
"name": "Acme Corp",
"emailDomain": "acmecorp.com",
"isEmailDomainEnforced": true,
"admin": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@acmecorp.com",
"phoneNumber": "+233201234567",
"position": "HR Manager"
}
}
| Field | Type | Notes | |
|---|---|---|---|
name | string | required | 1–200 characters |
emailDomain | string | required | Company domain, e.g. acmecorp.com |
isEmailDomainEnforced | boolean | required | When true, employees must use a work email on this domain. When false, other domains are allowed but those links need company-admin approval. |
admin.firstName | string | required | 1–100 characters |
admin.lastName | string | required | 1–100 characters |
admin.email | string | required | Must be unique across the platform |
admin.phoneNumber | string | optional | International format — e.g. +233201234567. Must be unique if provided. |
admin.position | string | optional | Admin's job title. Defaults to HR. |
"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-24T09:00:00.000Z",
"updatedAt": "2026-04-24T09:00:00.000Z",
"profile": null
},
"admin": {
"firstName": "Jane",
"lastName": "Doe",
"email": "jane@acmecorp.com"
}
}
}
company.id — you will need it for every subsequent company-scoped API call. A temporary password is auto-generated and sent to the admin's email along with an invitation to log in — you do not set it.Common errors
| Status | Message | Fix |
|---|---|---|
409 | An account with this email already exists | Use a different admin email |
409 | An account with this phone number already exists | Use a different phone number or omit it |
profile key.All fields are optional — send only what you want to set or update. Top-level name, emailDomain, and isEmailDomainEnforced can also be updated alongside profile.
"name": "Acme Corp",
"profile": {
"industrySector": "TECHNOLOGY",
"employeeCountRange": "FIFTY_ONE_TO_TWO_HUNDRED",
"hasPhysicalOffice": true,
"payrollSystem": "Sage",
"incorporationCountry": "Ghana",
"website": "https://acmecorp.com",
"linkedIn": "https://linkedin.com/company/acme",
"facebook": "https://facebook.com/acme"
}
}
Profile fields
| Field | Type | Notes |
|---|---|---|
industrySector | string | AGRICULTURE CONSTRUCTION EDUCATION ENERGY FINANCE HEALTHCARE HOSPITALITY MANUFACTURING MEDIA MINING RETAIL TECHNOLOGY TELECOMMUNICATIONS TRANSPORTATION OTHER |
employeeCountRange | string | ONE_TO_FIVE SIX_TO_TWENTY TWENTY_ONE_TO_FIFTY FIFTY_ONE_TO_TWO_HUNDRED TWO_HUNDRED_PLUS |
hasPhysicalOffice | boolean | Whether the company operates from a physical office |
payrollSystem | string | Name of the payroll software in use, e.g. Sage |
incorporationCountry | string | Country of incorporation |
website / linkedIn / facebook | string (URL) | Online presence links, max 500 characters each |
logoUrl / incorporationCertUrl | string (URL) | Links to the company logo and certificate of incorporation |
workingDayStartDay / workingDayEndDay | integer 1–31 | Start and end day of the pay cycle. Used to calculate earned wages for EWA. |
cutoffDay | integer 1–31 | Last day of the month employees can request EWA |
repaymentDay | integer 1–31 | Day of the month advances are repaid from salary |
maxSalaryExposurePercentage | integer 0–100 | Maximum share of salary that rent and other benefit repayments plus EWA can take together in a pay period |
contactAdminId | integer | Must be the id of an admin of this company, otherwise 400 |
"status": "success",
"data": {
"id": 42, "name": "Acme Corp", "status": "PENDING",
"profile": {
"industrySector": "TECHNOLOGY",
"employeeCountRange": "FIFTY_ONE_TO_TWO_HUNDRED",
"payrollSystem": "Sage",
"website": "https://acmecorp.com"
}
}
}
workingDayStartDay, workingDayEndDay, cutoffDay, repaymentDay) before employees start requesting EWA. They control how earned wages, cut-off dates and repayment dates are calculated. The response returns the full company, including profile, locations and _count.status changes"status": "success",
"data": {
"id": 42,
"status": "ACCEPTED", // was "PENDING"
"updatedAt": "2026-04-24T09:30:00.000Z",
...
}
}
| Status | What it means | What to do |
|---|---|---|
| PENDING | Awaiting review by Work Benefit | Make sure the profile is complete (Step 3). You can continue with Steps 5 and 6 while you wait. |
| ACCEPTED | Company is live and approved | Employees can now request EWA. |
| REJECTED | Application was declined | Contact Work Benefit support for the reason and next steps. |
ACTIVE, or WAITLISTED if the benefit itself is on a waitlist. When it is ACTIVE, every approved employee of the company is enrolled automatically. Start by fetching the available benefits to get the correct benefitId.1 · List available benefits
"status": "success",
"data": [
{
"id": 1,
"name": "Earned Wage Access",
"code": "EWA",
"description": "...",
"readMore": "...",
"status": "ACTIVE",
"tag": "...",
"fixedPaymentOption": false
},
...
]
}
code to find the benefit you need (for example EWA or RENT) and pass its id as benefitId.2 · Enroll a benefit
"status": "success",
"message": "Company benefit created successfully",
"data": {
"id": 3,
"companyId": 42,
"benefitId": 1,
"status": "ACTIVE",
"paymentOption": "PAYROLL_DEDUCTION",
"createdAt": "2026-04-24T10:00:00.000Z",
"updatedAt": "2026-04-24T10:00:00.000Z"
}
}
data.id is the company benefit id. Use it (not the catalog benefitId) when you update the enrollment.| Status | Message | Fix |
|---|---|---|
404 | Benefit not found | Use an id from GET /benefits |
404 | Company not found | Use a company created with your developer account |
409 | Resource already exists | The benefit is already enrolled. Update it instead. |
3 · Update or deactivate a benefit
:id is the company benefit id. Send at least one of status (ACTIVE or INACTIVE) and paymentOption (PAYROLL_DEDUCTION or EMPLOYEE_SELF_PAYMENT). Some benefits have a fixed payment option and return 400 if you try to change it. Setting a WAITLISTED benefit to INACTIVE removes the enrollment. Suspended benefits can only be managed by Work Benefit (403). To see current enrollments, call GET /api/public/v1/companies/:companyId/benefits (BENEFIT_READ).
GET /api/public/v1/companies/:companyId/ewa/settings (EWA_READ).All fields are optional — send only what you want to change. By default, all rules are disabled and every EWA request requires manual approval.
Example A — Auto-approve all requests
Example B — Auto-approve up to 50% of accrued earnings
"enabledAutoApprovalForAllEmployees": true,
"hasPercentageOfAccruedWage": true,
"percentageOfAccruedWage": 50
}
Example C — Auto-approve weekdays 8 am – 5 pm only
"enabledAutoApprovalForAllEmployees": true,
"enabledBetweenTimePeriods": true,
"smartAutoApprovalStartTime": "08:00",
"smartAutoApprovalEndTime": "17:00",
"smartAutoApprovalDays": "WEEKDAYS"
}
Example D — Limit the number of requests per month
"enabledAutoApprovalForAllEmployees": true,
"hasMadeLessThanMaxRequestsPerMonth": true
}
The monthly request limit itself is set per employee (see Per-employee settings below).
All settings fields
| Field | Type | Description |
|---|---|---|
enabledAutoApprovalForAllEmployees | boolean | Master switch. When true, requests are auto-approved subject to the rules below. When false, only employees whose own approvalMode is AUTOMATIC can be auto-approved; everyone else needs manual approval. |
hasPercentageOfAccruedWage | boolean | Enable a cap on the percentage of accrued earnings that can be withdrawn |
percentageOfAccruedWage | number 0–100 | Used when hasPercentageOfAccruedWage is true. A request must be at or below this percentage of earned wages. |
hasPermanentOrConfirmedEmployeeStatus | boolean | Only auto-approve employees whose company link is approved |
hasRequestedLessThanMaxAmountPerCycle | boolean | Enable a maximum total withdrawal amount per pay cycle |
maxAmountPerCycle | number | Used when hasRequestedLessThanMaxAmountPerCycle is true |
hasMadeLessThanMaxRequestsPerMonth | boolean | Only auto-approve while the employee is under their maxRequestsPerMonth limit |
hasPendingPaymentFromPriorCycles | boolean | Accepted and stored, but not applied to auto-approval at the moment |
enabledBetweenTimePeriods | boolean | Restrict auto-approval to a specific time window |
smartAutoApprovalStartTime | string HH:MM | Start of the auto-approval window, 24-hour format. Set it when enabledBetweenTimePeriods is true. |
smartAutoApprovalEndTime | string HH:MM | End of the auto-approval window |
smartAutoApprovalDays | string | WEEKDAYS, WEEKENDS, or EVERYDAY |
Per-employee settings
Get each employee's linkId from GET /api/public/v1/companies/:companyId/ewa/employees (EWA_READ). Send at least one of: enrolled (boolean), maxWithdrawalPercentage (0–50), approvalMode (MANUAL or AUTOMATIC), maxRequestsPerMonth (1–50).
hasPercentageOfAccruedWage (50%) and enabledBetweenTimePeriods means requests are auto-approved only if they are within the time window AND within the wage cap.What's next
The company is now live and fully configured. The next step is onboarding employees — continue to the Employee Onboarding Guide for the full flow covering registration, phone verification, selfie upload, company linking, and work email verification.
companyId and workEmail during registration, the company link is created in the same call and you can skip the separate link step.Employee link status lifecycle
Every new company link starts as PENDING. After work email verification, the link is approved immediately if the email matches the company domain. Otherwise it stays pending until a company admin reviews it.
companyId and workEmail, Work Benefit creates a pending company link and emails a separate 4-digit code to the work email."firstName": "Ama",
"lastName": "Mensah",
"phoneNumber": "+233201234567",
"companyId": 42,
"workEmail": "ama@acmecorp.com"
}
| Field | Type | Notes | |
|---|---|---|---|
firstName | string | required | 1–100 characters |
lastName | string | required | 1–100 characters |
phoneNumber | string | required | International format, e.g. +233201234567. Receives the SMS code and must be unique. |
companyId | integer | optional | A company created with your developer account. If present, workEmail is also required. |
workEmail | string | optional | If present, companyId is also required. Must match the company domain when the domain is enforced. |
gender or dateOfBirth here; the request is rejected with 400. Set them later with PATCH /api/public/v1/employees/:employeeId/profile."status": "success",
"message": "Employee registered successfully",
"data": {
"id": 108,
"firstName": "Ama",
"lastName": "Mensah",
"phoneNumber": "+233201234567",
"gender": null,
"dateOfBirth": null,
"createdAt": "2026-04-24T10:15:00.000Z",
"companyLinks": [
{
"id": 55,
"workEmail": "ama@acmecorp.com",
"emailVerified": false,
"status": "PENDING",
"createdAt": "2026-04-24T10:15:00.000Z",
"company": { "id": 42, "name": "Acme Corp", "code": "A1B2C3D4", "status": "ACCEPTED" }
}
]
}
}
data.id. You will use it as the employee id in every later employee call.| Status | Message | Fix |
|---|---|---|
400 | Work email must use the company domain: acmecorp.com | Use a work email on the company domain |
404 | Company not found | Use a company created with your developer account |
409 | An account with this phone number already exists | The phone number is already registered |
"status": "success",
"message": "Phone number verified successfully"
}
POST /api/public/v1/employees/:id/resend-phone-otp (no body) to send another SMS code.| Status | Message | Fix |
|---|---|---|
409 | Invalid OTP code | Ask the employee to check the code and try again |
409 | OTP code has expired | Resend the code |
409 | Phone number is already verified | Continue to the next step |
Send the image as multipart/form-data in a field named selfie.
-H "X-Api-Key: wb_test_..." \
-F "selfie=@ama.jpg"
"status": "success",
"message": "Selfie uploaded successfully",
"data": {
"selfie": "https://.../employees/108/selfie/....jpg",
"previousSelfie": null
}
}
| Status | Message | Fix |
|---|---|---|
400 | Selfie image is required | Send the file in the selfie field |
400 | Verify the employee's phone number before uploading a selfie | Complete Step 2 first |
400 | Face check failed (no face, several faces or poor quality) | Ask the employee for a clear photo of their face only |
companyId and workEmail during registration. Linking creates a PENDING link and emails a 4-digit code to the work email. An employee can have only one active company link at a time."companyId": 42,
"workEmail": "ama@acmecorp.com"
}
"status": "success",
"data": {
"id": 55,
"companyId": 42,
"workEmail": "ama@acmecorp.com",
"status": "PENDING",
"otpSent": true,
"createdAt": "2026-04-24T10:15:00.000Z"
}
}
| Status | Message | Fix |
|---|---|---|
400 | A work email with the domain @acmecorp.com is required to link to this company | Use the company's domain when email-domain enforcement is enabled |
404 | Company not found | Use a valid company linked to your developer account |
409 | Your current company link must be revoked before linking to a new company | Only one active company relationship is allowed at a time |
409 | Your link to this company was rejected and cannot be re-attempted | Rejected links are terminal for that employee-company pair |
409 | This work email is already in use at this company | Use the employee's own work email |
companyId. After successful verification, the link becomes APPROVED immediately when the verified email matches the company domain. Otherwise, it remains PENDING until a company admin reviews it on the Work Benefit platform."companyId": 42,
"code": "4821"
}
"status": "success",
"data": {
"id": 55,
"companyId": 42,
"workEmail": "ama@acmecorp.com",
"status": "APPROVED",
"createdAt": "2026-04-24T10:15:00.000Z"
}
}
POST /api/public/v1/employees/:id/resend-work-email-otp with { "companyId": 42 } to send another work email OTP.| Status | Message | Fix |
|---|---|---|
409 | Invalid OTP code | Ask the employee to check the code and try again |
409 | OTP has expired. Please re-initiate the link. | Link the company again (Step 4) |
409 | Work email is already verified | Continue to Step 6 |
404 | Company link not found | Check companyId, or link the company first |
"status": "success",
"data": {
"id": 108,
"companyLinks": [
{
"id": 55,
"workEmail": "ama.personal@gmail.com",
"emailVerified": true,
"status": "PENDING",
"company": { "id": 42, "name": "Acme Corp" }
}
]
}
}
| Status | What it means | What to do |
|---|---|---|
| PENDING | Awaiting company-admin review | Wait for the company admin to approve or reject in the Work Benefit platform |
| APPROVED | The employee is successfully linked to the company | Continue with income verification and benefit usage flows |
| REJECTED | The company declined the employee link | This employee cannot re-attempt linking to the same company |
/api/public/v1/employees/:employeeId/income.Verification lifecycle
The employee can self-report salary before verification. Once an income verification is approved, that salary becomes locked. If an admin needs more evidence, the verification stays PENDING and the employee updates the existing request instead of creating a new one.
1 · Check current salary
"status": "success",
"data": {
"amount": "5000.00",
"isVerified": false
}
}
2 · Set or update salary
"status": "success",
"data": {
"amount": "5000.00",
"isVerified": false,
"previousAmount": 4500
}
}
| Status | Message | Fix |
|---|---|---|
409 | Cannot update a verified salary | Once verification is approved, treat salary as read-only |
Case A — first-time or still pending
"status": "success",
"data": {
"id": 12,
"status": "PENDING",
"incomeAmount": "5000.00",
"documents": [],
"rfes": [
{ "id": 3, "adminComment": "Please provide a clearer bank statement" }
],
"documentVerificationFee": "UNPAID"
}
}
Case B — approved, no new review pending
"status": "success",
"data": {
"id": 7,
"status": "APPROVED",
"incomeAmount": "4500.00",
"verifiedAmount": "4500.00",
"salaryReview": null
}
}
Case C — approved, but a salary review is pending
"status": "success",
"data": {
"id": 7,
"status": "APPROVED",
"incomeAmount": "4500.00",
"salaryReview": {
"id": 12,
"status": "PENDING",
"incomeAmount": "5500.00"
}
}
}
documentVerificationFee is PAID, WAIVED or UNPAID; it is waived automatically when the employee's work email matched the company domain. Approval and rejection are done by Work Benefit, not through the API.PATCH instead of submitting another POST."monthlyIncome": 5000,
"bank": [
{
"name": "January Statement",
"bankName": "GCB Bank",
"password": "abc123",
"file": "data:application/pdf;base64,..."
}
],
"momo": [
{
"name": "MTN Jan",
"file": "data:application/pdf;base64,..."
}
],
"otherFiles": [
{
"type": "PAY_SLIP",
"file": "data:application/pdf;base64,..."
}
]
}
| Field | Type | Notes | |
|---|---|---|---|
monthlyIncome | number | required | Must be 0 or greater |
bank | array | optional | Bank statement documents |
momo | array | optional | Mobile money statement documents |
otherFiles[].type | string | PAY_SLIP, OFFER_LETTER, BUSINESS_REGISTRATION, or OTHER | |
otherFiles[].file | string | Base64-encoded file content. No name field for other files. |
"status": "success",
"data": {
"id": 42,
"status": "PENDING",
"incomeAmount": "5000.00",
"documents": [
{ "id": 156, "documentType": "BANK_STATEMENT" },
{ "id": 157, "documentType": "PAY_SLIP" }
]
}
}
| Status | Message | Fix |
|---|---|---|
409 | A pending income verification already exists | Load the existing pending request and update it with PATCH instead |
bank, momo, or otherFiles array, the service deactivates the currently active documents for those document types and stores the new uploads instead.All fields are optional. Send only the parts that need to change.
"monthlyIncome": 5500,
"bank": [
{
"name": "Updated Statement",
"bankName": "GCB Bank",
"file": "data:application/pdf;base64,..."
}
]
}
"status": "success",
"data": {
"id": 42,
"status": "PENDING",
"incomeAmount": "5500.00",
"documents": [
{ "id": 201, "documentType": "BANK_STATEMENT", "isActive": true }
]
}
}
404 with No pending income verification found. In that case, start a new request with POST instead.Prerequisites
Before any EWA request can be submitted, the following must all be true. The API returns a descriptive error if any condition is not met.
| Condition | How to satisfy it |
|---|---|
Employee has an APPROVED link to a company with status ACCEPTED | Complete the Employee Onboarding flow first |
The company has the EWA benefit enrolled and ACTIVE, and the employee is enrolled in it | Complete Step 5 of the Company Registration flow. Approved employees are enrolled automatically. |
| The employee has a verified salary on file | Complete the Income Verification flow |
| The employee account is active | Contact Work Benefit if the account was deactivated |
No other EWA request is PENDING | Wait for the existing request to be approved or rejected |
| The employee has a PIN (for withdrawals only) | Step 4 of this guide |
EWA request status lifecycle
Requests start as PENDING. Depending on the company's EWA auto-approval settings, a request may be approved immediately on submission or it may wait for manual approval by a company admin (or by you, through the company EWA endpoints).
"status": "success",
"data": {
"earnedWage": 1820.00,
"availableToUse": 1820.00,
"maxAmountAllowed": 2100.00,
"availableToWithdraw": 0.00,
"serviceChargePercentage": 0.049,
"maximumWithdrawalPercentage": 70,
"totalRequestedAmount": 0.00,
"totalRequests": 0,
"cutoffDate": "2026-04-28T00:00:00.000Z",
"repaymentDate": "2026-04-30T00:00:00.000Z",
"linkId": 55,
"message": "You can withdraw all of your estimated earned salary"
}
}
| Field | Description |
|---|---|
earnedWage | Wages accrued so far this pay cycle based on days worked |
availableToUse | Amount still available to request this cycle. The request amount must not be higher than this. |
maxAmountAllowed | Upper limit for this cycle, based on the employee's maximumWithdrawalPercentage and the company's salary exposure cap |
availableToWithdraw | Approved funds not yet transferred out — ready to withdraw now |
serviceChargePercentage | Service charge rate applied to each request (0.049 = 4.9%) |
cutoffDate | Deadline for submitting EWA requests this pay cycle |
repaymentDate | Date the advance is deducted from the employee's salary |
0, linkId is null and message explains why. Returns 400 if the employee has no salary on file.autoApproved is true. Otherwise it stays PENDING for manual review."status": "success",
"message": "Earned wage request submitted successfully",
"data": {
"id": 77,
"reference": "EWA-20260427-A3BX9Z",
"companyLinkId": 55,
"amount": "500",
"serviceCharge": "24.5",
"status": "APPROVED",
"requestDate": "2026-04-27T11:20:00.000Z",
"cutOffDate": "2026-04-28T00:00:00.000Z",
"repaymentDate": "2026-04-30T00:00:00.000Z",
"approvedById": null,
"createdAt": "2026-04-27T11:20:00.000Z",
"updatedAt": "2026-04-27T11:20:00.000Z",
"autoApproved": true
}
}
| Status | Message | Fix |
|---|---|---|
400 | You have a pending request | Wait for the existing pending request to be approved or rejected before submitting a new one |
400 | Amount requested exceeds available amount. Available: {n} | Use a value ≤ availableToUse from Step 1 |
400 | EWA requests are no longer accepted for this pay cycle. The cut-off date has passed. | Wait for the next pay cycle |
404 | You are not associated with an active company. Please contact your administrator. | The employee needs an approved link to an ACCEPTED company |
403 | Earned Wage Access is not currently available at your company. | Enroll the EWA benefit for the company and make sure it is ACTIVE |
403 | You are not enrolled for Earned Wage Access. Please contact your administrator for assistance. | Enroll the employee with PATCH /companies/:companyId/ewa/employees/:linkId and { "enrolled": true } |
403 | Your salary details have not been verified yet. Please contact your administrator. | Complete the income verification flow first |
403 | Sandbox API keys cannot be used in a production environment. | Use a wb_live_ key for production |
autoApproved is true, skip Step 3. The funds are ready to withdraw.PATCH /api/public/v1/companies/:companyId/ewa/requests/:requestId/approve or /reject (EWA_WRITE). Poll the requests list to see when the status changes to APPROVED or REJECTED.Filter by status to narrow results. Supports pagination via page and pageSize (max 100).
"status": "success",
"data": {
"data": [
{
"id": 77,
"reference": "EWA-20260427-A3BX9Z",
"status": "APPROVED",
"amount": 500,
"serviceCharge": 24.5,
"requestDate": "2026-04-27T11:20:00.000Z",
"cutOffDate": "2026-04-28T00:00:00.000Z",
"repaymentDate": "2026-04-30T00:00:00.000Z",
"companyName": "Acme Corp"
}
],
"total": 1,
"page": 1,
"pageSize": 20,
"totalPages": 1
}
}
| Status | What it means | What to do |
|---|---|---|
| PENDING | Awaiting manual review | Poll again later |
| APPROVED | Funds are ready to withdraw | Continue to Step 4 |
| REJECTED | Request was declined | Inform the employee and let them submit a new request in the next cycle |
1 · Check whether the employee has a PIN
"status": "success",
"message": "PIN status fetched successfully",
"data": { "hasPin": false }
}
If hasPin is true, skip to Step 5.
2 · Create a PIN setup session
redirectUrl is optional. It is where the employee is sent after setting the PIN.
"status": "success",
"message": "PIN setup session created",
"data": {
"url": "https://api-v2.workbenefits.app/hosted/pin/3f9c...",
"expiresAt": "2026-04-27T12:05:00.000Z"
}
}
3 · Employee sets the PIN on the hosted page
url. They choose a 4-digit PIN and are then sent to your redirectUrl. The URL can be used once and expires after 30 minutes.Pass accountType (BANK or MOBILE_MONEY) and amount as query parameters.
"status": "success",
"data": {
"fee": 7,
"requestedAmount": 500,
"receivedAmount": 493,
"minimumAmount": 10
}
}
"amount": 500,
"accountType": "BANK",
"redirectUrl": "https://yourapp.com/ewa/complete"
}
| Field | Type | Notes | |
|---|---|---|---|
amount | number | required | Amount to withdraw, at least 1, up to 2 decimal places. It must also be at least GHS 10 and no more than availableToWithdraw when the employee submits. |
accountType | string | required | BANK or MOBILE_MONEY |
redirectUrl | string (URL) | optional | Where to send the employee after a successful withdrawal |
"status": "success",
"data": {
"url": "https://api-v2.workbenefits.app/hosted/withdrawal/eyJhbGciO...",
"expiresAt": "2026-04-27T12:05:00.000Z"
}
}
| Status | Message | Fix |
|---|---|---|
403 | Your account has been deactivated. Please contact your administrator for assistance. | The employee account is inactive |
403 | Sandbox API keys cannot be used in a production environment. | Use a wb_live_ key for production |
404 | Employee not found | Use an employee registered with your developer account |
BANK, they pick their bank and enter the account number. For MOBILE_MONEY, they pick the network (MTN, Telecel or AirtelTigo) and enter the phone number. They then enter their 4-digit PIN and submit. Work Benefit starts the transfer.redirectUrl, if you set one. If something is wrong (wrong PIN, no PIN set, amount above the available balance, below GHS 10, or above the GHS 2,500 daily limit), the page shows the error to the employee. Check the result in the employee's transaction history.
Check withdrawal outcome
Supports page, pageSize (max 100) and type query parameters. type is one of requests, withdrawals, rent or shop. Use type=withdrawals to see only withdrawals. The result is grouped into separate lists, not the standard list envelope.
"status": "success",
"message": "Transaction history retrieved",
"data": {
"requests": [ ... ],
"withdrawals": [
{
"id": 203,
"reference": "...",
"status": "COMPLETED",
"transferType": "BANK",
"amount": "500",
"transferFee": "7",
"createdAt": "2026-04-27T11:45:00.000Z"
}
],
"external": [ ... ],
"total": 1,
"page": 1,
"pageSize": 20
}
}
PROCESSING (transfer started, waiting for the payment provider), COMPLETED (funds delivered), FAILED (transfer failed).Application stage lifecycle
Each application moves through five stages in order. Poll GET /rent/applications/:id and read the stage field to know what to do next.
Prerequisites
All of the following must be in place before a rent application can be submitted.
| Condition | How to satisfy it |
|---|---|
| Employee has a salary on file | Set via PUT /employees/:id/income — does not need to be verified first |
| Employee has an emergency contact | Add via POST /employees/:id/emergency-contact |
Employee is linked to a company with Rent benefit ACTIVE | Complete Employee Onboarding and enroll the Rent benefit via Company Registration Step 5 |
/identity-verification and /income/verification ensures Renmo has the evidence it needs to approve the application faster.1 · Get the employee's rent limit
"status": "success",
"data": {
"maxMonthlyPayment": 2028,
"maxTotalPayback": 18720,
"maxPaybackPeriod": 12,
"premuimRate": 0.025
}
}
premuimRate in the response. Returns 404 if the employee has no salary on file. maxMonthlyPayment and maxTotalPayback are the ceiling. The employee can choose a lower amount or shorter duration. Use the calculator below to show them the cost for any combination.2 · Preview costs for a specific amount & duration
Pass amount (the rent cost to finance) and paybackDuration (months to repay) as query parameters. premiumRate is optional; the default is 2.5% per month.
"status": "success",
"data": {
"totalPayback": 5750,
"monthlyRent": 958
}
}
3 · Check physical inspection fees
"status": "success",
"data": {
"standardFee": 130,
"expressFee": 250
}
}
4 · Check qualification (optional)
Pass amount and paybackDuration as query parameters. The check uses the employee's income and employment details.
"status": "success",
"message": "Rent qualification checked successfully",
"data": { "results": "P" }
}
results is "P" when the employee qualifies, or "F" with a message when they do not.
"monthlyRentBudget": 1200,
"durationOfRent": 12,
"rentPaybackPeriod": 6,
"fullNameOfLandlord": "Kwame Asante",
"phoneNumberOfLandlord": "+233201234567",
"propertyGpsAddress": "GS-0001-0001"
}
| Field | Type | Notes | |
|---|---|---|---|
monthlyRentBudget | number | required | The employee's monthly rent cost in GHS |
durationOfRent | integer | required | How many months the tenancy lasts (minimum 1) |
rentPaybackPeriod | integer | required | How many months to repay the advance (minimum 1). Keep it within maxPaybackPeriod from the limit. |
fullNameOfLandlord | string | optional | Landlord's full name |
phoneNumberOfLandlord | string | optional | Landlord's contact number |
propertyGpsAddress | string | optional | Ghana Post GPS address of the property |
"status": "success",
"data": {
"id": 8801,
"application_date": "2026-04-27",
"monthly_rent_value": "1200",
"payback_duration": 6,
...
}
}
data.id as the applicationId. You will need it for every subsequent step in this flow.| Status | Message | Fix |
|---|---|---|
404 | No emergency contact found | Add an emergency contact via POST /employees/:id/emergency-contact first |
404 | No salary information found | Set salary via PUT /employees/:id/income first |
400 | You are not eligible for this benefit. A minimum monthly salary of GHS 1,500 is required. | The employee's salary is below the minimum |
"expressServiceForPhysicalInspection": false,
"dateAndTimeForVerification": "2026-05-02"
}
| Field | Type | Notes | |
|---|---|---|---|
expressServiceForPhysicalInspection | boolean | optional | When true, charges GHS 250 (express). When false or omitted, charges GHS 130 (standard). |
dateAndTimeForVerification | string | optional | Preferred inspection date, format YYYY-MM-DD |
"status": "success",
"data": {
"authorization_url": "https://checkout.paystack.com/...",
"access_code": "...",
"reference": "..."
}
}
Redirect the employee to authorization_url. Once they complete the payment, the application stage advances to PAID_PHYSICAL_INSPECTION.
paymentSchedule and moveInDepositBreakdown are filled in."status": "success",
"data": {
"applicationId": 8801,
"stage": "PAID_PHYSICAL_INSPECTION",
"moveInDepositBreakdown": {
"monthlyRent": 1200,
"refundableRentSecurity": 1200,
"serviceFee": 300,
"extraPayment": 0,
"proRatedFirstMonth": 0,
"totalMoveInDeposit": 2700
},
"paymentSchedule": [ ... ]
}
}
Once the payment plan is in place, the tenancy agreement is ready to review and sign. Continue to Step 5.
1 · Generate the tenancy agreement link
"status": "success",
"data": {
"link": "https://api-v2.workbenefits.app/api/v2/employee/rent/tenancy-agreement/eyJ..."
}
}
2 · Optionally email the agreement
3 · Record the employee's signature
No request body required. Calling this endpoint records the employee's acceptance of the tenancy agreement terms.
"status": "success",
"data": { "tenant_has_signed": "yes", ... }
}
totalMoveInDeposit breakdown from Step 4, then initialize a Paystack payment for that total. Once paid, Work Benefit disburses funds to the landlord and the stage advances to PAID_INITIAL_DEPOSIT.No request body required. The amount is taken from the application's initial deposit. Returns 400 if the deposit is not available yet.
"status": "success",
"data": {
"authorization_url": "https://checkout.paystack.com/...",
"access_code": "...",
"reference": "..."
}
}
Redirect the employee to authorization_url. After payment, the stage advances to PAID_INITIAL_DEPOSIT and the employee can move in.
1 · Get the payment plan
"status": "success",
"data": [
{
"applicationId": 8801,
"rentMonth": "May",
"rentYear": 2026,
"amount": 958,
"amountPaid": 0,
"totalAmountDue": 958,
"fines": 0,
"isLate": false,
"paymentStatus": "Unpaid",
"expectedPaymentDate": "2026-05-30"
},
...
]
}
2 · Initialize a monthly payment
"status": "success",
"data": {
"authorization_url": "https://checkout.paystack.com/...",
"reference": "..."
}
}
3 · View payment history
"status": "success",
"data": [
{
"id": 451,
"paymentPurpose": "PHYSICAL_VERIFICATION_FEE",
"amountPaid": 130,
"transactionReference": "ps_ref_abc123",
"datePaymentWasMade": "2026-04-27",
"paymentMonth": null,
"paymentTitle": "Physical Inspection Fee"
},
...
]
}
paymentPurpose values: PHYSICAL_VERIFICATION_FEE, INITIAL_DEPOSIT, RENT_PAYMENT. Use this history to reconcile what the employee has paid and what remains outstanding.