Paperfly.Courier 1.0.1

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

Paperfly.Courier

ci NuGet License: MIT

Unofficial, community-maintained SDK for the Paperfly courier API. Not affiliated with, or endorsed by, Paperfly.

A .NET SDK (net10.0) for the Paperfly Courier API: typed clients for order creation (regular and exchange), cancellation by merchant order reference, and tracking that normalizes the sparse milestone-column matrix into ordered milestones with derived state — authenticated with Basic auth (merchant-panel credentials) plus the shared paperflykey header. Zero third-party runtime dependencies beyond BCL + Microsoft.Extensions.* abstractions.

Status: v0.1.0-alpha — all five documented operations, DI/multi-merchant factory, examples and release engineering are complete; see TRACKER.md.

Install

dotnet add package Paperfly.Courier --prerelease

Prerelease until 1.0 — versions derive from git tags (v0.1.0-alpha.1, …) and publish from the releases workflow.

Quickstart

using Paperfly.Courier;
using Paperfly.Courier.Configuration;
using Paperfly.Courier.Models.Orders;

using var client = new PaperflyCourierClient(new PaperflyClientOptions
{
    Username = Environment.GetEnvironmentVariable("PAPERFLY__USERNAME"),  // merchant panel login
    Password = Environment.GetEnvironmentVariable("PAPERFLY__PASSWORD"),
});

OrderCreated order = await client.Orders.CreateAsync(new CreateOrderRequest
{
    MerchantOrderReference = "Test_01610",
    StoreName = "Ovi",
    ProductBrief = "Test Product",
    PackagePrice = 10m,
    MaxWeightKg = 0.3m,
    CustomerName = "Liton Ovi",
    CustomerAddress = "Banani, Dhaka",
    CustomerPhone = "01610202717",
});

// persist ALL THREE identifiers (see "Split identity" below)
Console.WriteLine(order.TrackingNumber);

TrackingInfo status = await client.Tracking.TrackAsync("Test_01610");
Console.WriteLine(status.Latest);

DI and per-merchant factory usage: Dependency injection. A runnable walkthrough lives in examples/Paperfly.Courier.ConsoleDemo.

API coverage

Every documented operation, no more (undocumented endpoints are deliberately absent):

SDK call HTTP Endpoint Live status
Orders.CreateAsync POST /merchant/api/service/new_order_v2.php verified
Orders.CreateExchangeAsync POST /merchant/api/service/new_order_v2.php (orderType: "Exchange") verified
Tracking.TrackAsync POST /API-Order-Tracking verified
Orders.CancelAsync POST /api/v1/cancel-order documented path 404s server-side — see Cancellation

Quirks the SDK normalizes

  • Basic auth with panel credentials — the username/password are the merchant panel login, not API-only keys; the SDK computes the Authorization header per request and never logs it.
  • paperflykey shared header — documented for order creation only, but live verification showed the API demands it everywhere; attached to every request, overridable per client.
  • Four JSON naming conventions in one API — camelCase requests with a snake_case max_weight straggler, a PascalCase ReferenceNumber tracking key, a snake_case order_id cancel key, and mixed responses; every wire name is pinned explicitly.
  • String-typed numerics — packagePrice: "10", max_weight: "0.3": the SDK takes decimal and serializes the documented wire form.
  • Milestone-column tracking matrix — one object whose fields are milestone value/time columns where "not happened" is null or ""; normalized into ordered milestones with nullable timestamps and derived state (details in Tracking).
  • Split identity model — create returns a tracking number + barcode, but tracking and cancellation are keyed by your MerchantOrderReference only: persist all three at creation.

Orders

Creating an order

using Paperfly.Courier.Configuration;
using Paperfly.Courier.Models.Orders;
using Paperfly.Courier.Orders;

using var client = new PaperflyOrdersClient(
    new HttpClient(),
    new PaperflyClientOptions
    {
        BaseUrl = PaperflyUrls.Production,  // https://api.paperfly.com.bd
        Username = Environment.GetEnvironmentVariable("PAPERFLY__USERNAME"),
        Password = Environment.GetEnvironmentVariable("PAPERFLY__PASSWORD"),
    });

OrderCreated order = await client.CreateAsync(new CreateOrderRequest
{
    MerchantOrderReference = "Test_01610",
    StoreName = "Ovi",
    ProductBrief = "Test Product",
    PackagePrice = 10m,
    MaxWeightKg = 0.3m,
    CustomerName = "Liton Ovi",
    CustomerAddress = "Banani, Dhaka",
    CustomerPhone = "01610202717",
});

Console.WriteLine(order.TrackingNumber);   // Z-051125-63821-A3-A1
Console.WriteLine(order.TrackingBarcode);  // 751820115459

Every request is authenticated with Basic auth (merchant panel username/password) and additionally carries the shared paperflykey header, whose documented value is the SDK default and can be overridden via PaperflyClientOptions.PaperflyKey.

