Adsefid.Sdk 0.6.0

dotnet add package Adsefid.Sdk --version 0.6.0
                    
NuGet\Install-Package Adsefid.Sdk -Version 0.6.0
                    
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Adsefid.Sdk" Version="0.6.0" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Adsefid.Sdk" Version="0.6.0" />
                    
Directory.Packages.props
<PackageReference Include="Adsefid.Sdk" />
                    
Project file
For projects that support Central Package Management (CPM), copy this XML node into the solution Directory.Packages.props file to version the package.
paket add Adsefid.Sdk --version 0.6.0
                    
#r "nuget: Adsefid.Sdk, 0.6.0"
                    
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
#:package Adsefid.Sdk@0.6.0
                    
#:package directive can be used in C# file-based apps starting in .NET 10 preview 4. Copy this into a .cs file before any lines of code to reference the package.
#addin nuget:?package=Adsefid.Sdk&version=0.6.0
                    
Install as a Cake Addin
#tool nuget:?package=Adsefid.Sdk&version=0.6.0
                    
Install as a Cake Tool

Adsefid.Sdk

CI NuGet

A .NET client SDK for the adsefid.com SMS Web Service REST API: SMS, Messenger and User endpoints, plus outgoing webhook signature verification.

  • Single target framework: net8.0
  • Core package has zero external dependencies (BCL only) and zero reflection — JSON is handled by a source-generated System.Text.Json serializer context
  • Optional Adsefid.Sdk.DependencyInjection package integrates with IHttpClientFactory
  • Throws typed exceptions on error; never returns a Result/Either wrapper

Requirements

  • .NET 8.0 SDK or later

Install

dotnet add package Adsefid.Sdk

Quickstart

using Adsefid.Sdk;
using Adsefid.Sdk.Sms.Models;

var client = new AdsefidClient(new AdsefidClientOptions
{
    ApiKey = Environment.GetEnvironmentVariable("ADSEFID_API_KEY")!,
});

var result = await client.Sms.SendSingleAsync(new SendSingleSmsRequest
{
    Receptor = "98912****567",
    LineNumber = "983000XXX",
    Message = "Hello from Adsefid.Sdk",
    LocalId = "order-10001",
});

Console.WriteLine($"message_id={result.MessageId} status={result.Status} cost={result.Cost}");

A runnable version of this is in examples/Quickstart.

Auth setup

The SDK never reads environment variables itself — read your API key explicitly and pass it in:

var apiKey = Environment.GetEnvironmentVariable("ADSEFID_API_KEY")
    ?? throw new InvalidOperationException("ADSEFID_API_KEY is not set.");

var client = new AdsefidClient(new AdsefidClientOptions { ApiKey = apiKey });

Configuration

var client = new AdsefidClient(new AdsefidClientOptions
{
    ApiKey = apiKey,

    // Override the base URL (defaults to "https://api.adsefid.com")
    BaseUrl = "https://api.adsefid.com",

    // Defaults to "adsefid-dotnet/<SDK_VERSION>".
    UserAgent = "my-service/1.0.0",

    // Supply your own HttpClient, e.g. one created via IHttpClientFactory.
    // When omitted, every AdsefidClient shares one static HttpClient. The SDK
    // always sends the absolute request URI and the X-API-KEY header itself,
    // so the client you supply needs no BaseAddress or default headers.
    HttpClient = httpClientFactory.CreateClient("adsefid"),
});

To control timeouts, configure them on the HttpClient you supply (HttpClient.Timeout, or via IHttpClientFactory handler configuration) — the SDK does not impose its own timeout.

Monetary response properties (Cost, TotalCost, and CreditLeft) use decimal and may contain fractional values.

Dependency injection

Install the optional companion package. It includes the core SDK transitively:

dotnet add package Adsefid.Sdk.DependencyInjection

Register AdsefidClient as a typed HTTP client:

using Adsefid.Sdk;
using Adsefid.Sdk.Sms.Models;
using Microsoft.Extensions.DependencyInjection;

builder.Services
    .AddAdsefid(_ => new AdsefidClientOptions
    {
        ApiKey = builder.Configuration["Adsefid:ApiKey"]
            ?? throw new InvalidOperationException("Adsefid:ApiKey is not set."),
    })
    .ConfigureHttpClient(httpClient => httpClient.Timeout = TimeSpan.FromSeconds(30));

Inject the concrete client into transient or scoped services:

public sealed class MessageSender(AdsefidClient client)
{
    public Task<SendSingleSmsResponse> SendAsync(
        SendSingleSmsRequest request,
        CancellationToken cancellationToken = default) =>
        client.Sms.SendSingleAsync(request, cancellationToken);
}

