TL;DR
News Link pages are beta-only, so you need the Microsoft.Graph.Beta NuGet package. For banner image uploads, you also need Microsoft.Kiota.Serialization.Multipart. The tricky part is that the multipart case has no official C# snippet — Microsoft’s documentation just says “Snippet not available.”
Here’s the short version:
var page = new NewsLinkPage
{
OdataType = "#microsoft.graph.newsLinkPage",
Title = title,
NewsWebUrl = url,
AdditionalData = new Dictionary<string, object>
{
["@microsoft.graph.bannerImageWebUrlContent"] = "name:content"
}
};
var multipartBody = new MultipartBody();
multipartBody.AddOrReplacePart("request", "application/json", page);
multipartBody.AddOrReplacePart("content", "image/jpeg", bannerImageContent, fileName: "banner.jpg");
Want the full working implementation? Read on. 👇
The Problem With the Documentation
News Link pages in SharePoint — those cards that link to external news articles — are only available through the Graph API’s beta endpoint. That’s fine, beta APIs are part of life.
What’s not fine is that Microsoft’s official documentation covers the simple case well, but the moment you want to add a banner image, the C# code snippet disappears and is replaced with:
“Snippet not available.”
Great. Thanks.
The banner image upload requires a multipart request. Figuring out how to do that with the Graph Beta SDK means piecing together documentation about MultipartBody, some Kiota internals, and a few quirks you’ll only discover by actually trying it.
This post covers the full working solution.
Prerequisites
Install these two NuGet packages:
Microsoft.Graph.Beta
Microsoft.Kiota.Serialization.Multipart
Microsoft.Graph.Beta is the Beta SDK — it contains NewsLinkPage and all the beta models. Microsoft.Kiota.Serialization.Multipart provides the MultipartBody class needed to construct multipart requests. Without it, you don’t have the types to send binary data alongside the JSON payload.
The Simple Case: No Banner Image
If you just want a News Link without a banner image, the Beta SDK fluent API handles it cleanly:
var page = new NewsLinkPage
{
OdataType = "#microsoft.graph.newsLinkPage",
Title = "Contoso Unveils First Self-Driving Car",
NewsWebUrl = "https://someexternalnewssite.com/article",
Description = "A brief description of the article."
};
var result = await GraphClient.Sites[siteId].Pages.PostAsync(page, requestConfiguration =>
{
requestConfiguration.Headers.Add("prefer", "include-unknown-enum-members");
});
Two things to note:
The Prefer: include-unknown-enum-members header is required. Without it, the API won’t return newsLink as a valid pageLayoutType value — it’s an evolvable enum that hasn’t been promoted to v1.0 yet, so Graph treats it as unknown by default.
The page is also created as a draft. You still need to publish it separately before it shows up in the news feed.
The Full Solution: With Banner Image
Here’s the complete implementation including banner image upload and publishing:
internal async Task CreateNewsLink(
string siteId,
string? title,
string? url,
string? description,
Stream? bannerImageContent)
{
var page = new NewsLinkPage
{
OdataType = "#microsoft.graph.newsLinkPage",
Title = title,
NewsWebUrl = url,
Description = description,
AdditionalData = new Dictionary<string, object>
{
["@microsoft.graph.bannerImageWebUrlContent"] = "name:content"
}
};
var multipartBody = new MultipartBody();
multipartBody.AddOrReplacePart("request", "application/json", page);
multipartBody.AddOrReplacePart("content", "image/jpeg", bannerImageContent, fileName: "banner.jpg");
var requestInfo = new RequestInformation
{
HttpMethod = Method.POST,
UrlTemplate = $"https://graph.microsoft.com/beta/sites/{siteId}/pages"
};
requestInfo.Headers.Add("prefer", "include-unknown-enum-members");
requestInfo.SetContentFromParsable(GraphClient.RequestAdapter, "multipart/form-data", multipartBody);
var response = await GraphClient.RequestAdapter.SendAsync<NewsLinkPage>(
requestInfo,
NewsLinkPage.CreateFromDiscriminatorValue
);
var createdNewslink = await GraphClient.Sites[siteId].Pages[response.Id].GetAsync(
requestConfiguration =>
{
requestConfiguration.Headers.Add("prefer", "include-unknown-enum-members");
}
);
var publishRequestInfo = new RequestInformation
{
HttpMethod = Method.POST,
UrlTemplate = $"https://graph.microsoft.com/beta/sites/{siteId}/pages/{createdNewslink.Id}/microsoft.graph.newsLinkPage/publish"
};
await GraphClient.RequestAdapter.SendNoContentAsync(publishRequestInfo);
}
It looks like a lot, but each part has a specific reason for being there. Let me explain the non-obvious bits.
Breaking Down the Code
The @microsoft.graph.bannerImageWebUrlContent Annotation
AdditionalData = new Dictionary<string, object>
{
["@microsoft.graph.bannerImageWebUrlContent"] = "name:content"
}
This is the glue between the JSON part and the image bytes. The value "name:content" tells the Graph API: “find the image bytes in the multipart part named ‘content’”.
When the API processes the request, it reads this annotation, locates the content part in the multipart body, saves the image to the site’s assets library, and sets the bannerImageWebUrl property on the created page. The naming has to match — the part you add with AddOrReplacePart("content", ...) is what the annotation references.
Why MultipartBody Instead of the Fluent API
The normal fluent API — GraphClient.Sites[siteId].Pages.PostAsync(...) — only supports JSON payloads. There’s no overload that accepts binary data or constructs a multipart request.
MultipartBody fills that gap. You compose the request from named parts, each with their own content type:
var multipartBody = new MultipartBody();
multipartBody.AddOrReplacePart("request", "application/json", page);
multipartBody.AddOrReplacePart("content", "image/jpeg", bannerImageContent, fileName: "banner.jpg");
The "request" part carries the JSON, the "content" part carries the image bytes. The names are what you reference in @microsoft.graph.bannerImageWebUrlContent.
Why RequestInformation Directly
Since the fluent API can’t send multipart requests, we drop down one level to RequestInformation — the underlying request abstraction that all Kiota-generated clients use internally:
var requestInfo = new RequestInformation
{
HttpMethod = Method.POST,
UrlTemplate = $"https://graph.microsoft.com/beta/sites/{siteId}/pages"
};
requestInfo.SetContentFromParsable(GraphClient.RequestAdapter, "multipart/form-data", multipartBody);
SetContentFromParsable serializes the MultipartBody and sets the correct Content-Type header — including the boundary parameter that multipart requests require. This is one of those things you have to discover by reading the Kiota source code rather than the docs.
The Extra GET After Creation
You might notice the code fetches the page again right after creating it:
var createdNewslink = await GraphClient.Sites[siteId].Pages[response.Id].GetAsync(...);
This is a quirk of the multipart POST. The response from a raw RequestInformation-based call doesn’t go through the same deserialization pipeline as the fluent API, which means the returned NewsLinkPage object isn’t fully populated. Rather than fighting with it, a quick GET on the newly created page — with the proper prefer header — gives you a cleanly deserialized object with the correct Id to use for publishing.
Publishing the News Link
Pages are created as drafts. To make the News Link appear in the SharePoint news feed, you have to explicitly publish it:
var publishRequestInfo = new RequestInformation
{
HttpMethod = Method.POST,
UrlTemplate = $"https://graph.microsoft.com/beta/sites/{siteId}/pages/{createdNewslink.Id}/microsoft.graph.newsLinkPage/publish"
};
await GraphClient.RequestAdapter.SendNoContentAsync(publishRequestInfo);
Again, RequestInformation directly — the Graph Beta SDK’s fluent API doesn’t expose a typed publish method for newsLinkPage.
What About the v1.0 SDK?
Not supported yet. The NewsLinkPage type, the pageLayout: newsLink enum value, and the publish endpoint are all beta-only. When the API graduates to v1.0, the approach should be almost identical — just swap Microsoft.Graph.Beta for Microsoft.Graph.
Until then, you’re on beta. Microsoft officially cautions against using beta APIs in production, but in practice this particular API has been stable for a while. Use your own judgment.