Skip to content

Repository files navigation

Newsdata.io logo

Newsdata.io .NET SDK

NuGet NuGet downloads CI .NET License OpenAPI

Official .NET client (SDK) for the Newsdata.io News API. Async methods for every endpoint (latest, archive, sources, crypto, market, count, crypto/count, market/count) with client-side parameter validation, retries with exponential backoff, IAsyncEnumerable-based pagination, and a typed exception hierarchy.

Built on .NET 8's HttpClient and System.Text.Jsonzero runtime dependencies. Thread-safe.

Installation

dotnet add package Newsdata.Api

Or csproj:

<PackageReference Include="Newsdata.Api" Version="0.2.1" />

Quickstart

using Newsdata.Api;
using Newsdata.Api.Exceptions;

using var client = new NewsDataApiClient(
    Environment.GetEnvironmentVariable("NEWSDATA_API_KEY")!);

var resp = await client.LatestAsync(Params.Of()
    .With("q", "bitcoin")
    .With("country", new[] { "us", "gb" })
    .With("language", new[] { "en" }));

foreach (var article in resp.GetArticles())
{
    Console.WriteLine($"{article.Title}{article.Link}");
}

Endpoints

Method Endpoint Notes
client.LatestAsync(params) /1/latest Real-time news
client.ArchiveAsync(params) /1/archive Historical news
client.SourcesAsync(params) /1/sources Available sources (single page)
client.CryptoAsync(params) /1/crypto Cryptocurrency news
client.MarketAsync(params) /1/market Market / financial news
client.CountAsync(params) /1/count Aggregate counts (requires from_date, to_date)
client.CryptoCountAsync(params) /1/crypto/count Aggregate crypto counts
client.MarketCountAsync(params) /1/market/count Aggregate market counts
client.WebsocketRegisterAsync(params) /1/websocket/register Register a real-time query
client.WebsocketFetchAsync() /1/websocket/fetch List registered queries
client.WebsocketDeleteAsync(id) /1/websocket/delete Delete a registered query

params is IDictionary<string, object?> — pass a Params instance for fluent construction. Values may be string, int, double, bool, or IEnumerable<string> (sent comma-joined). Parameter names are case-insensitive — qInTitle and qintitle are equivalent.

Every method accepts an optional CancellationToken as the last argument.

Pagination

// 1) ScrollAllAsync: follow nextPage cursors and return one merged response.
var all = await client.ScrollAllAsync(
    Endpoint.Latest,
    Params.Of().With("q", "news"),
    maxResults: 200);   // 0 = no cap

// 2) Paginate: idiomatic IAsyncEnumerable.
await foreach (var page in client.Paginate(
    Endpoint.Latest, Params.Of().With("q", "news")))
{
    foreach (var article in page.GetArticles())
        Process(article);
}

Use break (or a cancelled CancellationToken) to stop the await foreach early.

Raw query

await client.LatestAsync(Params.Of()
    .With("rawQuery", "q=bitcoin&country=us&language=en"));

rawQuery is mutually exclusive with every other parameter and is parsed and validated against the endpoint's allowed keys before the request leaves.

Client-side validation

A NewsdataValidationException is thrown — before any HTTP request — when:

  • a parameter is not accepted by that endpoint;
  • mutually-exclusive parameters are set together — q/qInTitle/qInMeta, country/excludecountry, category/excludecategory, language/excludelanguage, domain/domainurl/excludedomain;
  • size is outside 1–50;
  • sentiment_score is set without sentiment;
  • a count endpoint is missing from_date or to_date.

Booleans for full_content, image, video, and removeduplicate are coerced to "1"/"0".

Real-time news (WebSocket)

Register a query first — the returned registration_id identifies it from then on:

var registered = await client.WebsocketRegisterAsync(
    Params.Of().With("q", "bitcoin").With("language", "en"));
var registrationId = registered.Results.GetProperty("registration_id").GetString()!;

WebsocketRegisterAsync takes the familiar filter names (q, country, language, domain, …) — no date or paging filters, since a registered query matches news as it is published. Registering an identical query twice throws NewsdataApiException with status 409; the existing id is in its response body. WebsocketFetchAsync() lists every registered query and WebsocketDeleteAsync(id) removes one.

Then stream — each response has the familiar Status / TotalResults / Results shape:

using var ws = new NewsDataApiWebSocket(client);

await foreach (var response in ws.StreamAsync(registrationId))
{
    foreach (var article in response.GetArticles())
        Console.WriteLine($"{article.Title} - {article.Link}");
}

Break out of the loop, cancel the token, or dispose the instance to stop; the connection is closed either way.