AddAdsefid returns IHttpClientBuilder, so configure handlers, proxies, logging, and timeouts on that builder. Do not set AdsefidClientOptions.HttpClient in the options factory. Typed clients are transient; do not capture one in a singleton service.

Resource reference

Resource SDK method HTTP endpoint Notes
Sms SendSingleAsync(request) POST /v1/sms/single
Sms SendBulkAsync(request) POST /v1/sms/bulk Partial success is a normal typed return, not an exception
Sms SendP2PAsync(request) POST /v1/sms/p2p Partial success is a normal typed return, not an exception
Sms SendTemplateAsync(request) POST /v1/sms/template
Sms GetStatusAsync(messageIds?, localIds?) GET /v1/sms/status
Sms CancelAsync(request) POST /v1/sms/cancel Awaiting approval, queued, or scheduled; no refund; max 2,000 distinct IDs combined
Sms GetReceivedAsync(lineNumber, count?, since?) GET /v1/sms/receive
Messenger SendSingleAsync(request) POST /v1/messenger/single
Messenger SendBulkAsync(request) POST /v1/messenger/bulk Partial success is a normal typed return, not an exception
Messenger SendP2PAsync(request) POST /v1/messenger/p2p Partial success is a normal typed return, not an exception
Messenger UploadFileAsync(fileStream, fileName, contentType) POST /v1/messenger/file Accepts a Stream (caller owns its lifetime)
Messenger CancelAsync(request) POST /v1/messenger/cancel Awaiting approval, queued, or scheduled; no refund; max 2,000 distinct IDs combined
Messenger SendTemplateAsync(request) POST /v1/messenger/template
Messenger GetStatusAsync(messageIds?, localIds?) GET /v1/messenger/status
User GetInfoAsync() GET /v1/user/info
User GetLinesAsync() GET /v1/user/lines
User GetProfilesAsync() GET /v1/user/profiles
User GetTemplatesAsync(state?, skip?, take?) GET /v1/user/templates

Error handling

Every resource method throws on failure instead of returning a result wrapper:

using Adsefid.Sdk.Enums;
using Adsefid.Sdk.Exceptions;

try
{
    var result = await client.Sms.SendSingleAsync(request);
}
catch (AdsefidRateLimitException ex)
{
    // ex.Code is MessageLimitReached (2035) or RequestLimitReached (2036)
    Console.WriteLine($"Rate limited: {ex.Name}");
}
catch (AdsefidApiException ex)
{
    Console.WriteLine($"API error {ex.Code} ({ex.Name}), HTTP {ex.HttpStatusCode}");
    if (ex.Details?.Errors is { } errors)
    {
        foreach (var (field, error) in errors)
        {
            Console.WriteLine($"{field}: {error.Name} ({(int)error.Code})");
        }
    }
}
catch (AdsefidValidationException ex)
{
    // Thrown client-side, before any network call (e.g. an invalid local_id format)
    Console.WriteLine($"Invalid request: {ex.Message}");
}
catch (AdsefidTransportException ex)
{
    // Network, DNS, or timeout failure
    Console.WriteLine($"Transport error: {ex.Message}");
}

AdsefidApiException.Details is an ApiErrorDetails?. Its optional Errors dictionary maps field names (or rejected cancel IDs) to ApiFieldError { Code, Name }; its optional Items list contains ApiItemError { Index, Errors } for rejected bulk/P2P entries. Both properties are absent when empty. Unknown numeric codes remain readable through the integer-backed enum.

Bulk and P2P send endpoints have partial-success semantics: an HTTP 200 with status: "success" can still contain some failed items. These are not exceptions — they come back as a normal typed response (e.g. SendBulkSmsResponse.Receptors) where each item carries its own status/code. The SDK therefore validates request-level fields locally but sends each item unchanged for the API to accept or reject independently.

Each item's Status is the raw WebServiceCode from the service: 1000-1999 means the item was accepted and MessageStatus gives it as a WebServiceMessageStatus?; 2000 and above means that one item was rejected and ErrorCode gives it as a WebServiceResponseCode?. Both views are null for a code this SDK does not know yet, and WebServiceCode.AsMessageStatus/AsErrorCode do the same split for any raw value:

foreach (var item in result.Receptors)
{
    Console.WriteLine(item.ErrorCode is { } error
        ? $"{item.Receptor}: rejected ({error})"
        : $"{item.Receptor}: {item.MessageStatus?.ToString() ?? item.Status.ToString()}");
}

