SharePointVault ~/guides ⌕ Search 404 pages, error messages, scripts…Ctrl K ★ Star
M↓ README.mdM↓ calling-sharepoint-rest-like-a-pro.md
sharepoint-vault›guides›calling-sharepoint-rest-like-a-pro.mdhistoryedit on GitHub
● GUIDE· 5 min read

Calling SharePoint REST like a pro

applies to: SharePoint Online (most of it also SharePoint Server)reviewed: 2026-09-24#rest-api#spfx#odata#guide

Most SharePoint REST pain comes from three things the docs gloss over: which of the two clients (and OData worlds) you're in, the headers that actually matter, and how to write data safely. Get those right and the sharp edges mostly disappear.

Většina bolesti se SharePoint REST pramení ze tří věcí, které dokumentace obchází: ve kterém ze dvou klientů (a OData světů) jste, které hlavičky doopravdy rozhodují, a jak bezpečně zapisovat data. Zvládněte je a ostré hrany z velké části zmizí.

SharePoint's REST API is powerful, well-documented in the happy path — and full of sharp edges the docs don't mention. This guide is the map we wish we'd had: which client to use, which headers actually matter, how to write data without surprises, and how to diagnose the inevitable 400.

1. Two clients, two OData worlds

Context Client Auth OData default
SPFx web part / extension SPHttpClient handled for you (including request digest) 4.0, minimalmetadata
Browser console, bookmarklet, external script fetch with cookies session cookie + X-RequestDigest for writes whatever you send

Two consequences people trip over:

  • SPHttpClient silently adds OData-Version: 4.0. Legacy endpoints that only speak OData 3 (search, /Files/add) fail until you override it — see the header table below.
  • Payload style must match the OData mode: modern calls use '@odata.type': '#SP.List'; the old __metadata: { type: 'SP.List' } exists only in odata=verbose — and under SPHttpClient even a fully verbose request with it 400s, because of the added OData-Version: 4.0. Drop __metadata; for ordinary writes the type comes from the URL, and where the verbose form is really needed, an empty odata-version header puts SPHttpClient into OData v3 mode.

Getting a request digest outside SPFx:

const ctx = await (await fetch(`${webUrl}/_api/contextinfo`, {
  method: 'POST',
  headers: { 'Accept': 'application/json;odata=nometadata' }
})).json();
// ctx.FormDigestValue → send as X-RequestDigest on every write; refresh before it expires

2. The headers that matter

Hard-won pairings — when a call fails with 400/406/500 and you don't know why, check this table first:

Call Headers that work If you get it wrong
GET …/items, most reads Accept: application/json;odata=nometadata — (this one's forgiving)
POST …/items Accept: application/json — or ;odata=nometadata, both return 201 Nothing breaks, but the response shape moves: with OData-Version: 4.0 (every SPHttpClient call) SharePoint ignores odata=nometadata and even odata=verbose in Accept and answers odata.metadata=minimal (no d wrapper, extra @odata.* keys); ask for ;odata.metadata=none if you want none. An older version of this row claimed a 406 — a live test on 2026-09-24 did not reproduce it. POST …/lists and …/fields were not re-measured
Anything /_api/search/* odata-version: 3.0 + Accept: application/json;odata=nometadata 500 UnknownError, search only
POST …/Files/add(…) Accept: application/json;odata=verbose + OData-Version: 3.0 406 "ACCEPT header missing or invalid"
Body contains __metadata Drop it and send plain JSON — verbose headers work only without OData v4: a bare fetch, or SPHttpClient with odata-version: '' 400 "property '__metadata' does not exist" / "JSON Light … not supported"

3. Writing data

Create — POST to the items collection; prefer resolving the list by URL, not title:

await spHttpClient.post(
  `${webUrl}/_api/web/GetList(@u)/items?@u='${listUrl}'`,
  SPHttpClient.configurations.v1,
  {
    headers: { 'Accept': 'application/json', 'Content-Type': 'application/json' },
    body: JSON.stringify({ Title: 'Hello', EventDate: new Date(input).toISOString() })
  });

Update — POST with method-override headers (works everywhere, including old servers):

headers: {
  'Accept': 'application/json',
  'Content-Type': 'application/json',
  'X-HTTP-Method': 'MERGE',
  'IF-MATCH': '*'          // or a concrete etag for optimistic concurrency
}

Delete — same shape with X-HTTP-Method: DELETE. Consider POST …/items(id)/recycle() instead — recoverable beats gone.

Field-value traps to know before your first write: DateTime wants full ISO · lookup/person fields are written via <Name>Id · Choice fields validate nothing · apostrophes in OData literals double.

Non-negotiable: check res.ok on every write and read the body on failure. SharePoint's error messages usually name the real problem — a silently swallowed 400 becomes "the feature randomly doesn't work" three weeks later:

if (!res.ok) {
  const body = await res.text().catch(() => '');
  throw new Error(`Save failed (HTTP ${res.status}): ${body.slice(0, 300)}`);
}

4. Reading well

  • $select only what you render; add $expand for lookup and person columns, $expand=File&$select=File/Length for file size and version.
  • Past a few thousand items, page with odata.nextLink — $skip is silently ignored on items — and make sure your $filter leads with an indexed column.
  • Text filters on /_api/web/siteusers are case-sensitive (substringof('megan', Title) won't find "Megan") — test the endpoint you rely on rather than assuming one behaviour for all of them. For user-facing search-as-you-type, fetch a candidate set and filter client-side, or use SP Search.

5. Creating lists and fields over REST

Provisioning-style calls have their own micro-rules:

  • Field types go through the base type + FieldTypeKind — some specific OData types simply don't exist:

    { '@odata.type': '#SP.Field', FieldTypeKind: 8, Title: 'IsActive' }      // Boolean — '#SP.FieldBoolean' is NOT a thing
    { '@odata.type': '#SP.Field', FieldTypeKind: 9, Title: 'Amount' }        // Number  — '#SP.FieldNumber' neither
    { '@odata.type': '#SP.FieldText', FieldTypeKind: 2, Title: 'Code' }      // this one exists
  • Don't set Indexed: true at creation time — SharePoint may answer 500 (index limits); create the field first, index it as a separate step.

  • Prefer try-create-and-tolerate-exists over check-then-create — existence probes cost an extra round trip and some of them return 406 through SPHttpClient anyway.

6. When it fails anyway — the ten-minute diagnosis

  1. Read the response body. Not just the status. The body names the field, the type, the missing header.
  2. 406? Wrong Accept for that endpoint family — see the table in §2.
  3. 400 on a write? A/B-test it: same call as a harmless no-op (a MERGE with one known-good field) in plain mode vs verbose mode — with the OData-Version header your real client sends, or a bare fetch lets the verbose arm pass and tells you the opposite of what your app will see. One minute, and you know whether it's the wire format or your payload.
  4. 500 only on search? It's the odata-version header. It's always the header.
  5. Works for you, empty for users? Same query returning rows to one account and zero (HTTP 200!) to another is usually item-level security (ReadSecurity) or permissions — check which account you're testing with before blaming cache.
  6. Don't trust adjacent evidence. "The admin portal shows the data" or "it works in the browser address bar" does not prove your call path — different client, different headers, different auth. Reproduce with the failing client before concluding anything.

Every claim above earned its place by breaking something first. Corrections and additions welcome — see CONTRIBUTING.

Was this helpful?ask or add your experience on GitHub →
Saved you an hour? A star on GitHub helps other SharePoint people find the vault.★ Star on GitHub
last reviewed: 2026-09-24last updated: 2026-09-24historyedit on GitHubview source