GuidesAPI ReferenceChangelogAPI StatusAPI PolicyGusto Security
Guides

Batch Payroll Cancellation API

Cancel multiple payrolls for companies

The Batch Payroll Cancellation API provides partners with a single API call to cancel multiple payrolls across one or more client companies. The most common use case: cancelling payrolls that couldn't be funded, such as when a bank debit was rejected

The API is asynchronous: a single POST /v1/payroll_batches kicks off a server-side computation, and a GET /v1/payroll_batches/{payroll_batch_uuid} returns the results once they're ready.

The pattern mirrors the People Batch API​, so partners already familiar with that API can integrate quickly.

Key features

FeatureDescription
Multi-company operationsCancel up to 100 payrolls for companies in a single batch.
Asynchronous processingSubmission and fulfillment are decoupled, so requests do not time out regardless of how much work is processed.
Standardized async patternPOST /v1/payroll_batches to submit, GET /v1/payroll_batches/{payroll_batch_uuid} to poll, with the same status lifecycle, idempotency contract, and result envelope as our other async batch APIs.
Success and failurePayrolls for companies that are accessible successfully appear in results; companies or payrolls that could not be processed appear in exclusions with a category and message. A single bad UUID never fails the whole batch. Duplicate UUIDs will be listed in exclusions. This API does not support partial_success.
System-level authenticationUses your partner application's system access token, the same token used for provisioning and bulk onboarding status.
Idempotent submissionDuplicate POSTs with the same idempotency_key return the original batch without triggering a second computation.

Step 1: Submit the request

  1. Provide an idempotency_key (a UUID) for each new request. The key is unique per idempotency_key, partner_id. If the same key arrives a second time, Gusto returns a 409 Conflict referencing the original batch UUID inside the error metadata—it will not recompute.
  2. Call POST /v1/payroll_batches with batch_action: "cancel", authenticated with your system access token and the payroll_batches:write OAuth scope. Max 100 payrolls per request.
POST /v1/payroll_batches
Authorization: Bearer {system_access_token}
X-Gusto-API-Version: 2026-06-15
Content-Type: application/json

{
  "idempotency_key": "<partner-provided uuid>",
  "batch_action": "cancel",
  "batch": [
    {
      "entity_type": "payroll",
      "uuid": "a1c4e6f8-3d2b-4a9c-8e1f-5b7d0c2a4e69",
      "company_uuid": "9b2d4f6a-8c1e-4d3b-a5f7-1e9c0b3d5a82"
    },
    {
      "entity_type": "payroll",
      "uuid": "c7f2b9d4-6e3a-4b1c-9d8f-0a2e5c7b1f43",
      "company_uuid": "9b2d4f6a-8c1e-4d3b-a5f7-1e9c0b3d5a82"
    },
    {
      "entity_type": "payroll",
      "uuid": "b6f1c3e9-4a2d-4e8b-9c7f-1a5d0b2e6c34",
      "company_uuid": "3e8a1d5c-9f2b-4a7e-b1c4-6d0f2a9e3b58"
    },
    {
      "entity_type": "payroll",
      "uuid": "d2a7e5f1-8c3b-4d6a-a9e2-7f0c1b4d8a63",
      "company_uuid": "5c9e2a7d-1f4b-4e6c-9a3d-8b0f2c5e1d47"
    }
  ]
}

Each entry in batch references an existing payroll for an existing company—both by their Gusto uuid. Companies must be mapped to your partner application—Gusto enforces this server-side, and unmapped companies are returned in exclusions.

Payrolls that exist and are owned by a mapped company but are not in a cancellable state are returned in results[] with status: "failed" and a category such as not_cancellable; exclusions[] is reserved for submission-time issues (unknown UUID, unmapped company, duplicate UUID in the request).

📘

What's checked at submission vs during processing

When you submit the batch, Gusto only rejects two things: payrolls it can't find (unknown UUID or a company that isn't mapped to your partner) and duplicate UUIDs within the same request. These land in exclusions[].

Whether a payroll can actually be cancelled is checked later, during async processing. Those outcomes show up in results[], never in exclusions[].

Step 2: Receive the batch acknowledgement

Gusto validates authorization and the request body, creates a batch record, and enqueues the computation.

Response codes:

  • 201 / Created Batch accepted, processing enqueued. Use the returned uuid for all subsequent polling.
  • 409 / Conflict Duplicate idempotency_key. Response body is an error envelope; the existing batch UUID is at errors[0].metadata.request_uuid. No duplicate work is enqueued.
  • 422 / Unprocessable Entity Request body validation error (e.g. malformed JSON, missing required fields, > 100 items, batch bad_action).

A 201 response looks like the following:

{
  "uuid": "f0b3d8a6-5e1c-4f2d-8a9b-4c6e2d0f7a15",
  "idempotency_key": "<partner-provided uuid>",
  "batch_action": "cancel",
  "status": "pending"
}

Step 3: Poll for results

