Stendly 0.2.1

dotnet add package Stendly --version 0.2.1
                    
NuGet\Install-Package Stendly -Version 0.2.1
                    
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="Stendly" Version="0.2.1" />
                    
For projects that support PackageReference, copy this XML node into the project file to reference the package.
<PackageVersion Include="Stendly" Version="0.2.1" />
                    
Directory.Packages.props
<PackageReference Include="Stendly" />
                    
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 Stendly --version 0.2.1
                    
#r "nuget: Stendly, 0.2.1"
                    
#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 Stendly@0.2.1
                    
#: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=Stendly&version=0.2.1
                    
Install as a Cake Addin
#tool nuget:?package=Stendly&version=0.2.1
                    
Install as a Cake Tool

Stendly .NET SDK

Non-custodial payments on Solana — official .NET SDK for Stendly API. Accept USDC payments in your .NET applications with 0% merchant fees, instant settlement, and no chargebacks.

NuGet version .NET License: MIT Documentation

Table of Contents


Features

  • 🔒 Secure: Webhook signature verification with constant-time comparison & replay attack protection
  • ⚡ Fast: HttpClient-based with connection pooling, automatic retries with exponential backoff
  • 🎯 Type-safe: Full C# nullable annotations and XML documentation
  • 🛡️ Robust: Comprehensive error handling with typed exception classes
  • 💪 Production-ready: .NET standard patterns with IStendlyClient interface for DI and mocking

Installation

dotnet add package Stendly

Or via Package Manager Console:

Install-Package Stendly

Or install from source:

git clone https://github.com/stendly-dev/dotnet-sdk.git
cd dotnet-sdk/
dotnet build

Requirements

  • .NET 8.0+ (.NET 10.0 recommended)

Quick Start

1. Get your API key

Log into your Stendly Dashboard and navigate to API Keys. Copy your secret key (starts with st_live_). Use environment: "devnet" for development and environment: "mainnet" for production.

2. Install the SDK

dotnet add package Stendly

3. Initialize the client

The constructor requires an HttpClient and apiKey:

using Stendly;

// Initialize with HttpClient and API key
var client = new StendlyClient(new HttpClient(), "st_live_your_api_key_here");

// Create a payment intent
var intent = await client.Intents.CreateIntentAsync(
    amountCents: 4999,     // $49.99
    orderId: "order_001"
);

Console.WriteLine($"Escrow address: {intent.ReferenceAddress}");
Console.WriteLine($"Destination: {intent.DestinationAddress}");
Console.WriteLine($"Expires at: {intent.ExpiresAt}");

// Check payment status
var retrieved = await client.Intents.RetrieveIntentAsync(intent.Id);
Console.WriteLine($"Status: {retrieved.Status}");

With dependency injection (ASP.NET Core):

// Program.cs
builder.Services.AddHttpClient<IStendlyClient, StendlyClient>((sp, client) =>
{
    var config = sp.GetRequiredService<IConfiguration>();
    return new StendlyClient(client, config["Stendly:ApiKey"]);
});

Authentication

API Key Format

Stendly uses secret API keys that start with st_live_.

Use the environment parameter to select the network:

  • environment: "mainnet" — Production
  • environment: "devnet" — Development/sandbox

Never commit API keys to version control!

// Good: Load from configuration
var client = new StendlyClient(new HttpClient(), configuration["Stendly:ApiKey"]);

// Good: Environment variable
var client = new StendlyClient(
    new HttpClient(),
    Environment.GetEnvironmentVariable("STENDLY_API_KEY")
);

// Bad: Hardcoded (DO NOT DO THIS)
var client = new StendlyClient(new HttpClient(), "st_live_xxxxx"); // ❌

Environment Selection

The SDK uses st_live_ key prefix for both environments. Set environment explicitly:

var client = new StendlyClient(new HttpClient(), "st_live_xxx", environment: "mainnet");
var client = new StendlyClient(new HttpClient(), "st_live_xxx", environment: "devnet");

Note: The same st_live_ key prefix is used for both environments. Set environment: "devnet" for development/testing.


Payment Intents

Creating a Payment Intent

var client = new StendlyClient(new HttpClient(), "st_live_...");

var intent = await client.Intents.CreateIntentAsync(
    amountCents: 4999,         // $49.99
    orderId: "PREMIUM-001"    // Your order reference
);

Console.WriteLine($"Escrow: {intent.ReferenceAddress}");
Console.WriteLine($"Payout to: {intent.DestinationAddress}");
Console.WriteLine($"Expires: {intent.ExpiresAt}");

Checking Payment Status

var intent = await client.Intents.RetrieveIntentAsync(intentId);
if (intent.Status == PaymentIntentStatus.Paid)
{
    DeliverGoods();
}

Using Terminals

var terminal = await client.Terminals.CreateTerminalAsync("Store Counter 1");