Property Wire field Rules
MerchantOrderReference merchantOrderReference Required, non-empty; must be unique — yours to guarantee
StoreName storeName Required, non-empty; free text (no store registry exists)
ProductBrief productBrief Required, non-empty
PackagePrice packagePrice Required, BDT, ≥ 0
MaxWeightKg max_weight Required, kilograms, > 0
CustomerName customerName Required, non-empty
CustomerAddress customerAddress Required, non-empty
CustomerPhone customerPhone Required, exactly 11 digits (e.g. 01610202717)

All rules are enforced client-side (PaperflyValidationException, listing every violation) before any HTTP call. Note the API's mixed field naming — camelCase with the snake_case max_weight straggler — and that numerics travel as strings ("10", "0.3"); the SDK takes decimal and serializes the documented wire form for you.

Exchange orders

An exchange order is the same operation with orderType: "Exchange" and the exchange fields merged into the payload. The API's field table spells these snake_case (exchange_description, …) but its own sample payload uses camelCase — the SDK mirrors the sample (orderType, exchangeDescription, exchangePrice, exchangeWeight), pinned by byte-exact snapshot tests and live-verified per the implementation tracker.

OrderCreated exchange = await client.CreateExchangeAsync(
    orderRequest,
    new ExchangeDetails
    {
        Description = "exchange product",
        Price = 100m,     // BDT, ≥ 0
        WeightKg = 1.5m,  // > 0
    });

Persist all three identifiers

Creation returns TrackingNumber and TrackingBarcode, but tracking and cancellation are keyed by your MerchantOrderReference only. Persist the reference together with the returned tracking number and barcode when the order is created.

Tracking

The milestone matrix, normalized

The tracking endpoint (POST /API-Order-Tracking — a POST carrying a PascalCase ReferenceNumber body) does not return a status list. It returns one object whose fields are milestone columns — a value column and a time column per milestone — where "not happened" is encoded as either null or "" (the API mixes both):

{ "Pick": null, "PickTime": null, "inTransit": "", "inTransitTime": "", …,
  "onHoldSchedule": "", "close": "", "closeTime": "" }

TrackAsync flattens this sparse matrix into a TrackingInfo:

using Paperfly.Courier.Tracking;

TrackingInfo info = await tracking.TrackAsync("Test_01610");

info.Milestones;        // reached milestones, chronologically ordered
info.Latest;            // the newest reached milestone (null when none yet)
info.DeliveredAt;       // nullable DateTimeOffset per milestone (PickedAt, InTransitAt, …)
info.OnHoldSchedule;    // bool — the API defines no timestamp for this milestone
info.ReceivedAmount;    // decimal? — collected BDT ("" → null)
info.InvoiceNumber;     // string? — invoice number ("" → null)
info.IsDelivered;       // derived helpers: IsDelivered / IsReturned / IsClosed

Normalization rules:

  • Reached = the milestone's value column or time column is non-empty (null and "" both mean "not happened"). A milestone can be reached with a value only, a timestamp only, or both — real matrices populate the columns inconsistently.
  • Timestamps parse to DateTimeOffset?; naive values are interpreted as UTC. An unparseable timestamp becomes null but still marks the milestone reached.
  • Ordering: Milestones sorts by timestamp ascending; timestamp-less milestones (only OnHoldSchedule is documented without a time column) sort after dated ones, and equal timestamps break ties by the canonical progression order (Pick → InTransit → ReceivedAtPoint → PickedForDelivery → Delivered → Returned → Partial → OnHoldSchedule → Close).
  • Undocumented shapes are tolerated, not guessed at: a missing/empty trackingStatus list normalizes to zero reached milestones; when multiple rows are returned, the first is used.

Cancellation

CancelResult result = await client.CancelAsync("Test_01610");
Console.WriteLine(result.Message);  // e.g. "succesfully canceled" (sic — API message)

Cancellation posts {"order_id": "<reference>"} to /api/v1/cancel-order, keyed by your merchant order reference. Failure throws: a PaperflyApiException (HTTP status + best-effort message + raw body) — success is never inferred from the API's message text, and Message is informational only.

Note on paperflykey: the API documentation lists the header for order creation only, but live verification showed the API rejects tracking without it (400 Please provide all information in the header). The SDK therefore attaches paperflykey to every request; the value remains overridable via PaperflyClientOptions.PaperflyKey in case Paperfly rotates it.

Live status of cancellation (2026-08-23): the documented endpoint /api/v1/cancel-order returns an Apache 404 on the live server (probed across every plausible sibling path — none exists). The SDK implements the documented wire contract faithfully; until Paperfly publishes a working path, live cancellation cannot be verified and fails with a PaperflyApiException (404 + raw HTML body). Order creation and tracking are live-verified and working.

Dependency injection

Client facade

