ASP.NET Core
The FoPost .NET client wired into ASP.NET Core, with the options pattern, webhooks, and a health check.
FoPost.AspNetCore is a thin wrapper over the .NET SDK. It adds nothing to the API surface: no models, no HTTP, no retries, no error types. What it adds is the wiring an ASP.NET Core app expects: AddFoPost(…) with options validated at startup, an IHttpClientFactory client underneath, MapFoPostWebhook(…) with raw-body signature verification, and a health check that never mentions your key.
Needs .NET 8 or .NET 9.
dotnet add package FoPost.AspNetCoreFoPost.Sdk comes with it.
This is a 0.x release. The surface is still settling and a minor version may break something. Pin an exact version if that matters to you.
Published on NuGet. Source and issues: github.com/fopost/fopost-aspnet. MIT licensed.
Quick start
// Program.cs
var builder = WebApplication.CreateBuilder(args);
builder.Services.AddFoPost(); // binds the "FoPost" configuration section
builder.Services.AddFoPostHealthCheck(tags: ["ready"]);
var app = builder.Build();
app.MapHealthChecks("/health");
app.MapFoPostWebhook("/fopost/webhook");
app.MapGet("/workspaces", async (FoPostClient fopost, CancellationToken cancellationToken) =>
await fopost.Workspaces.ListAsync(cancellationToken));
app.Run();Configuration
{
"FoPost": {
"ApiKey": "fp_your_key_here",
"BaseUrl": "https://api.fopost.com",
"Timeout": "00:00:30",
"MaxRetries": 3,
"WebhookSecret": "whsec_your_secret_here"
}
}| Setting | Default | What it does |
|---|---|---|
ApiKey | FOPOST_API_KEY env var | Sent as X-API-Key. Required unless BearerToken is set |
BearerToken | none | Dashboard session token for the few endpoints that need one |
BaseUrl | https://api.fopost.com | API root. Override for another deployment |
Timeout | 00:00:30 | Per-request timeout |
MaxRetries | 3 | Attempts for a rate limited request, honouring Retry-After |
WebhookSecret | none | Verifies X-FoPost-Signature. Required only for MapFoPostWebhook |
Never commit the key. Use user-secrets in development and an environment variable in production: FoPost__ApiKey, or the SDK's own FOPOST_API_KEY, which is consulted when nothing is configured.
The options are validated with ValidateDataAnnotations().ValidateOnStart(), so a missing key or a malformed base URL fails the host at startup rather than on the first request.
Registering without configuration binding:
builder.Services.AddFoPost(options =>
{
options.ApiKey = keyFromYourVault;
options.MaxRetries = 5;
});
// or bind an explicit section, then adjust
builder.Services.AddFoPost(
builder.Configuration.GetSection("Integrations:FoPost"),
options => options.Timeout = TimeSpan.FromSeconds(10));Injecting the client
FoPostClient is registered as a singleton and behaves like any other service.
[ApiController]
[Route("posts")]
public sealed class PostsController : ControllerBase
{
private readonly FoPostClient _fopost;
public PostsController(FoPostClient fopost) => _fopost = fopost;
[HttpPost]
public async Task<IActionResult> Create(DraftRequest request, CancellationToken cancellationToken)
{
var post = await _fopost.Posts.CreateAsync(
new CreatePostOptions
{
WorkspaceId = request.WorkspaceId,
Content = [new PostContent(request.Text)],
Accounts = request.Accounts,
},
cancellationToken);
return Created($"/posts/{post.Id}", post);
}
}The client is built on a named IHttpClientFactory client, FoPostDefaults.HttpClientName, so you can decorate it:
builder.Services
.AddHttpClient(FoPostDefaults.HttpClientName)
.AddHttpMessageHandler<MyTracingHandler>();Because FoPostClient is a singleton it holds its HttpClient for the life of the process, which would defeat the factory's handler rotation. So the registration turns rotation off and sets SocketsHttpHandler.PooledConnectionLifetime to two minutes instead: connections are recycled and DNS changes are picked up, which is what the rotation was for.
Receiving webhooks
FoPost signs every delivery with HMAC-SHA256 over the raw request body and sends it as X-FoPost-Signature: sha256=<hex>, alongside X-FoPost-Event and X-FoPost-Delivery.
MapFoPostWebhook reads the raw bytes before anything can re-encode them, compares the digest in constant time, and answers 401 when it does not match, so no handler runs. The endpoint is anonymous by design: the signature is its authentication.
builder.Services.AddFoPostWebhookHandler<PublishedPostHandler>();
app.MapFoPostWebhook("/fopost/webhook");
internal sealed class PublishedPostHandler : IFoPostWebhookHandler
{
// Leave Events empty to receive every event.
public IReadOnlyCollection<string> Events =>
[FoPostWebhookEvents.PostPublished, FoPostWebhookEvents.PostFailed];
public Task HandleAsync(FoPostWebhookEvent webhookEvent, CancellationToken cancellationToken)
{
var postId = webhookEvent.Data?["post_id"]?.GetValue<string>();
// ...
return Task.CompletedTask;
}
}Handlers are scoped, so they may take a DbContext or anything else scoped, and they run in registration order. Throwing surfaces as a 500, which FoPost retries with backoff, so throw when the work should be retried and swallow when it should not. DeliveryId is unique per send and is what to key on for idempotency.
Events: post.published, post.failed, post.partially_failed, delivery.published, delivery.failed, delivery.delayed, account.health_changed, all on FoPostWebhookEvents.
Health checks
builder.Services.AddFoPostHealthCheck(tags: ["ready"]);
app.MapHealthChecks("/health/ready", new HealthCheckOptions
{
Predicate = registration => registration.Tags.Contains("ready"),
});The check lists the workspaces the credential can see: the cheapest authenticated call the API offers, so it proves reachability and the credential in one request. A 429 reports Degraded; anything else reports the registered failure status. Descriptions never include the key, the base URL, or a raw provider message.
Errors and the rest of the API
All of it lives in FoPost.Sdk and is unchanged here: the FoPostException hierarchy (FoPostValidationException, FoPostAuthenticationException, FoPostPaymentRequiredException, FoPostPermissionDeniedException, FoPostNotFoundException, FoPostRateLimitException), automatic retries on 429, the Posts, Accounts, Workspaces, Labels and Ai resources, and RequestAsync for endpoints the SDK does not wrap yet. See the .NET SDK page.
Next
Related documentation
- SDKs & Integrations
Official FoPost clients for TypeScript, Python, PHP, Ruby, Go, Rust, Java, .NET, Swift, Kotlin, Dart, and Elixir, framework integrations from Laravel to Next.js, and tooling for the CLI, CI, Terraform, and the automation platforms.
- SDKs Overview
Every official FoPost client, what it covers, and how to pick one.
- TypeScript SDK
The official TypeScript and Node.js client for the FoPost API.
- Python SDK
The official Python client for the FoPost API.
- PHP SDK
The official PHP client for the FoPost API, with no framework and no HTTP library.
- Ruby SDK
The official Ruby client for the FoPost API, with no runtime dependencies.
- Go SDK
The official Go client for the FoPost API.
- Rust SDK
The official Rust client for the FoPost API, async and built on reqwest.