How to Bulk-Restore Deleted Items by Date Range
This guide explains the algorithm behind bulk-restoring deleted items by date range. It walks through every stage in execution order and shows the exact API calls at each step.
Overview
The algorithm consists of the following stages:
- Search — find every deleted item in the date window using the bsearch API.
- Coalesce — group items by the snapshot timestamp they were last seen in.
- Snapshot lookup — resolve each timestamp group to a concrete snapshot ID.
- Submit — POST one restore job per group.
- Verify (optional) — confirm each job was accepted and track its progress.
All requests require HTTP Basic authentication and the standard API headers. See the Authentication and API Headers pages for details.
Stage 1 — Search for Deleted Items
See the Search Backup page for details.
Use the bsearch endpoint to find all deleted items within a date window. The
window is matched against each item's snapshot timestamp — the <updated> value
in the results (Stage 2): an item is returned when the snapshot in which it was
last seen falls between snaptimeFrom and snaptimeTo.
| Method | GET |
| Endpoint | https://{baseUrl}/users/{accountId}/bsearch |
| Body | none |
Set the following query parameters:
| Parameter | Value |
|---|---|
apiVersion | 2 — must be 2; deleted items are not returned under the default version |
device | {connectorId} |
pathRoot | {pathRoot} — see pathRoot by item type |
filterAnd | AND:deleted; — returns only deleted items |
snaptimeFrom | {iso8601} — start of the date window (UTC ISO 8601) |
snaptimeTo | {iso8601} — end of the date window (UTC ISO 8601) |
recursive | 1 to search all subfolders under pathRoot; optional, defaults to 0 |
Optionally, you can paginate through the results: request one page of count
items at a time, increasing startIndex by count for each next page, until a
page returns fewer than count items. For example, with count=100, request
startIndex=0, then 100, then 200, and so on. Combine the <entry> elements
from every page into a single result set for Stage 2.
pathRoot by item type
The pathRoot value depends on the item type being restored.
Email:
Code
Where userIdentity is the user's UPN or Keepit user ID, and folderName is the
mailbox folder to search (e.g. Inbox, Sent, or Deleted Items).
Note: Paths are case-sensitive and must be localized — if the mailbox
language isn't English, use the localized folder name (Inbox is not converted
automatically).
User objects:
Code
Search at the top-level Users folder. After retrieving results, filter
client-side: keep only items whose title field contains the target UPN. This
handles the case where a user was deleted and recreated with a new user ID.
OneDrive:
Code
If the search returns zero results, retry with the alternate OneDrive path:
- If
pathFromRootstarts withOneDrive:/Users/{userIdentity}/OneDriveSP/DocLibs/Documents/Content - Otherwise:
/Users/{userIdentity}/OneDrive
SharePoint:
Code
Omit /{encodedSiteUrl} to search across all SharePoint sites. To target one
site, set encodedSiteUrl to the escape-masked site URL. For example,
https://m365x47291058.sharepoint.com/sites/allcompany becomes
/SharePoint/https-c-s-sm365x47291058.sharepoint.com-ssites-sallcompany.
Groups & Teams:
Code
Omit /{groupId} to search across all groups. To target one group, set groupId to the escape-masked group GUID. For example, /Groups/f3a1b2c4--d7e8--4f91--a023--b6c5d4e3f2a1.
Path segment encoding
Special characters inside individual path segments must be escaped before
building pathRoot. See Escape Masking for the full rules and
examples.
curl
Code
Example
Searching for deleted items in a user's Outlook Inbox between two dates:
Code
Response
Code: 200 OK
The API returns XML. Each <entry> represents one item and carries a
<deleted/> element:
Code
[!TIP]
<updated>is a UTC ISO 8601 timestamp identifying the snapshot in which the item was last seen — you will need this value in Stage 2.<opensearch:totalResults>gives the total match count across all pages.
Stage 2 — Coalesce by Snapshot Timestamp
Take the items collected in Stage 1 and group them by their <updated> value.
Each distinct <updated> timestamp corresponds to a different backup snapshot,
and all items sharing that timestamp can be restored together in a single job —
far more efficient than one job per item.
The grouping, in illustrative Python (deletedItems is the Stage 1 result set):
Code
The result is a map of snapshot timestamp → list of items, where each item
still carries its full <id> from Stage 1. The timestamp key drives the
snapshot lookup in Stage 3; each item's <id> becomes a <Path> in Stage 4.
For example, items last seen across two snapshots collapse into two groups:
Code
Carry each group into Stage 3 independently — one snapshot lookup per group.
Stage 3 — Resolve Snapshot IDs
See the [Retrieve Snapshot History Range] page for details.
For each timestamp group you need the snapshot ID that corresponds to that
point in time. Query the snapshot history range endpoint backwards from the
group's <updated> timestamp (UTC ISO 8601).
| Method | PUT |
| Endpoint | https://{baseUrl}/users/{accountId}/devices/{connectorId}/history/range |
| Body | XML <range> |
Send the following <range> body:
| Body field | Value |
|---|---|
start | {updated} — the group's <updated> value (UTC ISO 8601) |
span | P30D — look back up to 30 days from start |
count | 1 — only the single nearest snapshot is needed |
reverse | (empty element) — walk backwards in time from start |
curl
Code
Example
Resolving the snapshot nearest to a group's <updated> timestamp:
Code
Code
Response
Code: 200 OK
Code
Extract the <root> of the first <backup> element — a 64-character hash. That
is the snapshot ID to use when building the restore job for this timestamp group.
Repeat this call once per distinct timestamp group. Each group will yield its own snapshot ID, which is used independently in Stage 4.
Stage 4 — Build and Submit Restore Jobs
See the Create Device Job page for details.
For each timestamp group, construct the XML job definition and POST it to the API.
| Method | POST |
| Endpoint | https://{baseUrl}/users/{accountId}/devices/{connectorId}/jobs/ |
| Body | XML <job> |
Build the following <job> body:
| Body field | Value |
|---|---|
description | free-text label, e.g. Bulk Restore ({type}) - {N} items ({type} = item type: Email, OneDrive, User object, …) |
type | srestore |
immediate | (empty element) — run the job immediately |
priority | 1 |
SnapshotId | {snapshotId} — snapshot ID from Stage 3 for this group |
FolderRestoreMode | DeltaAppend |
FileConflictResolutionMode | Restore — overwrite existing items on conflict |
Method | InPlace — restore to the original location |
Path | {xmlEscapedPath} — one element per item; the <id> with kng://{connectorId} stripped and XML-escaped |
XML job definition
Code
Path values inside <Path> elements:
Strip the kng://{connectorId} prefix from item.Id to get the bare path, then
XML-escape the result as defined by the XML specification (encode &, <, >,
", and ' as their XML entities).
curl
Code
Example
Submitting a restore job for one timestamp group:
Code
Code
Response
Code: 201 Created — no body.
The job object is not returned by this endpoint. To confirm the job was accepted and track its progress, use Stage 5.
Stage 5 — Verify the Restore Jobs (optional)
See the List Device Jobs page for details.
To confirm the jobs were accepted and track their progress, query the jobs
endpoint and match on the description you set in Stage 4.
| Method | GET |
| Endpoint | https://{baseUrl}/users/{accountId}/devices/{connectorId}/jobs |
| Body | none |
curl
Code
Example
Code
Response
Code: 200 OK
Code
<progress>— a float between0.0and1.0.<succeeded>/<failed>— on completion, the job carries one of these timestamps:<succeeded>if it succeeded,<failed>if it failed.<active>—trueonly while the job is still running.