Skip to main content
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:
Save the 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 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. 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.
Pass ?includeFxRates=true to receive the captured FX rates synchronously in a 200 OK body — { "fxRates": { "USD-PHP": 58.75, ... } }, keyed {fundingCurrency}-{salaryCurrency} — instead of polling GET /v1/payrolls/{id}. Omitting the flag preserves the default 204 No Content behavior.
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. Example:
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.
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:
Error 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 moves to Awaiting Funds (the payroll.status.updated webhook reports this as approved). After approval:
  • With sufficient funds, the payroll moves to Scheduled within moments. With insufficient funds, it stays in Awaiting Funds until 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’s workerType — see Response Shape by Worker Type below. Contractor response:
Enum values are returned in mixed case (Contractor, One_off, Completed) — compare them case-insensitively.
The response includes an invoiceId once entries are saved (from Pending Submission onward). Use GET /v1/invoices/{invoiceId} to retrieve a detailed breakdown of fees charged for the payroll run.
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

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): 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 for Completed payrolls. Once a payroll completes, call GET /v1/payrolls/{payrollId}/payslip-links. By default the response returns one URL per person.
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 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.
In sandbox, save the payroll with a past payrollDate — it completes immediately on approval, making payslips available right away. See Sandbox & Testing.

List All Payrolls

Response:
List items are summaries — fetch a payroll by ID for its type, dates, and entries.

Status Flow

See Payroll Lifecycle for the full status list (including terminal states), the meaning of each value, webhook payloads, and multi-currency handling.

Webhooks

Subscribe to payroll and transaction events to track progress without polling. Payroll status changes:
JSON
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


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