> ## 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.

# Payroll Lifecycle

> Status transitions, webhook events, multi-currency handling, and edge cases for payrolls

export const ApiExample = ({method = "GET", path, params, body, reference, tenantKey}) => {
  const baseUrl = "https://api.cadanapay.com";
  const query = params ? "?" + Object.entries(params).map(([k, v]) => `${k}=${v}`).join("&") : "";
  const url = `${baseUrl}${path}${query}`;
  const isPlatformPath = (/^\/(v1\/)?platform(\/|$)/).test(path || "");
  const includeTenantKey = tenantKey === undefined ? !isPlatformPath : tenantKey;
  let curl = `curl -X ${method} '${url}' \\\n  -H 'Authorization: Bearer YOUR_API_KEY'`;
  if (includeTenantKey) {
    curl += ` \\\n  -H 'X-MultiTenantKey: YOUR_BUSINESS_TENANT_KEY'`;
  }
  if (body) {
    const formatted = JSON.stringify(body, null, 2);
    curl += ` \\\n  -H 'Content-Type: application/json' \\\n  -d '${formatted}'`;
  }
  return <div>
      <CodeBlock language="bash" filename="bash" wrap>
        {curl}
      </CodeBlock>
      {reference && <div style={{
    marginTop: "-0.5rem",
    marginBottom: "1rem"
  }}>
          <a href={reference} style={{
    fontSize: "0.875rem"
  }}>
            Try it in the playground →
          </a>
        </div>}
    </div>;
};

A payroll moves through a series of statuses from creation to completion. This page covers what happens at each stage, the webhook events you'll receive, and how multi-currency payrolls are handled.

***

## Overview

```mermaid theme={null}
sequenceDiagram
    participant App as Your App
    participant Cadana
    participant Webhook

    App->>Cadana: 1. POST /v1/payrolls
    Cadana->>Webhook: payroll.created

    App->>Cadana: 2. POST /v1/payrolls/{id}/save
    Cadana->>Webhook: payroll.status.updated (saved)

    App->>Cadana: 3. POST /v1/payrolls/{id}/approve
    Cadana->>Webhook: payroll.status.updated (approved)

    Note over Cadana: Automatic (if balance sufficient)
    Cadana->>Webhook: payroll.status.updated (scheduled)

    Note over Cadana: Automatic (on disbursement date)
    Cadana->>Webhook: payroll.status.updated (processing)

    Note over Cadana: Automatic
    Cadana->>Webhook: payroll.status.updated (completed)
```

**Only 3 API calls needed** — everything after approval is automatic.

<Steps>
  <Step title="Create a payroll">
    Select the worker type (employee/contractor) and create a payroll.
    [`POST /v1/payrolls`](/api-reference/workforce/payrolls/create) → Webhook: `payroll.created`
  </Step>

  <Step title="Save entries">
    Provide the pay date, pay period, and compensation for each worker.
    [`POST /v1/payrolls/{id}/save`](/api-reference/workforce/payrolls/save) → Webhook: `payroll.status.updated` (saved)
  </Step>

  <Step title="Approve">
    Approve the payroll to trigger fund collection and scheduling.
    [`POST /v1/payrolls/{id}/approve`](/api-reference/workforce/payrolls/approve) → Webhook: `payroll.status.updated` (approved)
  </Step>
</Steps>

For the full integration walkthrough, see [Pay Workers via Payroll](/workforce/pay-workers-via-payroll).

***

## What Happens After Approval

Once you call `POST /v1/payrolls/{id}/approve`, the rest is automatic:

### Balance Check

Cadana checks whether the business account has sufficient funds to cover the payroll.

* **Sufficient funds** — the payroll moves to `Scheduled` and is queued for the payroll date. Webhook: `payroll.status.updated` (scheduled)
* **Insufficient funds** — the payroll moves to `Awaiting Funds`. If a bank account is connected, an ACH debit initiates automatically. Once the account is funded, the payroll proceeds — no need to re-approve.

### Scheduling

If the payroll date is in the future, the payroll stays in `Scheduled` until that date. If the payroll date is today or in the past, disbursement begins immediately.

### Disbursement

On the payroll date, Cadana triggers disbursements for each worker. The payroll moves to `Processing`. Each worker's payment is handled independently — if one worker's payment fails, the others continue. Webhook: `payroll.status.updated` (processing)

Once the payroll reaches `Processing`, each entry on the `GET /v1/payrolls/{payrollId}` response includes a `transactionIds` array linking it to its underlying disbursement transactions. This is a polling-friendly alternative to the per-worker `transaction.*` webhook events for clients that prefer to fetch state on demand.

### Completion

Once all disbursements are confirmed, the payroll moves to `Completed`. Webhook: `payroll.status.updated` (completed)

