---
title: "Save draft edits to validation-failed roster rows"
method: PATCH
path: "/roster-records/{rosterId}/draft"
tags: ["RosterRecord"]
---

# Save draft edits to validation-failed roster rows

`PATCH /roster-records/{rosterId}/draft`

Applies corrections to a batch of roster rows and stores them as an in-progress draft.

**When to use:** call this after reviewing validation errors, to record proposed fixes before asking for the roster to be checked again; follow it with `POST /roster-records/{rosterId}/revalidate` when the corrections are ready to be re-run.

**Preconditions:** supply the `tenant-id` header and the `rosterId` of an existing roster job. Only rows currently in `VALIDATION_FAILED` state can be drafted; every entry in `rows` must set `recordKey` to the `rowId` of a row in that roster, obtained from the roster row listing endpoints. Each entry's `rowData` is merged into the stored row one level deep - top-level keys you supply replace their stored counterparts, keys you omit are left untouched, and nested objects are replaced wholesale rather than merged. The batch is limited to 100 entries by default; a larger batch is rejected with 400 and should be split.

**Partial success:** entries are processed individually, so one bad entry does not discard the rest. An entry is reported as a failure - and the others still saved - when its `recordKey` matches no row in the roster, when its `rowData` is not a JSON object, or when the target row is not in `VALIDATION_FAILED` state. Always read `failures` in the response rather than relying on the HTTP status alone. **What you get back:** `succeeded`, the number of rows saved, and `failures`, one entry per rejected row with the `recordKey` and the reason.

**Side effects:** each saved row is marked as drafted and its payload is appended to that row's draft history, so this operation is not idempotent - submitting identical edits twice appends two history entries. Row status is left at `VALIDATION_FAILED`; rows are not revalidated until you call the revalidate endpoint.

## Path parameters

- `rosterId` string, required

## Headers

- `tenant-id` string, required

## Request body

- RosterRowsDraftBulkRequest — Bulk draft save request
  - `rows` RosterRowDraftEntry[], required — Rows with edited data to persist
    - `recordKey` string, required — Stable row id from roster row list APIs (Spanner row_id)
    - `rowData` JsonNode, required
      - `empty` boolean
      - `valueNode` boolean
      - `containerNode` boolean
      - `missingNode` boolean
      - `array` boolean
      - `object` boolean
      - `nodeType` 'ARRAY' | 'BINARY' | 'BOOLEAN' | 'MISSING' | 'NULL' | 'NUMBER' | 'OBJECT' | 'POJO' | 'STRING'
      - `pojo` boolean
      - `number` boolean
      - `integralNumber` boolean
      - `floatingPointNumber` boolean
      - `short` boolean
      - `int` boolean
      - `long` boolean
      - `float` boolean
      - `double` boolean
      - `bigDecimal` boolean
      - `bigInteger` boolean
      - `textual` boolean
      - `boolean` boolean
      - `null` boolean
      - `binary` boolean

## Response `200`

The outcome of the batch: how many rows were saved, and one entry per row that was rejected. A non-empty `failures` list can accompany a successful response.

- RosterRowsDraftBulkResponse — Result of a bulk draft save
  - `succeeded` integer — Number of rows whose edits were successfully saved
  - `failures` DraftRowSaveFailure[] — Per-row failures (other rows may still have succeeded)
    - `recordKey` string — recordKey / row_id that failed
    - `message` string — Reason the row was not updated

## Other responses

- `400` — Returned when the batch exceeds the maximum number of entries, or the request body fails validation - for example an empty `rows` list or an entry with a blank `recordKey`. Split the batch or correct the payload and retry.
- `401` — Returned when the request carries no valid authentication.
- `403` — Returned when the caller lacks the UPDATE_ROSTER permission for this tenant.
- `404` — Returned when no roster job with the supplied `rosterId` exists for the tenant.

---

[API](https://skmtc.net/certifyos/apis/certify-api-layer.md) · [All operations](https://skmtc.net/certifyos/apis/certify-api-layer/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/certifyos/certify-api-layer/revisions/563848e0ecc0/schema)