Poll GET /v1/payroll_batches/{payroll_batch_uuid} with the uuid returned on submission until the batch reaches a terminal status (completed or failed). This requires the payroll_batches:read OAuth scope. While you poll, Gusto looks up payrolls for all requested companies in the background.

  GET /v1/payroll_batches/{payroll_batch_uuid}
  Authorization: Bearer {system_access_token}
  X-Gusto-API-Version: 2026-06-15
  Content-Type: application/json

Response codes:

  • 200 / OK Batch found. Batch states: pending, processing, completed, failed
  • 404 / Not Found Unknown batch uuid
  • 410 / Gone The batch was processed, but its results have expired. Batch results are held in Redis for 15 days after the batch completes; submit a new batch to re-run the operation.

The batch goes through this lifecycle:

pendingprocessingcompleted | failed

🚧

Completed does not mean everything succeeded

The batch status reports the lifecycle of the work, not the per-company outcome. A completed batch may include companies in exclusions.

Always inspect the arrays to determine what actually came back.

Response while processing:

{
  "uuid": "f0b3d8a6-5e1c-4f2d-8a9b-4c6e2d0f7a15",
  "status": "processing",
  "submitted_at": "2026-04-01T14:30:00Z",
  "submitted_items": 4,
  "processed_items": 0,
  "excluded_items": 0,
  "results": [],
  "exclusions": []
}

Step 4: Read the results

Once the batch is in a terminal state, the GET /v1/payroll_batches/{payroll_batch_uuid} response contains the full results inline. Each entry in results[] and exclusions[] carries an idx that maps back to the position of the payroll in your original batch array, so you can correlate outcomes to your input.

Per-item status in results[] is either success or failed—a cancel is atomic, so partial_success never appears.

The example below is an annotated shape, not a literal response payload: the pipe-separated status value shows the possible enum values (the actual response contains one of those four).

{
  "uuid": "f0b3d8a6-5e1c-4f2d-8a9b-4c6e2d0f7a15",
  "idempotency_key": "...",
  "batch_action": "cancel",
  "status": "pending | processing | completed | failed",
  "submitted_at": "2026-04-01T14:30:00Z",
  "completed_at": null,
  "submitted_items": 4,
  "processed_items": 3,
  "excluded_items": 1,
  "results": [
    {
      "idx": 0,
      "uuid": "a1c4e6f8-3d2b-4a9c-8e1f-5b7d0c2a4e69",
      "status": "success"
    },
    {
      "idx": 1,
      "uuid": "c7f2b9d4-6e3a-4b1c-9d8f-0a2e5c7b1f43",
      "status": "failed",
      "errors": [ { "error_key": "base", "category": "not_cancellable", "message": "..." } ]
    },
    {
      "idx": 3,
      "uuid": "d2a7e5f1-8c3b-4d6a-a9e2-7f0c1b4d8a63",
      "status": "success"
    }
  ],
  "exclusions": [
    {
      "idx": 2,
      "entity_type": "payroll",
      "uuid": "b6f1c3e9-4a2d-4e8b-9c7f-1a5d0b2e6c34",
      "company_uuid": "3e8a1d5c-9f2b-4a7e-b1c4-6d0f2a9e3b58",
      "status": "failed",
      "category": "not_found",
      "message": "..."
    }
  ]
}

Status vocabularies

Two distinct status vocabularies appear in the response. Don't conflate them.

Batch status (top-level)

Reports the lifecycle of the batch request itself.

  • pending—Request accepted, processing has not started yet.
  • processing—Data is being fetched for one or more companies.
  • completed—Processing finished. Inspect results and exclusions for outcomes.
  • failed—Request failed (e.g., internal error). Can be retried.

Per-payroll status (inside results[] and exclusions[])

Reports the outcome for an individual payroll.

  • success—The payroll was cancelled.
  • partial_success—Does not apply for this API.
  • failed—The payroll could not be cancelled. Appears in exclusions.

FAQ

How do I correlate results back to the payrolls I requested?

Each entry in both results and exclusions includes idx, the zero-based position of the payroll in your original batch array, and uuid, the payroll UUID you submitted. exclusions[] entries additionally include company_uuid. Every UUID you submit appears in exactly one of the two arrays.

What OAuth scopes are required?

You need the payroll_batches:read and payroll_batches:write scopes on your system access token. This scope is gated per partner application; reach out to your Gusto Embedded contact to enable it.

How do I handle if I need to cancel more than 100 payrolls?

Submit multiple non-overlapping batches—for example, two batches of 100 payrolls. Each batch is processed independently and has its own uuid. Results can be merged on your side.

Should I poll or use a webhook?

Currently polling is the only supported delivery mechanism. Poll GET /v1/payroll_batches/{payroll_batch_uuid} every few seconds until the batch reaches completed or failed.

Can the same idempotency_key be reused?

No. Each idempotency_key is unique per partner application. Reusing a key returns the original batch—it will not trigger a new computation, even if the provided list has changed. Provide a fresh UUID for every new request.

What are exclusions?

Exclusions lists items (payrolls) that you wanted to cancel, but either the company uuid that you provided is not mapped to your partner app, or the payroll might be in a state where it can not be cancelled. If Gusto encounters such errors while processing your request, that payroll is returned in exclusions with a descriptive category and message. The rest of the batch completes normally.


Did this page help you?