Rate limits

Codes 2035 (MessageLimitReached) and 2036 (RequestLimitReached) — and a bare HTTP 429 with an unparseable body — surface as AdsefidRateLimitException (which derives from AdsefidApiException).

Enums

Enum Namespace
LineSelector Adsefid.Sdk.Enums
WebServiceMessageStatus Adsefid.Sdk.Enums
WebServiceResponseCode Adsefid.Sdk.Enums
TemplateState Adsefid.Sdk.Enums
TemplateParameterType Adsefid.Sdk.Enums

AdsefidApiException.Code is parsed permissively: an unrecognized numeric code becomes (WebServiceResponseCode)rawInt instead of throwing, so the SDK degrades gracefully as the API adds new codes over time. TemplateParameterType covers the documented set only — the live service also emits an undocumented third value, and such a parameter is dropped from UserTemplate.Parameters rather than surfaced as an enum member that does not exist.

Template parameters, leading zeros and decimals

TemplateParameterValue holds either a string or a number, with implicit conversions from string, int, long, decimal, and double. A parameter the template declares as number may be sent either as a JSON number or as a JSON string, and the service substitutes a numeric string verbatim — so a string is the only way to keep a value's exact digits:

await client.Sms.SendTemplateAsync(new SendTemplateSmsRequest
{
    TemplateId = "invoice_notice",
    Parameters = new Dictionary<string, TemplateParameterValue>
    {
        ["invoice"] = "001234", // renders as 001234 — the number 1234 would lose the zeros
        ["amount"]  = "1.50",   // renders as 1.50   — the number 1.5 would lose the zero
        ["count"]   = 2,        // an ordinary integer
        ["rate"]    = 19.99m,   // a decimal, which round-trips exactly
    },
    Receptor = "09120000000",
    LineNumber = "983000XXX",
});

Numbers are held as decimal, so an ordinary decimal survives a round trip that double would have perturbed; reading one back gives value.AsNumber, and a string gives value.AsString. Reach for a string whenever the rendered text must match the digits you supplied — invoice and account numbers, zero-padded codes, and money amounts with a fixed number of decimal places.

A default(TemplateParameterValue) was never assigned and is not a usable value: serializing one throws rather than silently emitting JSON null.

See examples/Templates for a runnable version.

File upload example

using var fileStream = File.OpenRead("brochure.pdf");
var upload = await client.Messenger.UploadFileAsync(fileStream, "brochure.pdf", "application/pdf");

await client.Messenger.SendSingleAsync(new SendSingleMessengerRequest
{
    Message = "See attached brochure",
    Receptor = "98912****567",
    Profile = profileId,
    FileId = upload.FileId,
});

The service enforces its documented MIME allowlist and 15 MB limit. Oversized uploads return FileTooLarge (2047, HTTP 413).

Webhook verification

using Adsefid.Sdk.Webhooks;

var app = WebApplication.Create();

app.MapPost("/webhooks/adsefid", async (HttpRequest request) =>
{
    // Verify the exact bytes the service signed; do not let a JSON body reader touch them first.
    using var buffer = new MemoryStream();
    await request.Body.CopyToAsync(buffer);
    var rawBody = buffer.ToArray();

    var signature = request.Headers[WebhookHeaderNames.Signature].ToString();
    var timestamp = request.Headers[WebhookHeaderNames.Timestamp].ToString();

    WebhookEvent webhookEvent;
    try
    {
        webhookEvent = WebhookVerifier.VerifyAndParse(
            rawBody,
            signature,
            timestamp,
            secret: Environment.GetEnvironmentVariable("ADSEFID_WEBHOOK_SECRET")!);
    }
    catch (AdsefidWebhookVerificationException)
    {
        return Results.Unauthorized();
    }

    switch (webhookEvent)
    {
        case ReceiveWebhookEvent receive:
            foreach (var item in receive.Data)
            {
                Console.WriteLine($"Inbound SMS from {item.Sender}: {item.Message}");
            }
            break;

        case StatusWebhookEvent status:
            foreach (var item in status.Data)
            {
                Console.WriteLine($"SMS {item.Id} -> {item.StatusDelivery}");
            }
            break;

        case MessengerStatusWebhookEvent messengerStatus:
            foreach (var item in messengerStatus.Data)
            {
                Console.WriteLine($"Messenger message {item.Id} -> {item.StatusDelivery}");
            }
            break;
    }

    return Results.Ok();
});

app.Run();

