Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.cadanapay.com/llms.txt

Use this file to discover all available pages before exploring further.

This guide details the steps needed for a business to pay its workers via a scheduled payroll. Cadana handles fund collection, scheduling, and disbursement automatically.

Prerequisites

1

API key from Dashboard

Get your API key from the Cadana Dashboard. See Authentication for details.
2

Compliance-approved business

Your business must have completed KYB verification. See KYB Requirements.
3

Account funded

Your business account must have sufficient funds to cover the payroll. See Fund Your Account.
4

Onboarded workers

Workers must be onboarded as Persons with payment methods configured — you’ll use their personId to set up pay entries. See Onboard Workers.

Step 1: Create a Payroll

Start by creating an empty payroll shell. Specify the worker type and whether this is a one-off or regular payroll. Response:
{
  "payrollId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789"
}
Save the payrollId — you’ll use it in the next steps.

Payroll Types

FieldValuesDescription
workerTypeEMPLOYEE, CONTRACTORType of workers being paid
typeONE_OFF, REGULAROne-off payment or recurring payroll

Step 2: Save Entries

Add the pay date, pay period, and compensation entries for each worker. Each entry requires a personId and salary amount. If you set compensation info (compInfo) when onboarding the worker, you can retrieve it from the Person profile with GET /v1/persons/{personId} and use those values for the salary entry. Returns 204 on success. The payroll status moves to saved. Fetch the payroll with GET /v1/payrolls/{id} to see the calculated debit — the total amount that will be debited from the business account.
Amounts are in the lowest denomination of the currency. For USD, 100000 = $1,000.00. For GHS, 500000 = GHS 5,000.00.
You can re-save a payroll to update entries before approving. Each save replaces the previous entries.
For workers with variable pay (e.g., hourly contractors), set compInfo.salary to 0 during onboarding and provide the actual amount when saving payroll entries.

Per-Cycle Compensation Fields

Beyond personId and salary, each entry accepts per-cycle compensation overrides. Fields are optional — send what’s relevant to the cycle.
FieldTypeDescription
bonusMoneyBonus paid this period.
commissionMoneyCommission paid this period.
allowancesAllowance[]Non-statutory allowance line items (e.g., housing, transport stipends).
deductionsDeduction[]Voluntary deduction line items (e.g., pension top-ups, garnishments, equipment repayments).
timeWorkednumberUnits of work this period (hours, days, or 1 for salaried-monthly). The unit is implied by the person’s compInfo.frequency — never sent on the entry. Defaults to 1 when omitted.
overtimeMoneyOvertime rate (when paired with overtimeWorked) or a flat overtime amount (when sent alone).
overtimeWorkednumberOvertime units worked this period. Multiplies against overtime when both are sent. Must be non-negative.
Example:
{
  "personId": "8ef9a712-cdae-4110-b1ea-9ba95abbee6e",
  "salary":     { "amount": 500000, "currency": "USD" },
  "bonus":      { "amount":  50000, "currency": "USD" },
  "commission": { "amount":  25000, "currency": "USD" },
  "allowances": [
    { "name": "Wellness Stipend",  "isTaxable": true,  "amount": { "amount": 20000, "currency": "USD" } },
    { "name": "Internet Stipend",  "isTaxable": false, "amount": { "amount": 10000, "currency": "USD" } }
  ],
  "deductions": [
    { "name": "Voluntary Pension Top-up", "isStatutory": false, "amount": { "amount": 15000, "currency": "USD" } }
  ]
}
Reads as $5,000 monthly base + $500 bonus + $250 commission + $200 wellness stipend + $100 internet stipend − $150 voluntary pension. Statutory items (income tax, social security, etc.) are computed by the engine and returned on GET.
Frequency is always sourced from the person’s stored compInfo.frequency. Sending it on the entry is unsupported — adjust the person’s frequency via PUT /v1/persons/{personId}/jobInfo if it needs to change.
Tax profile information are person-level, not per-cycle. Set them once via PUT /v1/persons/{personId}/taxProfile