var intent = await client.Intents.CreateIntentAsync(
    amountCents: 1000,
    orderId: "walk-in-order",
    terminalId: terminal.Id
);

Payment Intent Lifecycle

PENDING → PAID
   ↓
EXPIRED (after 30 min)
   ↓
CANCELLED (manual)

Webhooks

Verifying Signatures

Critical: Always verify webhook signatures before processing. The method is async and expects byte[] payload.

using Stendly;
using Stendly.Exceptions;

var client = new StendlyClient(new HttpClient(), "st_live_...");
var webhookSecret = Environment.GetEnvironmentVariable("STENDLY_WEBHOOK_SECRET");

// ASP.NET Core controller
[HttpPost("webhooks/stendly")]
public async Task<IActionResult> HandleWebhook()
{
    var signature = Request.Headers["X-Stendly-Signature"].FirstOrDefault();
    if (string.IsNullOrEmpty(signature))
    {
        return BadRequest("Missing signature");
    }

    // Read raw body as bytes (not string)
    byte[] payload;
    using (var ms = new MemoryStream())
    {
        await Request.Body.CopyToAsync(ms);
        payload = ms.ToArray();
    }

    try
    {
        // ConstructEventAsync is async and takes byte[] payload
        var webhookEvent = await client.Webhooks.ConstructEventAsync(
            payload,
            signature,
            webhookSecret!
        );

        // webhookEvent.Event contains the event type string
        if (webhookEvent.Event == "payment_intent.succeeded")
        {
            FulfillOrder(webhookEvent.Data.OrderId, webhookEvent.Data.AmountCents);
        }

        return Ok();
    }
    catch (StendlySignatureVerificationException ex)
    {
        _logger.LogWarning(ex, "Invalid webhook signature");
        return BadRequest("Invalid signature");
    }
}

Webhook Signing (How It Works)

signature = HMAC-SHA256(secret, timestamp + payload)
header = "t={timestamp},v1={signature}"

Error Handling

All SDK errors inherit from StendlyException:

using Stendly;
using Stendly.Exceptions;

try
{
    var intent = await client.Intents.CreateIntentAsync(1000, "test");
}
catch (StendlyAuthenticationException ex)
{
    _logger.LogError(ex, "Auth failed");
}
catch (StendlyValidationException ex)
{
    _logger.LogWarning(ex, "Invalid input");
}
catch (StendlyRateLimitException ex)
{
    _logger.LogInformation("Rate limited");
}
catch (StendlyApiConnectionException ex)
{
    _logger.LogError(ex, "Network error");
}
catch (StendlyException ex)
{
    _logger.LogError(ex, "API error");
}

Exception Hierarchy

StendlyException (base)
├── StendlyAuthenticationException (401, 403)
├── StendlyValidationException (400)
├── StendlyRateLimitException (429)
├── StendlyApiConnectionException (network failures)
└── StendlySignatureVerificationException (webhook invalid)

API Reference

StendlyClient

Main entry point. Implements IStendlyClient.

Constructor
public StendlyClient(
    HttpClient httpClient,
    string apiKey,
    string environment = "mainnet",
    int maxRetries = 2
)
Parameter Type Default Description
httpClient HttpClient required HttpClient instance (from IHttpClientFactory or new)
apiKey string required Secret API key (st_live_*)
environment string "mainnet" API environment: "mainnet" or "devnet"
maxRetries int 2 Maximum retry attempts for transient failures
Properties
Property Type Description
Intents IIntentsClient Payment intent operations
Terminals ITerminalsClient POS terminal management
Webhooks IWebhooksClient Webhook configuration and verification
Merchant IMerchantClient Merchant account data

Namespaces

client.Intents

Methods:

CreateIntentAsync(int amountCents, string orderId, Guid? terminalId = null, string? idempotencyKey = null, CancellationToken cancellationToken = default)

Creates a new payment intent.

var intent = await client.Intents.CreateIntentAsync(4999, "order_vip_001");
RetrieveIntentAsync(Guid intentId, CancellationToken cancellationToken = default)

Fetches a payment intent by ID.

var intent = await client.Intents.RetrieveIntentAsync(
    Guid.Parse("123e4567-e89b-12d3-a456-426614174000")
);
Console.WriteLine(intent.Status);

client.Terminals

Methods:

CreateTerminalAsync(string name, CancellationToken cancellationToken = default)

Creates a new terminal.

var terminal = await client.Terminals.CreateTerminalAsync("Main Counter");
ListTerminalsAsync(CancellationToken cancellationToken = default)

Returns all terminals.

var terminals = await client.Terminals.ListTerminalsAsync();

client.Webhooks

Methods:

UpdateWebhookUrlAsync(string url, CancellationToken cancellationToken = default)

Updates webhook URL. Sends PATCH /api/b2b/merchants/webhook with {"webhookUrl": "..."}.

await client.Webhooks.UpdateWebhookUrlAsync("https://myshop.com/webhooks/stendly");