VerifyAndParse throws AdsefidWebhookVerificationException for a bad signature, a stale timestamp (default max age: 5 minutes, override via the maxAge parameter), or a malformed payload. Signature verification uses CryptographicOperations.FixedTimeEquals for constant-time comparison.

WebhookHeaderNames (Id, Signature, Timestamp, Event, Attempt) exposes every header name as a constant, so you never have to type "X-Atlas-Webhook-Signature" yourself. WebhookEventTypes (Receive, Status, MessengerStatus) does the same for the type values.

You configure, per webhook endpoint, which event types it receives (in your adsefid.com panel) — an endpoint subscribed only to receive will never see a StatusWebhookEvent arrive, so don't assume every deployment gets all three; handle whichever ones you've subscribed to.

A runnable minimal API version of this is in examples/WebhookReceiver.

The signing secret is Base64

Your endpoint's signing secret is shown in the adsefid.com panel as the Base64 encoding of 32 random bytes, and the service signs with those raw bytes — not with the text of the Base64 string. Pass the secret exactly as the panel shows it and WebhookVerifier.VerifyAndParse decodes it for you; a secret that is not valid Base64 throws AdsefidWebhookVerificationException. If you already hold the decoded key, use the overload that takes a ReadOnlySpan<byte>.

Development

make deps    # dotnet restore
make fmt     # dotnet format
make lint    # dotnet format --verify-no-changes --severity warn
make build   # dotnet build -c Release
make test    # dotnet test -c Release --no-build

The suite is xUnit, under tests/Adsefid.Sdk.Tests. It drives the public AdsefidClient through a fake HttpMessageHandler supplied via AdsefidClientOptions.HttpClient, and reaches the internal pure helpers (Validation, CsvHelper, Limits) through InternalsVisibleTo. Golden fixtures under tests/Adsefid.Sdk.Tests/fixtures are byte-identical to the same tree in the sibling SDK repositories, and FixturesIntegrityTests verifies them against CHECKSUMS.txt.

Examples

Every directory under examples/ is a runnable console project, excluded from the NuGet package via <IsPackable>false</IsPackable>:

export ADSEFID_API_KEY=...
export ADSEFID_LINE_NUMBER=983000XXX

dotnet run --project examples/Account          # account info, lines, profiles, templates; client config
dotnet run --project examples/Quickstart       # send one SMS, with full error triage
dotnet run --project examples/BulkAndP2P       # bulk + P2P sends, and reading a partial success
dotnet run --project examples/Templates        # list templates and send one, incl. exact numeric values
dotnet run --project examples/StatusAndCancel  # delivery status, cancelling, inbound messages
dotnet run --project examples/Messenger        # upload an attachment and send it via a messenger profile
dotnet run --project examples/WebhookReceiver  # verify and dispatch inbound webhooks

examples/Account sends nothing, so it is the safest one to try first.

Versioning

This SDK follows Semantic Versioning independently of the API documentation.

  • Core SDK version: 0.6.0 (Adsefid.Sdk)
  • Dependency injection package version: 0.1.0 (Adsefid.Sdk.DependencyInjection)
  • Verified API documentation: v1.14.0

Core releases use v<SDK_VERSION> tags. Dependency injection releases use dependency-injection-v<PACKAGE_VERSION> tags. Package versions and API documentation versions move independently.

License

MIT — see LICENSE.

Product Compatible and additional computed target framework versions.
.NET net8.0 is compatible.  net8.0-android was computed.  net8.0-browser was computed.  net8.0-ios was computed.  net8.0-maccatalyst was computed.  net8.0-macos was computed.  net8.0-tvos was computed.  net8.0-windows was computed.  net9.0 was computed.  net9.0-android was computed.  net9.0-browser was computed.  net9.0-ios was computed.  net9.0-maccatalyst was computed.  net9.0-macos was computed.  net9.0-tvos was computed.  net9.0-windows was computed.  net10.0 was computed.  net10.0-android was computed.  net10.0-browser was computed.  net10.0-ios was computed.  net10.0-maccatalyst was computed.  net10.0-macos was computed.  net10.0-tvos was computed.  net10.0-windows was computed. 
Compatible target framework(s)
Included target framework(s) (in package)
Learn more about Target Frameworks and .NET Standard.
  • net8.0

    • No dependencies.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Adsefid.Sdk:

Package Downloads
Adsefid.Sdk.DependencyInjection

Microsoft.Extensions.DependencyInjection integration for Adsefid.Sdk.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.6.0 76 10/13/2026
0.5.0 114 9/12/2026
0.4.0 118 9/9/2026
0.3.0 125 9/8/2026
0.2.0 109 9/8/2026
0.1.0 116 9/7/2026