Transient drops (network errors, server restarts, abnormal closes) are reconnected automatically with a capped exponential backoff. Set Reconnect = false to stop on the first disconnect instead. A permanent rejection — bad API key or unknown registration_id, exhausted API credits, or too many simultaneous devices — throws NewsdataWebSocketAuthException and is not retried.

The server always accepts the handshake and then closes with code 1008 when the connection is refused, carrying one of three reasons: invalid credentials or registration not found, api limit reached, or device limit reached (more than 5 devices on one registration_id). Every other close code — including 1013 (send timeout, meaning the client read too slowly) — is transient and reconnects.

Each delivered article consumes 1 API credit per connected device.

Catch it like any other client error:

try
{
    await foreach (var response in ws.StreamAsync(registrationId)) { /* ... */ }
}
catch (NewsdataWebSocketAuthException e) { Console.Error.WriteLine($"rejected: {e.Message}"); }
catch (NewsdataWebSocketException e)     { Console.Error.WriteLine($"stream error: {e.Message}"); }

All connection options are optional:

using var ws = new NewsDataApiWebSocket(client, new NewsDataApiWebSocketOptions
{
    BaseUrl           = "wss://ws.newsdata.io/ws/event",  // staging / self-hosted / proxied
    Reconnect         = true,                             // default true
    ReconnectDelay    = TimeSpan.FromSeconds(1),          // first delay; doubles each retry
    ReconnectDelayMax = TimeSpan.FromSeconds(30),         // cap on the delay
    HandshakeTimeout  = TimeSpan.FromSeconds(10),         // Zero disables
    Headers           = new Dictionary<string, string> { ["X-Trace"] = "abc" },
});

Error handling

using Newsdata.Api.Exceptions;

try
{
    await client.LatestAsync(Params.Of().With("q", "news"));
}
catch (NewsdataValidationException e) { /* bad param — e.Param */ }
catch (NewsdataAuthException e)       { /* 401 / 403 */ }
catch (NewsdataRateLimitException e)  { /* 429 — e.RetryAfter */ }
catch (NewsdataServerException e)     { /* 5xx */ }
catch (NewsdataApiException e)        { /* other API errors — e.StatusCode */ }
catch (NewsdataNetworkException e)    { /* transport — e.InnerException */ }

Hierarchy:

NewsdataException (extends Exception)
├── NewsdataValidationException             (.Param)
├── NewsdataApiException                    (.StatusCode, .ResponseBody)
│   ├── NewsdataAuthException               (401 / 403)
│   ├── NewsdataRateLimitException          (429; .RetryAfter)
│   └── NewsdataServerException             (5xx)
├── NewsdataNetworkException                (.InnerException)
└── NewsdataWebSocketException              (real-time stream)
    └── NewsdataWebSocketAuthException      (policy-violation close 1008)

Configuration

using var client = new NewsDataApiClient(new NewsDataApiClientOptions
{
    ApiKey = apiKey,
    Timeout = TimeSpan.FromSeconds(30),
    MaxRetries = 5,
    RetryBackoff = TimeSpan.FromSeconds(2),
    RetryBackoffMax = TimeSpan.FromSeconds(60),
    PaginationDelay = TimeSpan.FromSeconds(1),
    BaseUrl = "https://staging.example/api/1/",
    IncludeHeaders = true,
    HttpClient = myInjectedClient,     // proxies, mTLS, etc.
    Logger = (level, msg) => Console.WriteLine($"{level}: {msg}"),
});

Retries cover network errors, HTTP 429, and 5xx. 429 honours the Retry-After header (integer seconds or HTTP-date); otherwise backoff is exponential. Auth (401/403) and other 4xx errors are never retried. The API key is redacted from any URL passed to the logger.

Concurrency

NewsDataApiClient is thread-safe — share one instance across your application. Dispose it on shutdown (or use using var); the SDK only disposes the underlying HttpClient if it created it.

Development

dotnet build
dotnet test                        # 33 tests, no API key needed
dotnet pack -c Release             # produces .nupkg + .snupkg

Tests use a custom HttpMessageHandler mock — no WireMock, no network, no external test dep.

Releasing

Tag-push driven. Bump <Version> in src/Newsdata.Api/Newsdata.Api.csproj, push a v-prefixed tag, and publish.yml does the rest:

git tag -a v0.0.2 -m "v0.0.2"
git push origin v0.0.2

See .github/workflows/publish.yml for the NuGet.org publishing flow.

Related libraries

Official Newsdata.io clients across languages and runtimes:

Also see free news datasets for ML / NLP work.

License

MIT

About

Official .NET client (SDK) for the Newsdata.io News API — async methods for real-time, historical, crypto and market news with validation, retries, and typed exceptions.

Topics

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages