Guides

Use-case recipes

Step-by-step recipes for common collection and discovery tasks. Every sample is a self-contained, runnable Python script (only the requests library is required) — copy any one script and run it directly. For cross-cutting rules (pagination, dates, PascalCase responses) see the API conventions.


1 · List the seeded locations

Retrieve every source and destination location pre-configured in your trial tenant. Use the returned Id values as inputs to discovery calls and job policies.

import requests

BASE      = "https://api.dev.datatap.stream"
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

locations = get(f"/api/{TENANT_ID}/locations")
for loc in locations:
    print(loc["Id"], loc["Type"], loc.get("Name", "—"))
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"

curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/locations"

2 · List the seeded jobs

See all jobs provisioned by the trial setup. Each entry includes the job Id, Type, and current Enabled state — the Id is what you need to activate a job in recipe 5.

import requests

BASE      = "https://api.dev.datatap.stream"
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

jobs = get(f"/api/{TENANT_ID}/jobs")
for job in jobs:
    print(job["Id"], job["Name"], job["Type"], "enabled:", job.get("Enabled"))
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"

curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/jobs"

3 · List users from the email source

A single direct call against the seeded Exchange location returns the first page of available mailboxes — enough to inspect what the location sees before scoping a job. For a full paginated enumeration of every mailbox see recipe 6.

import requests

BASE      = "https://api.dev.datatap.stream"
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

locs = get(f"/api/{TENANT_ID}/locations")
exchange_loc = next(l["Id"] for l in locs if l["Type"] == "EXCHANGE")

# One-shot first page — enough to inspect what mailboxes the seeded location sees.
# For a full paginated enumeration of every mailbox see the "Discover all the mailboxes" recipe.
page = get(f"/api/{TENANT_ID}/discovery/{exchange_loc}/users",
           params={"PageSize": 100, "filter.type": "USERS_AND_SHAREDMAILBOXES"})
for user in page["Sources"]:
    print(user["Email"], "—", user["Name"], f"({user['Type']})")
print("continuation:", page["Options"].get("Marker", "none"))
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"
EXCHANGE_LOC="<exchange-location-id>"   # Id from the list-locations call above

curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/discovery/$EXCHANGE_LOC/users?PageSize=100&filter.type=USERS_AND_SHAREDMAILBOXES"

4 · List users from the OneDrive source

Same pattern against the seeded OneDrive location — returns drive owners whose drives can be targeted in a onedrive-backup policy. For the full paginated version see recipe 7.

import requests

BASE      = "https://api.dev.datatap.stream"
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

locs = get(f"/api/{TENANT_ID}/locations")
onedrive_loc = next(l["Id"] for l in locs if l["Type"] == "ONEDRIVE")

# One-shot first page of drive owners on the seeded OneDrive location.
# For full paginated enumeration see the "Discover all the OneDrive accounts" recipe.
page = get(f"/api/{TENANT_ID}/discovery/{onedrive_loc}/users",
           params={"PageSize": 100})
for user in page["Sources"]:
    print(user["Email"], "—", user["Name"])
print("continuation:", page["Options"].get("Marker", "none"))
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"
ONEDRIVE_LOC="<onedrive-location-id>"  # Id from the list-locations call above

curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/discovery/$ONEDRIVE_LOC/users?PageSize=100"

5 · Activate the seeded collection jobs

Seeded jobs are created with enabled: true and schedule.type: DISABLED — wired up but idle. Activating one is two steps: first PATCH the job to scope its policy to your target accounts (leave the schedule unchanged), then call POST /api/{tenant}/jobs/{jobId}/start to trigger an immediate run. The job must be in READY or FAILED state. Two scenarios are shown — the Enron public corpus sample and a placeholder organization policy.

import requests

BASE      = "https://api.dev.datatap.stream"
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