Payslips exist only for `Completed` payrolls — [`GET /v1/payrolls/{payrollId}/payslip-links`](/api-reference/workforce/payrolls/get-payslip-links) returns entries under `notReady` until then. See [Downloading Payslips](/workforce/pay-workers-via-payroll#downloading-payslips).

After completion, the payroll response includes an `invoiceId`. Fetch the invoice for a detailed breakdown of fees charged for the payroll run.

<ApiExample method="GET" path="/v1/invoices/8dbaa174-b5a0-4396-8c87-77e746b50917" reference="/api-reference/workforce/invoices/get-invoice" />

### Status Reference

These are the values returned in the `status` field on `GET /v1/payrolls` and `GET /v1/payrolls/{id}`.

```mermaid theme={null}
flowchart TD
    A[Created] -->|POST /v1/payrolls/id/save| B[Pending Submission]
    B -->|POST /v1/payrolls/id/approve| C{Balance check}
    C -->|Sufficient| D[Scheduled]
    C -->|Insufficient| E[Awaiting Funds]
    E -->|When funded| D
    D -->|Disbursement triggered| F[Processing]
    F -->|All disbursements confirmed| G[Completed]
```

| Status               | Meaning                                                                                                                                                                                                                                                                                                              |
| :------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `Created`            | Empty payroll shell, no entries yet — returned right after [`POST /v1/payrolls`](/api-reference/workforce/payrolls/create)                                                                                                                                                                                           |
| `Pending Submission` | Entries added, ready for approval — this is the value the `status` field returns after a successful [`POST /v1/payrolls/{id}/save`](/api-reference/workforce/payrolls/save). (The corresponding `payroll.status.updated` webhook payload uses `saved`.)                                                              |
| `Pending Approval`   | Submitted for an approver to action. You will only see this when the dashboard is used to submit a payroll; the public API has no submit endpoint. The [`POST /v1/payrolls/{id}/approve`](/api-reference/workforce/payrolls/approve) endpoint accepts payrolls in either `Pending Submission` or `Pending Approval`. |
| `Awaiting Funds`     | Approved but the business balance is insufficient — payroll proceeds automatically once funded                                                                                                                                                                                                                       |
| `Scheduled`          | Funded and queued for the payroll date                                                                                                                                                                                                                                                                               |
| `Processing`         | Disbursements are being sent to workers                                                                                                                                                                                                                                                                              |
| `Completed`          | All disbursements confirmed (terminal)                                                                                                                                                                                                                                                                               |
| `Rejected`           | An approver declined the payroll (terminal)                                                                                                                                                                                                                                                                          |
| `Deleted`            | Caller deleted the payroll via [`DELETE /v1/payrolls/{id}`](/api-reference/workforce/payrolls/delete) before disbursement started (terminal)                                                                                                                                                                         |

<Note>
  `POST /v1/payrolls/{id}/save` returns `204` immediately, but the new `Pending Submission` status takes a few seconds to be reflected on `GET /v1/payrolls/{id}`. A tight poll loop right after save may still see `Created` briefly — poll with backoff until the status is `Pending Submission` before calling approve, otherwise approve will reject the payroll with `Payroll has an unexpected status Created`. `POST /v1/payrolls/{id}/approve` is strongly consistent — a follow-up GET will reflect the new status immediately.
</Note>

<Note>
  `Pending Submission` and `Awaiting Funds` are not auto-cancelled — they sit indefinitely until you explicitly approve, delete, or (for `Awaiting Funds`) fund the business balance.
</Note>

***

## Deleting a Payroll

You can delete a payroll at any point **before it starts processing**.

<ApiExample method="DELETE" path="/v1/payrolls/a1b2c3d4-e5f6-7890-abcd-ef0123456789" reference="/api-reference/workforce/payrolls/delete" />

Returns `204` on success. The payroll moves to `Deleted`.

<Warning>
  Once a payroll reaches `Processing` or `Completed`, it cannot be deleted.
</Warning>

***

## Webhook Events

Cadana emits two payroll webhook event types. Subscribe via the [Dashboard](https://app.cadanapay.com) or see [Webhooks](/reference/webhooks) for setup.

### payroll.created

Fired when a new payroll is created.

```json JSON theme={null}
{
  "id": "e13b9e14-c062-42ea-8563-8fc9223b29b5",
  "workerType": "CONTRACTOR",
  "type": "ONE_OFF",
  "tenantKey": "cad95193904"
}
```

### payroll.status.updated

Fired when the payroll moves into one of the values below.

```json JSON theme={null}
{
  "id": "e13b9e14-c062-42ea-8563-8fc9223b29b5",
  "status": "completed",
  "tenantKey": "cad95193904"
}
```

| `status` value | Meaning                                      |
| :------------- | :------------------------------------------- |
| `saved`        | Entries saved, ready for approval            |
| `approved`     | Payroll approved                             |
| `scheduled`    | Funds collected, queued for the payroll date |
| `processing`   | Disbursements being sent to workers          |
| `completed`    | All disbursements confirmed                  |

<Note>
  No webhook fires for the `Awaiting Funds`, `Rejected`, or `Deleted` states. Poll [`GET /v1/payrolls/{id}`](/api-reference/workforce/payrolls/get) if you need to detect them.
</Note>

### Per-Worker Transaction Events

Each worker's payment fires individual transaction events: `transaction.initiated`, `transaction.succeeded`, and `transaction.failed`. These let you track each worker's payout independently of the overall payroll status.

The transaction's `reference` field contains the `payrollId`. `recipientId` is the worker's identifier — use `recipientType` to interpret it: a `userId` when `recipientType: "USER"` (Cadana wallet payments), or a `personId` when `EMPLOYEE` or `CONTRACTOR` (direct bank payments).

```json JSON theme={null}
{
  "id": "9af0f05e-1efa-407b-be23-8595f89a1b2a",
  "amount": { "currency": "USD", "amount": 100000 },
  "type": "PAYROLL",
  "reference": "a1b2c3d4-e5f6-7890-abcd-ef0123456789",
  "recipientId": "8ef9a712-cdae-4110-b1ea-9ba95abbee6e",
  "recipientType": "USER",
  "tenantKey": "cad95193904"
}
```

See [Events](/reference/events) for all event types and payload details.

***

## Multi-Currency Payrolls

When a worker's salary currency differs from the business's funding currency (e.g., a US company paying a contractor in BRL), FX conversion is involved.

### How It Works

<Steps>
  <Step title="FX rates captured at save time">
    Cadana captures FX rates and calculates the debit amount in the funding currency. Each entry stores its conversion rate individually. Pass `?includeFxRates=true` on the save call to receive the captured rates in the response (`200 OK` with `{ "fxRates": { "USD-BRL": 5.41, ... } }`) instead of polling the payroll. Keys are `{fundingCurrency}-{salaryCurrency}` and each value is the number of salary-currency units per unit of funding currency.
  </Step>

  <Step title="Rates may refresh at disbursement time">
    If there's a delay between save and disbursement (future payroll date, awaiting funds, etc.), rates may be refreshed with current market rates.
  </Step>

  <Step title="Debit amount may change">
    After a rate refresh, the total debit is recalculated. If the new amount exceeds the business balance, the payroll moves back to `Awaiting Funds`.
  </Step>
</Steps>

```mermaid theme={null}
flowchart TD
    A[Created] -->|POST /v1/payrolls/id/save<br/>FX rates captured| B[Pending Submission]
    B -->|POST /v1/payrolls/id/approve| C{Balance check}
    C -->|Sufficient| D[Scheduled]
    C -.->|Insufficient| E[Awaiting Funds]
    E -.->|When funded| D

    subgraph Disbursement Time
        D --> F{FX rates<br/>expired?}
        F -->|No| G[Processing]
        F -->|Yes| H[FX Rate Refresh]
        H --> I{Balance OK?}
        I -->|Yes| G
        I -->|No| E
    end

    G -->|All disbursements confirmed| J[Completed]
```

### Recommended Approach

1. **Poll after save** — check the `debit` amount on the payroll to understand the total cost in your funding currency.
2. **Fund with headroom** — if your payroll date is days away, consider funding slightly above the quoted debit to absorb potential rate movement.
3. **Monitor `Awaiting Funds`** — if a rate refresh causes the debit to exceed your balance, the payroll pauses until you top up.

<ApiExample method="GET" path="/v1/payrolls/a1b2c3d4-e5f6-7890-abcd-ef0123456789" reference="/api-reference/workforce/payrolls/get" />

***

## Edge Cases

### Individual Worker Payment Fails

If one worker's disbursement fails (e.g., invalid bank details), the other workers' payments continue. The payroll still moves to `Completed` once all disbursements are resolved. Use `transaction.failed` webhooks to identify which worker's payment needs attention. The payroll itself does not have a `failed` status — that string only appears on per-worker `transaction.*` events.

### Multiple Payrolls Awaiting Funds

When multiple payrolls are in `Awaiting Funds` and the business adds funds, Cadana schedules them in priority order:

1. **Earliest payroll date first**
2. **Creation time** as a tiebreaker

Only payrolls fully coverable by the available balance move to `Scheduled`. The rest remain in `Awaiting Funds` until more funds are added.

### Payroll Stuck in Awaiting Funds

The payroll remains in `Awaiting Funds` indefinitely until the business account is funded. Once sufficient funds are available, the payroll proceeds automatically. See [Fund Your Account](/fund-your-account) for funding options.

### Re-saving Before Approval

You can call save multiple times before approving. Each save **replaces** the previous entries entirely — there is no incremental add/remove of individual entries.

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Pay Workers via Payroll" icon="rocket" href="/workforce/pay-workers-via-payroll">
    Step-by-step guide to create, save, and approve a payroll
  </Card>

  <Card title="Fund Your Account" icon="building-columns" href="/fund-your-account">
    Add funds via bank transfer, ACH, or crypto
  </Card>

  <Card title="Webhooks" icon="bell" href="/reference/webhooks">
    Configure your webhook endpoint
  </Card>

  <Card title="Events" icon="list" href="/reference/events">
    All event types and payload schemas
  </Card>
</CardGroup>
