Migrating list items with original authors, dates — and their version history
The Copy and Move API (CreateCopyJobs) handles files and folders only. For list items, create each one with AddValidateUpdateItemUsingPath and pass Author, Editor, Created and Modified in the same call — all four stick and the item has a single version. Older versions can be replayed: one ValidateUpdateListItem call per version with bNewDocumentUpdate: false, carrying that version's values plus its Editor and Modified. Times are kept to the minute, not the second. Always read the item back: two value formats are discarded with HTTP 200 and no error. This needs high rights: for a user who may only add items, SharePoint silently replaces the four values with the caller and the current time.
Copy and Move API (CreateCopyJobs) umí jen soubory a složky. Položky seznamu se přenášejí po jedné: AddValidateUpdateItemUsingPath s Author, Editor, Created a Modified v témže volání – všechny čtyři drží a položka má jedinou verzi. Starší verze jde přehrát: za každou verzi jedno ValidateUpdateListItem s bNewDocumentUpdate: false, hodnotami té verze a jejím Editor a Modified. Časy drží na minutu, ne na vteřinu. Položku vždy přečíst zpátky: dva tvary hodnot se zahodí s HTTP 200 a bez chyby. Chce to vysoká práva: uživateli, který smí jen přidávat, SharePoint všechny čtyři hodnoty tiše nahradí volajícím a aktuálním časem.
All results below were measured on 2026-10-27 on a custom list with versioning on (Czech locale, web time zone Pacific). The author and editor were deliberately a different account than the caller, so a match proves something. Every verdict comes from reading the item back, not from the response.
Create with all four fields — one call, one version
await fetch(`${web}/_api/web/GetList(@a)/AddValidateUpdateItemUsingPath?@a='${encodeURIComponent(listRel)}'`, {
method: 'POST',
headers: { Accept: 'application/json;odata=nometadata',
'Content-Type': 'application/json;odata=nometadata', 'X-RequestDigest': digest },
body: JSON.stringify({
listItemCreateInfo: { FolderPath: { DecodedUrl: listRel }, UnderlyingObjectType: 0 },
formValues: [
{ FieldName: 'Title', FieldValue: 'Request 42' },
{ FieldName: 'Author', FieldValue: JSON.stringify([{ Key: 'i:0#.f|membership|alice@contoso.com' }]) },
{ FieldName: 'Editor', FieldValue: JSON.stringify([{ Key: 'i:0#.f|membership|alice@contoso.com' }]) },
{ FieldName: 'Created', FieldValue: '4.3.2019 1:15' }, // web locale AND web time zone
{ FieldName: 'Modified', FieldValue: '19.7.2021 7:42' }
],
bNewDocumentUpdate: false
})
});
Read back: AuthorId, EditorId, Created, Modified all as sent, OData__UIVersionString 1.0, and the single version carries the foreign editor. The date must be in the web's locale and time zone — see Created/Modified on a document library for how to build it with utctolocaltime.
Who can set them: not a user who may only add items
The measurements above ran as a site owner. On 2026-11-12 the same call was sent by a user whose only right on the list was a custom add-only level (Add Items + View Items, item-level read and edit limited to their own items), with a foreign Author and Editor and an old Created. The response was HTTP 200 with HasException: false on every field — and the item read back with the caller as Author and Editor, Created = the time of the call, version 1.0. SharePoint silently replaced the values instead of rejecting them.
So in an add-only list the server's Author and Created do show who wrote the row and when, even if the client tries to send something else. Two limits: Contribute and Edit levels were not measured here, and anyone with Full Control can backdate a row exactly as shown above.
Creating a bare item first and then sending the four fields with ValidateUpdateListItem also works with bNewDocumentUpdate: true. With false you get a version 2.0, and version 1.0 keeps the caller and today's time in the history. Creating with all four at once avoids both.
Version history can be replayed
- Create the item with version 1's values and all four fields (
Modified=Created). - For every later version, call
items(id)/ValidateUpdateListItemwithbNewDocumentUpdate: false, that version's field values, itsEditorand itsModified.
Measured with three versions: the target shows 3.0, 2.0 and 1.0 with the right titles, the right editor on each (a different person on v2) and the right timestamps (to the minute). Created By and Created stay those of version 1. Send the timestamps in increasing order.
Attachments add a version
items(id)/AttachmentFiles/add(FileName='…') creates a new version and stamps Editor and Modified with the caller and now. Sending all four fields again with bNewDocumentUpdate: true restores the values, but the extra version stays in the history (with the restored metadata). Upload attachments before the last replayed version, or accept one extra version.
Content approval: approve in the same call
On a list with content approval, a new item is _ModerationStatus 2 (pending). Add { FieldName: '_ModerationStatus', FieldValue: '0' } to the call that carries the four fields (bNewDocumentUpdate: true). The result is approved, the four fields hold, and there is still one version.
Do not approve with a MERGE on OData__ModerationStatus. It approves, but stamps Editor/Modified with the caller. When the four fields are sent afterwards to fix that, the item goes back to pending and gains a version.
Value formats — and two that fail without a word
Each format was sent alone, because one rejected field cancels the whole update.
| field type | accepted | rejected |
|---|---|---|
| Number | 1,5 (the web's decimal separator) |
2.5 → HasException "Only numbers can go here." |
| DateTime | 6.5.2020 0:08 (web locale and zone) |
ISO → "You must specify a valid date…" |
| MultiChoice | A;#B, ;#B;#C;# |
— |
| UserMulti | [{"Key":"…"},{"Key":"…"}] |
— |
| Lookup | 1, 2;#Title |
— |
| LookupMulti | 1;#;#2;#, 2;#L2;#1;#L1 |
⚠ 1;#2 → stores only the first value, no error |
| URL | https://…, description |
— |
| Boolean | 1, No |
⚠ Yes → stores No, no error |
The two ⚠ rows look like success: HTTP 200, no HasException, a different value. Send Boolean as 1/0 and LookupMulti as id;#;#id;#. Compare the column values you read back with what you sent, not only the author and dates.
Over the 5,000 list view threshold: writing is the same, reading is not
5,100 items created with all four fields from one browser at concurrency 4 took 550 s, about 560 items per minute. There were seven 429 responses: after roughly 2,800 items SharePoint throttled for about two minutes (Retry-After), then continued at the same pace. No item failed. Creating items and replaying history past item 5,000 behaved exactly as on a small list.
Reading does change:
| query on 5,100 items | result |
|---|---|
$filter on a non-indexed column (Modified, Title) |
500 SPQueryThrottledException |
$filter=Id gt 4900 and Modified ge … |
500 – an indexed first clause does not save it |
$orderby=Id asc&$top=5000 + nextLink |
200, pages fine |
no $filter, $top=5000 |
200 with nextLink |
Page by Id and filter in code. An incremental "since last run" mark works as Id gt N, but not as a Modified filter.
Count what you wrote from your own run log, or with $orderby=Id desc&$top=1. If you read ItemCount during the run, fetch it with cache: 'no-store': repeating the same GET URL in the browser kept returning an early value (87 when more than 600 items existed). That was most likely the browser cache, not SharePoint.
Rule
One item = one AddValidateUpdateItemUsingPath with the four fields (plus _ModerationStatus if needed). Replay older versions with bNewDocumentUpdate: false. Then read everything back. Expect minute precision and an extra version for each attachment step.