FoPost.AspNetCore 0.2.0

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

FoPost for ASP.NET Core

NuGet Downloads CI License

Official ASP.NET Core integration for the FoPost API. Schedule and publish to +30 social platforms from your code.

This package is a thin wrapper around FoPost.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:

  • services.AddFoPost(…) — the options pattern, validated at startup, client registered in DI
  • an IHttpClientFactory client under the hood, so handlers and DNS behave
  • app.MapFoPostWebhook("/fopost/webhook") — raw-body signature verification and typed handlers
  • app.MapFoPostArticles("/fopost/articles") — receives published blog articles on your website
  • services.AddFoPostHealthCheck() — a health check that never mentions your key

Requirements

Installation

dotnet add package FoPost.AspNetCore

FoPost.Sdk comes with it.

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 staging or a self-hosted 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
Blog:WebhookSecret none The website secret from the blog settings page. Required only for MapFoPostArticles

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);
    }
}

HTTP client

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>, plus X-FoPost-Signature-256 over <timestamp>.<body> with X-FoPost-Timestamp, alongside X-FoPost-Event and X-FoPost-Delivery.

MapFoPostWebhook reads the raw bytes before anything can re-encode them and verifies them through FoPostWebhooks.Verify from FoPost.Sdk: the timestamped signature is preferred and refused when more than 300 seconds old, the plain one is accepted only when no timestamp was sent. Anything that does not verify is a 401 and 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. 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.

Receive blog articles

Set your site as the blog's website target in the dashboard and paste the website secret into FoPost:Blog:WebhookSecret. Then mount the receiver and register what to do with an article:

builder.Services.AddFoPostArticleHandler(async (article, cancellationToken) =>
{
    // Store article.Title, article.Slug, article.BodyHtml, article.Images, …
    // Image URLs are fetchable for six hours, so download your own copies here.
    return $"https://yourbrand.com/blog/{article.Slug}";
});

app.MapFoPostArticles("/fopost/articles");

The endpoint verifies each delivery with FoPostWebhooks.Verify, answers 401 on a bad signature, 200 { "ok": true } for the dashboard's connection test (article.test), and hands article.published to your handler as a BlogArticlePublishedPayload. Return the URL the article now lives at and the receiver echoes it as { "url": "…" }; return null for { "ok": true }. A class works too: implement IFoPostArticlePublishedHandler and register it with AddFoPostArticleHandler<T>().

The blog API itself (settings, keywords, articles) is FoPostClient.Blog from FoPost.Sdk; the injected client already exposes it.

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, retries, and the rest of the API

All of that 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 / Ai resources, and RequestAsync for endpoints the SDK does not wrap yet.

See the FoPost.Sdk README for the full API surface, and fopost.com/docs for the product documentation.

Examples

examples/MinimalApi is a runnable app that registers the client, exposes a health endpoint, receives webhooks, and creates a draft post.

Support

Open an issue at fopost/fopost-aspnet, or use the contact form at fopost.com/contact.

Contributing

dotnet build
dotnet test
dotnet pack src/FoPost.AspNetCore/FoPost.AspNetCore.csproj -c Release

License

MIT

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 is compatible.  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.

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.0 62 10/18/2026
0.1.1 123 8/31/2026
0.1.0 103 8/31/2026