Custom Metadata

You can pass a tags object on both the create and save requests to store custom metadata against the payroll (e.g., internal reference IDs, department codes). This is a free-form object — any key-value pairs you include will be persisted and returned when you retrieve the payroll.

Custom Fees (Platforms)

Platforms can pass a customFees array on the save request to add platform-specific fee line items to the payroll invoice. Each item requires a name and amount. These are additive — they supplement standard per-seat subscription fees and are included in the total amount collected by Cadana on your behalf.
{
  "customFees": [
    { "name": "Platform Fee", "amount": { "amount": 50000, "currency": "USD" } },
    { "name": "Processing Fee", "amount": { "amount": 10000, "currency": "USD" } }
  ]
}
Custom fee items do not participate in revenue share calculations. Validation returns 400 for empty names, non-positive amounts, or missing currency.

Save Errors

When saving entries fails validation, the API returns 400 with an invalid_request_body code. Each error in the params object is prefixed with a [ErrorType] tag you can use to programmatically filter or map errors. Response format:
{
  "code": "invalid_request_body",
  "message": "One or more input values are invalid. Please re-renter valid values",
  "params": {
    "entry-0-paymentInfo": "[EmptyWalletDetails] Jane Doe has invalid payment method set: Wallet details cannot be empty for wallet payments",
    "entry-1-personId": "[PersonNotFound] Person ID 8ef9a712-cdae-4110-b1ea-9ba95abbee6e is not a valid person in the system"
  }
}
Error types:
Error TypeDescription
NoPaymentMethodPerson has no payment method configured
PersonNotFoundPerson ID does not match any person in the system
EmptyWalletDetailsWallet details are missing when payment method is wallet
InvalidPreferredMethodPreferred method is not a supported type
RestrictedCountryBeneficiary country is restricted for payments
InvalidBankDetailsBank payment validation failed (e.g., missing account name, invalid IBAN)
InvalidMomoDetailsMobile money validation failed (e.g., missing phone number, provider)
InvalidWalletDetailsWallet validation failed (e.g., invalid currency)
InvalidCryptoDetailsCrypto wallet validation failed (e.g., missing address, unsupported chain)
InvalidAchDetailsACH payment validation failed
InvalidSwiftDetailsSWIFT payment validation failed
InvalidCardDetailsCard payment validation failed
InvalidProxyDetailsProxy payment validation failed
InvalidPaymentDetailsCatch-all for unrecognized payment method types
To extract the error type programmatically, split the param value on "] " — the text before it is the error type (without the leading [).

Step 3: Approve

Once you’re satisfied with the entries, approve the payroll. This triggers fund collection and schedules disbursement. Returns 204 on success. The payroll status moves to approved. After approval:
  • If the business has insufficient funds, the payroll moves to awaiting funds. If a bank account is connected, an ACH debit initiates automatically.
  • If the scheduled date is in the future, the payroll will be processed on the scheduled day.
  • Once funds are available, the payroll processes and disburses payments to workers automatically.

Tracking Payroll Status

Poll the payroll to check its current status and see calculated totals. The response shape depends on the payroll’s workerType — see Response Shape by Worker Type below. Contractor response:
{
  "payrollId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789",
  "workerType": "CONTRACTOR",
  "status": "completed",
  "type": "ONE_OFF",
  "invoiceId": "8dbaa174-b5a0-4396-8c87-77e746b50917",
  "payrollDate": "2026-02-28",
  "numPeople": 2,
  "debit": { "amount": 175000, "currency": "USD" },
  "gross": { "amount": 175000, "currency": "USD" },
  "net":   { "amount": 175000, "currency": "USD" },
  "payPeriod": {
    "fromDate": "2026-02-01",
    "toDate": "2026-02-28"
  },
  "entries": [
    {
      "personId": "8ef9a712-cdae-4110-b1ea-9ba95abbee6e",
      "salary": { "amount": 100000, "currency": "USD" },
      "gross":  { "amount": 100000, "currency": "USD" },
      "net":    { "amount": 100000, "currency": "USD" }
    },
    {
      "personId": "3c7d9e21-fa4b-4a12-bc89-0d1234567890",
      "salary": { "amount": 75000, "currency": "USD" },
      "gross":  { "amount": 75000, "currency": "USD" },
      "net":    { "amount": 75000, "currency": "USD" }
    }
  ]
}
Once a payroll reaches completed, the response includes an invoiceId. Use GET /v1/invoices/{invoiceId} to retrieve a detailed breakdown of fees charged for the payroll run.

Response Shape by Worker Type

For EMPLOYEE payrolls, the response also includes withholding aggregates at the top level and per-entry line items so you can reconcile gross-to-net for each worker. None of these fields appear on CONTRACTOR payrolls. Aggregate-level (employee payrolls only):
FieldTypeDescription
taxMoneyTotal tax withheld across all entries
pensionMoneyTotal pension contribution across all entries
statutoryDeductionsMoneyTotal of all legally-mandated deductions (income tax, social security, etc.)
employerContributionsMoneyTotal employer-side contributions (e.g., employer social security)
Per-entry (employee payrolls only):
FieldTypeDescription
allowancesAllowance[]Allowance line items (e.g., Transport, Housing). Omitted when empty.
deductionsDeduction[]Deduction line items. isStatutory distinguishes legally-mandated items from voluntary ones. Omitted when empty.
employerContributionsDeduction[]Employer-side contribution line items. Omitted when empty.
The Allowance and Deduction line-item shapes are the same ones returned by POST /v1/tax/calculate and POST /v1/tax/estimate. Employee response example:
{
  "payrollId": "7c4f1d36-2b9a-4bca-91e7-2b1f1e6e7c11",
  "workerType": "EMPLOYEE",
  "type": "REGULAR",
  "status": "Pending Submission",
  "payrollDate": "2021-06-26",
  "debit": { "amount": 31947900, "currency": "PHP" },
  "gross": { "amount": 31947900, "currency": "PHP" },
  "net":   { "amount": 31947900, "currency": "PHP" },
  "tax":                   { "amount": 0, "currency": "PHP" },
  "pension":               { "amount": 0, "currency": "PHP" },
  "statutoryDeductions":   { "amount": 0, "currency": "PHP" },
  "employerContributions": { "amount": 0, "currency": "PHP" },
  "payPeriod": { "fromDate": "2021-06-01", "toDate": "2021-06-30" },
  "entries": [
    {
      "personId": "04a8977d-5d99-4b28-8de4-8161401ca3fa",
      "salary": { "amount": 75000, "currency": "PHP" },
      "bonus":  { "amount": 10000, "currency": "PHP" },
      "gross":  { "amount": 85000, "currency": "PHP" },
      "net":    { "amount": 75704, "currency": "PHP" },
      "allowances": [
        { "name": "Transport", "isTaxable": true, "amount": { "amount": 5000,  "currency": "PHP" } },
        { "name": "Housing",   "isTaxable": true, "amount": { "amount": 10000, "currency": "PHP" } }
      ],
      "deductions": [
        { "name": "SSS",             "isStatutory": true,  "amount": { "amount": 4125, "currency": "PHP" } },
        { "name": "Bonus Tax",       "isStatutory": false, "amount": { "amount": 500,  "currency": "PHP" } },
        { "name": "Withholding Tax", "isStatutory": true,  "amount": { "amount": 4671, "currency": "PHP" } }
      ],
      "employerContributions": [
        { "name": "SSS", "isStatutory": false, "amount": { "amount": 9750, "currency": "PHP" } }
      ]
    }
  ]
}
Entry-level gross and net are omitted until the entry has been calculated. New aggregate and per-entry fields are additive — clients that don’t read them are unaffected.

Downloading Payslips

Distribute earning statements — gross pay, withholdings, deductions, and net pay — to workers by fetching presigned download URLs for each payslip PDF. Once a payroll is approved and payslips have been generated, call GET /v1/payrolls/{payrollId}/payslip-links. By default the response returns one URL per person.
{
  "payrollId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789",
  "links": [
    {
      "personId": "8ef9a712-cdae-4110-b1ea-9ba95abbee6e",
      "personName": "Jane Doe",
      "paystubId": "1b2e3f4a-5c6d-7e8f-9012-345678901234",
      "url": "https://example-bucket.s3.amazonaws.com/payslips/8ef9a712.pdf?X-Amz-Signature=...",
      "expiresAt": "2026-05-11T17:00:00Z"
    }
  ],
  "notReady": [
    { "personId": "3c7d9e21-fa4b-4a12-bc89-0d1234567890", "status": "generating" }
  ]
}
Each entry carries personId and paystubId so you can match the payslip back to the worker and pay period in your own system before delivery. personName is included for display. To receive a single archive of every ready payslip instead, pass format=zip. The response then contains a single zipUrl rather than the per-person links array — useful when handing a full payroll’s payslips to a finance team in one download.
Presigned URLs are accessible without authentication — anyone holding the link can download the payslip until it expires. Deliver them only to the intended recipient (over an authenticated channel), and treat the URL itself like a password. Do not log it, paste it into shared chats, or embed it in pages cached by third parties.
URLs expire shortly after issuance (see expiresAt) — download or rehost them promptly rather than storing them in long-lived UI. Payslips still rendering appear under notReady with status: "generating"; retry the request once generation finishes to pick them up.

List All Payrolls

Response:
{
  "data": [
    {
      "payrollId": "a1b2c3d4-e5f6-7890-abcd-ef0123456789",
      "workerType": "CONTRACTOR",
      "status": "completed",
      "type": "ONE_OFF",
      "payrollDate": "2026-02-28",
      "numPeople": 2,
      "debit": { "amount": 175000, "currency": "USD" },
      "gross": { "amount": 175000, "currency": "USD" },
      "net":   { "amount": 175000, "currency": "USD" },
      "payPeriod": {
        "fromDate": "2026-02-01",
        "toDate": "2026-02-28"
      }
    }
  ]
}

Status Flow

created → saved → approved → scheduled → processing → completed

                awaiting funds
StatusMeaning
createdEmpty payroll shell, no entries yet
savedEntries added, ready for approval
approvedApproved, Cadana is collecting funds
awaiting fundsInsufficient balance — payroll will proceed once funded
scheduledFunds collected, scheduled for the payroll date
processingDisbursements being sent to workers
completedAll disbursements confirmed
For the full lifecycle including webhooks and multi-currency handling, see Payroll Lifecycle.

Webhooks

Subscribe to payroll and transaction events to track progress without polling. Payroll status changes:
JSON
{
  "id": "e13b9e14-c062-42ea-8563-8fc9223b29b5",
  "status": "completed",
  "tenantKey": "cad95193904"
}
Individual transaction events (transaction.initiated, transaction.succeeded, transaction.failed) fire for each worker’s payment. See Webhooks to configure your endpoint and Events for all event types.

Troubleshooting

IssueCauseSolution
401 UnauthorizedInvalid or missing API keyCheck your API key in the Dashboard
400 on saveMissing required fieldsEnsure payrollDate, payPeriod, and at least one entry with personId and salary are provided
Stuck in awaiting fundsInsufficient business account balanceFund your business account, then the payroll will proceed automatically
Payroll won’t approveEntries not savedSave entries (Step 2) before approving

Next Steps

Payroll Lifecycle

Full status flow, webhooks, and multi-currency handling

Onboard Workers

Set up Person records and payment methods

Webhooks

Real-time notifications for payroll and transactions

Workforce API Reference

Full API documentation