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 — you’ll use it in the next steps.
Payroll Types
Step 2: Save Entries
Add the pay date, pay period, and compensation entries for each worker. Each entry requires apersonId 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. Saving is asynchronous — the payroll status moves to Pending Submission a few seconds later (poll with backoff before approving; see Payroll Lifecycle). Fetch the payroll with GET /v1/payrolls/{id} to see the calculated debit — the total amount that will be debited from the business account. What the debit collects depends on each worker’s employment model.
Amounts are in the lowest denomination of the currency. For USD,
100000 = $1,000.00. For GHS, 500000 = GHS 5,000.00.Per-Cycle Compensation Fields
BeyondpersonId and salary, each entry accepts per-cycle compensation overrides. Fields are optional — send what’s relevant to the cycle.
Example:
$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}/taxProfileCustom Metadata
You can pass atags 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 acustomFees 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.
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 returns400 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:
Step 3: Approve
Once you’re satisfied with the entries, approve the payroll. This triggers fund collection and schedules disbursement. Returns204 on success. The payroll moves to Awaiting Funds (the payroll.status.updated webhook reports this as approved).
After approval:
- With sufficient funds, the payroll moves to
Scheduledwithin moments. With insufficient funds, it stays inAwaiting Fundsuntil you fund your account — then it proceeds automatically, no re-approval needed. - If the scheduled date is in the future, the payroll will be processed on the scheduled day.
Tracking Payroll Status
Poll the payroll to check its current status and see calculated totals. The response shape depends on the payroll’sworkerType — see Response Shape by Worker Type below.
Contractor response:
Enum values are returned in mixed case (
Contractor, One_off, Completed) — compare them case-insensitively.A payroll can report
Completed up to a minute before the funds are visible in worker wallets. To confirm a payment landed, follow the entry’s transactionIds — each ID resolves via GET /v1/users/{userId}/transactions/{transactionId} — or check the worker’s wallet balance.Response Shape by Worker Type
ForEMPLOYEE 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):
Per-entry (employee payrolls only):
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:
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. Payslips exist only forCompleted payrolls. Once a payroll completes, call GET /v1/payrolls/{payrollId}/payslip-links. By default the response returns one URL per person.
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.
URLs expire shortly after issuance (see
expiresAt) — download or rehost them promptly rather than storing them in long-lived UI. Payslips for not-yet-processed payrolls, or still rendering, appear under notReady; retry the request once the payroll is Completed and generation finishes to pick them up.List All Payrolls
Response:Status Flow
Webhooks
Subscribe to payroll and transaction events to track progress without polling. Payroll status changes:JSON
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
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