ConstructEventAsync(byte[] payload, string signatureHeader, string webhookSecret, int toleranceSeconds = 300, CancellationToken cancellationToken = default)

CRITICAL SECURITY METHOD: Verifies webhook signature. Async method.

  • payload (byte[], required): Raw request body as bytes.
  • signatureHeader (string, required): Value of X-Stendly-Signature header.
  • webhookSecret (string, required): Your webhook secret.
  • toleranceSeconds (int, optional): Max age in seconds (default 300).

Returns: Task<WebhookEvent>

  • webhookEvent.Event contains the event type string (e.g., "payment_intent.succeeded")
var webhookEvent = await client.Webhooks.ConstructEventAsync(
    payload: rawBodyBytes,
    signatureHeader: signature,
    webhookSecret: webhookSecret
);

if (webhookEvent.Event == "payment_intent.succeeded")
{
    Fulfill(webhookEvent.Data.OrderId);
}

client.Merchant

Methods:

GetProfileAsync(CancellationToken cancellationToken = default)

Retrieves merchant profile. Sends GET /api/b2b/merchants/me.

var profile = await client.Merchant.GetProfileAsync();
Console.WriteLine(profile.Name);
Console.WriteLine(profile.PayoutAddress);
GetStatsAsync(CancellationToken cancellationToken = default)

Returns 30-day statistics. Sends GET /api/b2b/merchants/stats.

var stats = await client.Merchant.GetStatsAsync();
Console.WriteLine($"Volume: ${stats.TotalVolumeCents / 100m:N2}");

Data Models

PaymentIntent
Property Type Description
Id Guid Unique intent ID
OrderId string Your order reference
ExpectedAmountCents int Expected amount (cents)
ReferenceAddress string Escrow Solana address
DestinationAddress string Merchant payout address
Status PaymentIntentStatus Enum: Pending, Paid, Expired, Cancelled, Underpaid
ExpiresAt DateTime Expiration timestamp (UTC)
WebhookEvent
Property Type Description
Event string Event name (e.g., "payment_intent.succeeded")
Data WebhookData Event payload

Integration Examples

ASP.NET Core Minimal API

using Stendly;

var builder = WebApplication.CreateBuilder(args);
var apiKey = builder.Configuration["Stendly:ApiKey"]!;
var client = new StendlyClient(new HttpClient(), apiKey);

var app = builder.Build();

app.MapPost("/api/intents", async (CreateIntentRequest request) =>
{
    var intent = await client.Intents.CreateIntentAsync(
        request.AmountCents,
        request.OrderId
    );
    return Results.Ok(new
    {
        Id = intent.Id.ToString(),
        ReferenceAddress = intent.ReferenceAddress,
        ExpiresAt = intent.ExpiresAt
    });
});

app.MapGet("/api/intents/{id:guid}", async (Guid id) =>
{
    var intent = await client.Intents.RetrieveIntentAsync(id);
    return Results.Ok(new { intent.OrderId, intent.Status, intent.ReferenceAddress });
});

app.Run();

public record CreateIntentRequest(int AmountCents, string OrderId);

Batch Operations

public async Task<List<PaymentIntent>> CreateBulkIntentsAsync(
    IEnumerable<(int AmountCents, string OrderId)> orders)
{
    var client = new StendlyClient(
        new HttpClient(),
        Environment.GetEnvironmentVariable("STENDLY_API_KEY")!
    );

    var tasks = orders.Select(o =>
        client.Intents.CreateIntentAsync(o.AmountCents, o.OrderId));

    var results = await Task.WhenAll(tasks);
    return results.ToList();
}

Production Checklist

  • API key stored in configuration or environment variable
  • Webhook secret stored securely
  • Webhook endpoint uses HTTPS
  • All webhooks verified (no exceptions)
  • Client registered as singleton in DI (connection pooling)
  • Proper error handling (catch StendlyException)
  • Logging configured (structured logs)
  • Timeout set appropriately (HttpClient.Timeout)
  • Tests run in CI/CD pipeline

Troubleshooting

Common Issues

Issue Solution
StendlyAuthenticationException Check API key format; regenerate if leaked
StendlyValidationException Validate input before API call
StendlyRateLimitException Implement backoff; respect Retry-After header
StendlyApiConnectionException Check internet; increase timeout; retry
Webhook verification fails Verify webhook secret; use raw byte[] payload; check clock sync

License

MIT License. See LICENSE.


Built with ❤️ for the Solana ecosystem.

Product Compatible and additional computed target framework versions.
.NET net10.0 is compatible.  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.
  • net10.0

    • No dependencies.

NuGet packages

This package is not used by any NuGet packages.

GitHub repositories

This package is not used by any popular GitHub repositories.

Version Downloads Last Updated
0.2.1 74 5/18/2026
0.2.0 74 5/17/2026
0.1.0 65 5/16/2026