IPaperflyCourierClient composes both resource clients (Orders, Tracking) over one HttpClient. For direct usage, the options-only constructor owns (and disposes) its HttpClient; when an HttpClient is injected (DI, IHttpClientFactory), disposal is the owner's business:

using Paperfly.Courier;

using var client = new PaperflyCourierClient(new PaperflyClientOptions
{
    Username = Environment.GetEnvironmentVariable("PAPERFLY__USERNAME"),
    Password = Environment.GetEnvironmentVariable("PAPERFLY__PASSWORD"),
});

OrderCreated order = await client.Orders.CreateAsync(orderRequest);
TrackingInfo status = await client.Tracking.TrackAsync(orderRequest.MerchantOrderReference);

Single account

AddPaperflyCourier registers the facade as a typed client through IHttpClientFactory (named client PaperflyCourier). Options are validated on host start — username, password and a non-empty PaperflyKey are required, and the base URL must be an absolute http(s) URL — so a misconfigured app fails at boot instead of at the first order.

using Paperfly.Courier;
using Paperfly.Courier.Extensions;

builder.Services.AddPaperflyCourier(options =>
{
    options.Username = builder.Configuration["Paperfly:Username"];
    options.Password = builder.Configuration["Paperfly:Password"];
});

// inject IPaperflyCourierClient anywhere
public class CheckoutService(IPaperflyCourierClient paperfly)
{
    public Task<OrderCreated> PlaceOrder(CreateOrderRequest request)
        => paperfly.Orders.CreateAsync(request);
}

The underlying HTTP pipeline can be customized through the shared named client:

builder.Services.AddHttpClient(PaperflyCourierServiceCollectionExtensions.HttpClientName)
    .ConfigureHttpClient(client => client.Timeout = TimeSpan.FromSeconds(30));

Multi-merchant (per-tenant credentials)

Basic auth has no token lifecycle, so per-merchant clients are stateless: create them on demand from IPaperflyCourierClientFactory with each merchant's panel credentials. The factory rents HttpClients from IHttpClientFactory and holds no merchant state itself.

using Paperfly.Courier.Configuration;

builder.Services.AddPaperflyCourierFactory();

public class FulfillmentService(IPaperflyCourierClientFactory factory)
{
    public async Task<OrderCreated> Ship(Merchant merchant, CreateOrderRequest request)
    {
        IPaperflyCourierClient client = factory.Create(new PaperflyClientOptions
        {
            Username = merchant.PaperflyUsername,
            Password = merchant.PaperflyPassword,
        });
        return await client.Orders.CreateAsync(request);
    }
}

Credential management

The username and password are the merchant panel login — full account credentials, not API-only keys:

  1. Keep them out of source control: environment variables, user secrets or your platform's secret store, flowing into the app as env vars → configuration section → integration layer. Never commit them, never paste them into logs or error trackers.
  2. Where Paperfly allows it, create a dedicated merchant user for API access so human panel logins and API credentials can be rotated independently.
  3. The SDK never logs credentials and never includes them in exception messages; the Basic Authorization header is computed per request and never cached.
  4. PaperflyKey is a vendor-published shared constant (not a merchant secret): it defaults to the documented value and can be overridden per client if Paperfly rotates it.

Examples

examples/Paperfly.Courier.ConsoleDemo is a menu-driven console app walking through the full lifecycle: create an order (regular or exchange), track it by merchant reference, cancel it.

cd examples/Paperfly.Courier.ConsoleDemo
dotnet user-secrets set "PAPERFLY:USERNAME" "your-merchant-panel-username"
dotnet user-secrets set "PAPERFLY:PASSWORD" "your-merchant-panel-password"
dotnet run

(Credentials alternatively come from the PAPERFLY__USERNAME/PAPERFLY__PASSWORD environment variables, matching the integration-test convention.) Every side-effect action shows a summary and asks for confirmation before anything is sent — there is no sandbox, so creates/cancels are real.

Documented API gaps (out of scope)

The API documentation specifies only the four endpoints above. Consequently — and deliberately — the SDK does not include:

  • Webhooks — status comes only from polling TrackAsync; consumers own scheduling.
  • Bulk order creation, area/coverage lists, pricing calculators and balance endpoints — undocumented, absent.
  • Sandbox — the base URL always points at production and must stay config-driven.
  • Rate limits / retries — none are documented, so the SDK ships no built-in retry or throttling; wrap calls with your own resilience policy if you need one.

Contributing

Issues and pull requests are welcome at boniky-dev/paperfly-courier-sdk. Keep dotnet build -c Release at zero warnings and dotnet test green (live tests stay opt-in), never commit credentials, and pin any wire-format change with snapshot tests.

License

MIT — see LICENSE.

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.

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
1.0.1 109 8/23/2026
1.0.0 104 8/23/2026
0.1.0-alpha.1 73 8/23/2026