def post(path, json=None):
    r = requests.post(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, json=json, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

def patch(path, json=None):
    r = requests.patch(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, json=json, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

# Seeded jobs are created with enabled=true and schedule.type=DISABLED — ready but idle.
# Step 1: PATCH the job to scope its policy to the target users (leave schedule unchanged).
# Step 2: call /start to trigger an immediate run (STARTING → RUNNING → READY).
# The job must be in READY or FAILED state before it can be patched or started.
jobs = get(f"/api/{TENANT_ID}/jobs")

# ── Enron sample job ──────────────────────────────────────────────────────────────────────────────
enron_job = next(j for j in jobs if "enron" in j["Name"].lower())

patch(f"/api/{TENANT_ID}/jobs/{enron_job['Id']}", json={
    "id": enron_job["Id"],
    "policy": {
        "$type": "exchange-backup",
        "users": [
            {"item": {"id": "kenneth.lay",   "name": "Kenneth Lay",   "email": "kenneth.lay@enron.com",   "type": "User"}, "type": "INCLUDE"},
            {"item": {"id": "jeff.skilling", "name": "Jeff Skilling", "email": "jeff.skilling@enron.com", "type": "User"}, "type": "INCLUDE"}
        ]
    }
})
task = post(f"/api/{TENANT_ID}/jobs/{enron_job['Id']}/start")
print("Enron job started, taskId:", task["TaskId"])

# ── Organization job ──────────────────────────────────────────────────────────────────────────────
# Replace id/email with real values from the list-users-from-email-source recipe above.
org_job = next(j for j in jobs if "organization" in j["Name"].lower())

patch(f"/api/{TENANT_ID}/jobs/{org_job['Id']}", json={
    "id": org_job["Id"],
    "policy": {
        "$type": "exchange-backup",
        "users": [
            {"item": {"id": "user-1-id", "name": "Alice", "email": "alice@yourorg.com", "type": "User"}, "type": "INCLUDE"},
            {"item": {"id": "user-2-id", "name": "Bob",   "email": "bob@yourorg.com",   "type": "User"}, "type": "INCLUDE"}
        ]
    }
})
task = post(f"/api/{TENANT_ID}/jobs/{org_job['Id']}/start")
print("Org job started, taskId:", task["TaskId"])
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"
JOB_ID="<job-id>"   # Id from the list-jobs call above

# Step 1 — PATCH: scope the policy to your target users (schedule stays DISABLED):
curl -s -X PATCH \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "id": "'"$JOB_ID"'",
    "policy": {
      "$type": "exchange-backup",
      "users": [
        {"item": {"id": "kenneth.lay",   "name": "Kenneth Lay",   "email": "kenneth.lay@enron.com",   "type": "User"}, "type": "INCLUDE"},
        {"item": {"id": "jeff.skilling", "name": "Jeff Skilling", "email": "jeff.skilling@enron.com", "type": "User"}, "type": "INCLUDE"}
      ]
    }
  }' \
  "https://api.dev.datatap.stream/api/$TENANT_ID/jobs/$JOB_ID"

# Step 2 — START: trigger an immediate run:
curl -s -X POST \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/jobs/$JOB_ID/start"

6 · Discover all the mailboxes

List every user and shared mailbox in your Exchange source, following the Options.Marker continuation token until it is empty.

import requests

BASE      = "https://api.dev.datatap.stream"  # your environment's API base URL
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"                       # sent as the X-API-Key header

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

locs = get(f"/api/{TENANT_ID}/locations")
EXCHANGE_LOC = next(l["Id"] for l in locs if l["Type"] == "EXCHANGE")

# Discovery's /users endpoint on a SOURCE location reads live Microsoft Graph. filter.type=
# USERS_AND_SHAREDMAILBOXES adds shared/resource mailboxes; omit it for user mailboxes only.
def discover_sources(location_id, include_shared=True):
    sources, marker = [], None
    while True:
        params = {"PageSize": 100}
        if include_shared:
            params["filter.type"] = "USERS_AND_SHAREDMAILBOXES"
        if marker:
            params["Marker"] = marker
        page = get(f"/api/{TENANT_ID}/discovery/{location_id}/users", params=params)
        sources.extend(page["Sources"])               # each: {Id, Name, Email, Type}
        marker = page["Options"].get("Marker")
        if not marker:
            break
    return sources

mailboxes = discover_sources(EXCHANGE_LOC)
print(f"{len(mailboxes)} mailboxes")
for m in mailboxes:
    print(m["Email"], "—", m["Name"], f"({m['Type']})")
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"
EXCHANGE_LOC="<exchange-location-id>"   # Id from GET /api/$TENANT_ID/locations

# First page (PageSize=100). Follow Options.Marker for subsequent pages.
curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/discovery/$EXCHANGE_LOC/users?PageSize=100&filter.type=USERS_AND_SHAREDMAILBOXES"

7 · Discover all the OneDrive accounts

The same paginated discovery call against the OneDrive source location returns every drive owner.

import requests

BASE      = "https://api.dev.datatap.stream"
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

locs = get(f"/api/{TENANT_ID}/locations")
ONEDRIVE_LOC = next(l["Id"] for l in locs if l["Type"] == "ONEDRIVE")

# Same paginated /users call against the OneDrive SOURCE location: returns drive owners.
# Shared mailboxes don't apply to OneDrive, so no filter.type needed.
def discover_sources(location_id):
    sources, marker = [], None
    while True:
        params = {"PageSize": 100}
        if marker:
            params["Marker"] = marker
        page = get(f"/api/{TENANT_ID}/discovery/{location_id}/users", params=params)
        sources.extend(page["Sources"])
        marker = page["Options"].get("Marker")
        if not marker:
            break
    return sources

accounts = discover_sources(ONEDRIVE_LOC)
print(f"{len(accounts)} OneDrive accounts")
for a in accounts:
    print(a["Email"], "—", a["Name"])
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"
ONEDRIVE_LOC="<onedrive-location-id>"  # Id from GET /api/$TENANT_ID/locations

# First page. Follow Options.Marker for subsequent pages.
curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/discovery/$ONEDRIVE_LOC/users?PageSize=100"

8 · Collect emails & notes from two user accounts

Create a BACKUP job whose policy includes only the two chosen mailboxes. An Exchange collection collects their mail and notes; scope further with folder includes (IndexFromPaths) if you need only specific folders. Indexing for search is a premium feature, so the job runs with indexFiles: false.

import requests, time

BASE      = "https://api.dev.datatap.stream"
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

def post(path, json=None):
    r = requests.post(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, json=json, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

locs = get(f"/api/{TENANT_ID}/locations")
EXCHANGE_LOC = next(l["Id"] for l in locs if l["Type"] == "EXCHANGE")
BLOB_LOC     = next(l["Id"] for l in locs if l["Type"] == "BLOB")

# Scope a collection to two specific mailboxes via the policy's Users include-list. An Exchange
# collection collects each user's mail (all folders) and notes. (It also captures calendar items; to
# restrict to specific folders, add IndexFromPaths includes.)
def discover_sources(location_id, include_shared=True):
    sources, marker = [], None
    while True:
        params = {"PageSize": 100}
        if include_shared:
            params["filter.type"] = "USERS_AND_SHAREDMAILBOXES"
        if marker:
            params["Marker"] = marker
        page = get(f"/api/{TENANT_ID}/discovery/{location_id}/users", params=params)
        sources.extend(page["Sources"])
        marker = page["Options"].get("Marker")
        if not marker:
            break
    return sources

mboxes = {m["Email"].lower(): m for m in discover_sources(EXCHANGE_LOC)}
picked = [mboxes["alice@contoso.com"], mboxes["bob@contoso.com"]]

job = post(f"/api/{TENANT_ID}/jobs", json={
    "name": "Emails & notes — Alice + Bob",
    "description": "Collection of two mailboxes",
    "priority": "Medium",
    "type": "BACKUP",
    "schedule": {"type": "NOW"},
    "sourceId": EXCHANGE_LOC,
    "destinationId": BLOB_LOC,
    "indexFiles": False,                               # indexing is a premium (paid) feature
    "owner": "me@contoso.com",
    "policy": {
        "$type": "exchange-backup",                   # polymorphic discriminator
        "users": [                                     # INCLUDE only these two mailboxes
            {"item": {"id": u["Id"], "name": u["Name"], "email": u["Email"], "type": u["Type"]},
             "type": "INCLUDE"}
            for u in picked
        ]
    }
})
JOB_ID = job["Id"]

task = post(f"/api/{TENANT_ID}/jobs/{JOB_ID}/start")
TASK_ID = task["TaskId"]
while True:                                            # STARTING -> RUNNING -> READY | FAILED
    st = get(f"/api/{TENANT_ID}/jobs/{JOB_ID}/status/{TASK_ID}")
    print("state:", st["State"])
    if st["State"] in ("READY", "FAILED"):
        break
    time.sleep(10)
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"
EXCHANGE_LOC="<exchange-location-id>"
BLOB_LOC="<blob-location-id>"

# 1 · Create the BACKUP job scoped to two mailboxes:
JOB=$(curl -s -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Emails & notes — Alice + Bob",
    "priority": "Medium",
    "type": "BACKUP",
    "schedule": {"type": "NOW"},
    "sourceId": "'"$EXCHANGE_LOC"'",
    "destinationId": "'"$BLOB_LOC"'",
    "indexFiles": false,
    "owner": "me@contoso.com",
    "policy": {
      "$type": "exchange-backup",
      "users": [
        {"item": {"id": "<alice-id>", "name": "Alice", "email": "alice@contoso.com", "type": "User"}, "type": "INCLUDE"},
        {"item": {"id": "<bob-id>",   "name": "Bob",   "email": "bob@contoso.com",   "type": "User"}, "type": "INCLUDE"}
      ]
    }
  }' \
  "https://api.dev.datatap.stream/api/$TENANT_ID/jobs")
JOB_ID=$(echo $JOB | python3 -c "import sys,json; print(json.load(sys.stdin)['Id'])")

# 2 · Start it and poll status:
TASK=$(curl -s -X POST \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/jobs/$JOB_ID/start")
TASK_ID=$(echo $TASK | python3 -c "import sys,json; print(json.load(sys.stdin)['TaskId'])")

curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/jobs/$JOB_ID/status/$TASK_ID"

9 · List collected Sent Items for a date range

Browse the collected mailbox on the BLOB location: find its Sent Items folder, then page its messages between two timestamps. The mailbox must have been collected first (recipe 8). Each result carries the ItemId + FragmentId you need to download a single message as .eml.

import requests

BASE      = "https://api.dev.datatap.stream"
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

locs = get(f"/api/{TENANT_ID}/locations")
BLOB_LOC = next(l["Id"] for l in locs if l["Type"] == "BLOB")

# "Collected" data is browsed on the BLOB collection location (not the live source). First list the
# mailbox's folders to find Sent Items, then list its MessageItems within the date range.
SOURCE = "alice@contoso.com"                           # the collected mailbox (its owner id/email)

folders = get(f"/api/{TENANT_ID}/discovery/{BLOB_LOC}/source/{SOURCE}/items/-",
              params={"types": "MailboxFolder", "PageSize": 100})["Items"]
sent = next(f for f in folders
            if f.get("Discriminator") == "SentMailboxFolder" or f["Name"] == "Sent Items")

# Listing MessageItems requires a full-timestamp date range. Each item carries an ItemId + FragmentId
# (use them with the /download endpoint to pull a single message as .eml).
def list_messages(folder_id, from_date, to_date):
    items, marker = [], None
    while True:
        params = {"types": "MessageItem", "fromDate": from_date, "toDate": to_date, "PageSize": 100}
        if marker:
            params["Marker"] = marker
        page = get(f"/api/{TENANT_ID}/discovery/{BLOB_LOC}/source/{SOURCE}/items/{folder_id}",
                   params=params)
        items.extend(page["Items"])
        marker = page["Options"].get("Marker")
        if not marker:
            break
    return items

sent_items = list_messages(sent["ItemId"], "2024-01-01T00:00:00Z", "2024-03-31T23:59:59Z")
print(f"{len(sent_items)} sent items in range")
for it in sent_items:
    print(it["CreatedDate"], it["Name"], "fragment:", it["FragmentId"])
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"
BLOB_LOC="<blob-location-id>"
SOURCE="alice@contoso.com"

# 1 · List folders to find the Sent Items folder:
curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/discovery/$BLOB_LOC/source/$SOURCE/items/-?types=MailboxFolder&PageSize=100"

# 2 · List messages in Sent Items (replace FOLDER_ID with the ItemId from step 1):
FOLDER_ID="<sent-items-folder-item-id>"
curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/discovery/$BLOB_LOC/source/$SOURCE/items/$FOLDER_ID?types=MessageItem&fromDate=2024-01-01T00%3A00%3A00Z&toDate=2024-03-31T23%3A59%3A59Z&PageSize=100"

10 · Collect all files from a user OneDrive

Collect one user's entire drive with a OneDrive collection scoped to that drive owner.

import requests

BASE      = "https://api.dev.datatap.stream"
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

def post(path, json=None):
    r = requests.post(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, json=json, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

locs = get(f"/api/{TENANT_ID}/locations")
ONEDRIVE_LOC = next(l["Id"] for l in locs if l["Type"] == "ONEDRIVE")
BLOB_LOC     = next(l["Id"] for l in locs if l["Type"] == "BLOB")

def discover_sources(location_id):
    sources, marker = [], None
    while True:
        params = {"PageSize": 100}
        if marker:
            params["Marker"] = marker
        page = get(f"/api/{TENANT_ID}/discovery/{location_id}/users", params=params)
        sources.extend(page["Sources"])
        marker = page["Options"].get("Marker")
        if not marker:
            break
    return sources

# Collect a single user's entire OneDrive: a OneDrive collection scoped to one drive owner.
owner = next(a for a in discover_sources(ONEDRIVE_LOC)
             if a["Email"].lower() == "alice@contoso.com")

job = post(f"/api/{TENANT_ID}/jobs", json={
    "name": "OneDrive collection — Alice",
    "description": "Full drive collection",
    "priority": "Medium",
    "type": "BACKUP",
    "schedule": {"type": "NOW"},
    "sourceId": ONEDRIVE_LOC,
    "destinationId": BLOB_LOC,
    "indexFiles": False,
    "owner": "me@contoso.com",
    "policy": {
        "$type": "onedrive-backup",
        "users": [{"item": {"id": owner["Id"], "name": owner["Name"],
                            "email": owner["Email"], "type": owner["Type"]},
                   "type": "INCLUDE"}]
    }
})
post(f"/api/{TENANT_ID}/jobs/{job['Id']}/start")
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"
ONEDRIVE_LOC="<onedrive-location-id>"
BLOB_LOC="<blob-location-id>"
ALICE_ID="<alice-user-id>"   # Id from GET /discovery/$ONEDRIVE_LOC/users

# Create the BACKUP job scoped to Alice's drive:
JOB=$(curl -s -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "OneDrive collection — Alice",
    "priority": "Medium",
    "type": "BACKUP",
    "schedule": {"type": "NOW"},
    "sourceId": "'"$ONEDRIVE_LOC"'",
    "destinationId": "'"$BLOB_LOC"'",
    "indexFiles": false,
    "owner": "me@contoso.com",
    "policy": {
      "$type": "onedrive-backup",
      "users": [
        {"item": {"id": "'"$ALICE_ID"'", "name": "Alice", "email": "alice@contoso.com", "type": "OneDriveUser"}, "type": "INCLUDE"}
      ]
    }
  }' \
  "https://api.dev.datatap.stream/api/$TENANT_ID/jobs")
JOB_ID=$(echo $JOB | python3 -c "import sys,json; print(json.load(sys.stdin)['Id'])")

curl -s -X POST \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/jobs/$JOB_ID/start"

11 · List & download the PDF files from a user drive

Walk the collected drive and keep the .pdf files (filter on the file name — there is no server-side extension filter). The item /download endpoint serves emails only, so to retrieve the actual file bytes run a RESTORE job scoped to PDFs, which writes them back into the user's OneDrive.

import requests

BASE      = "https://api.dev.datatap.stream"
TENANT_ID = "<your-tenant-id>"
API_KEY   = "<your-tenant-api-key>"

def get(path, params=None):
    r = requests.get(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, params=params, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

def post(path, json=None):
    r = requests.post(f"{BASE}{path}", headers={"X-API-Key": API_KEY}, json=json, timeout=60)
    r.raise_for_status()
    return r.json() if r.content else None

locs = get(f"/api/{TENANT_ID}/locations")
BLOB_LOC     = next(l["Id"] for l in locs if l["Type"] == "BLOB")
ONEDRIVE_LOC = next(l["Id"] for l in locs if l["Type"] == "ONEDRIVE")

SOURCE = "alice@contoso.com"

# 1 · LIST — walk the collected drive (BLOB location) and keep the .pdf files. Browsing returns
#     OneDriveFile / OneDriveFolder nodes; recurse folders by their ItemId. There is no server-side
#     extension filter, so match on the file Name.
def list_children(node_id, types):
    items, marker = [], None
    while True:
        params = {"types": types, "PageSize": 100}
        if marker:
            params["Marker"] = marker
        page = get(f"/api/{TENANT_ID}/discovery/{BLOB_LOC}/source/{SOURCE}/items/{node_id}",
                   params=params)
        items.extend(page["Items"])
        marker = page["Options"].get("Marker")
        if not marker:
            break
    return items

def walk_pdfs(node_id="-"):
    pdfs = [f for f in list_children(node_id, "OneDriveFile")
            if f["Name"].lower().endswith(".pdf")]
    for folder in list_children(node_id, "OneDriveFolder"):
        pdfs += walk_pdfs(folder["ItemId"])
    return pdfs

pdfs = walk_pdfs()
print(f"{len(pdfs)} PDFs:", [p["ReadablePath"] for p in pdfs])

# 2 · DOWNLOAD — the /download endpoint currently serves emails only (dataType=email). To get the
#     actual PDF bytes back, run a RESTORE job scoped to PDFs; it writes them into the user's drive.
restore = post(f"/api/{TENANT_ID}/jobs", json={
    "name": "Restore PDFs — Alice",
    "description": "Restore .pdf files to OneDrive",
    "priority": "High",
    "type": "RESTORE",
    "schedule": {"type": "NOW"},
    "sourceId": BLOB_LOC,                              # restore FROM the collection
    "destinationId": ONEDRIVE_LOC,                     # …back INTO OneDrive
    "indexFiles": False,
    "owner": "me@contoso.com",
    "policy": {
        "$type": "onedrive-restore",
        "users": [{"item": {"id": "<alice-id>", "name": "Alice",
                            "email": "alice@contoso.com", "type": "OneDriveUser"}, "type": "INCLUDE"}],
        "extensions": [{"item": "PDF", "type": "INCLUDE"}]   # only .pdf files
    }
})
post(f"/api/{TENANT_ID}/jobs/{restore['Id']}/start")
API_KEY="<your-tenant-api-key>"
TENANT_ID="<your-tenant-id>"
BLOB_LOC="<blob-location-id>"
ONEDRIVE_LOC="<onedrive-location-id>"
SOURCE="alice@contoso.com"
ALICE_ID="<alice-user-id>"

# 1 · List the root of Alice's collected drive (repeat with ItemId to recurse into folders):
curl -s \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/discovery/$BLOB_LOC/source/$SOURCE/items/-?types=OneDriveFile&PageSize=100"

# 2 · Create a RESTORE job scoped to .pdf files only:
JOB=$(curl -s -X POST \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Restore PDFs — Alice",
    "priority": "High",
    "type": "RESTORE",
    "schedule": {"type": "NOW"},
    "sourceId": "'"$BLOB_LOC"'",
    "destinationId": "'"$ONEDRIVE_LOC"'",
    "indexFiles": false,
    "owner": "me@contoso.com",
    "policy": {
      "$type": "onedrive-restore",
      "users": [
        {"item": {"id": "'"$ALICE_ID"'", "name": "Alice", "email": "alice@contoso.com", "type": "OneDriveUser"}, "type": "INCLUDE"}
      ],
      "extensions": [{"item": "PDF", "type": "INCLUDE"}]
    }
  }' \
  "https://api.dev.datatap.stream/api/$TENANT_ID/jobs")
JOB_ID=$(echo $JOB | python3 -c "import sys,json; print(json.load(sys.stdin)['Id'])")

curl -s -X POST \
  -H "X-API-Key: $API_KEY" \
  "https://api.dev.datatap.stream/api/$TENANT_ID/jobs/$JOB_ID/start"