Paperfly.Courier
1.0.1
dotnet add package Paperfly.Courier --version 1.0.1
NuGet\Install-Package Paperfly.Courier -Version 1.0.1
<PackageReference Include="Paperfly.Courier" Version="1.0.1" />
<PackageVersion Include="Paperfly.Courier" Version="1.0.1" />
<PackageReference Include="Paperfly.Courier" />
paket add Paperfly.Courier --version 1.0.1
#r "nuget: Paperfly.Courier, 1.0.1"
#:package Paperfly.Courier@1.0.1
#addin nuget:?package=Paperfly.Courier&version=1.0.1
#tool nuget:?package=Paperfly.Courier&version=1.0.1
Paperfly.Courier
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
Authorizationheader per request and never logs it. paperflykeyshared 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_weightstraggler, a PascalCaseReferenceNumbertracking key, a snake_caseorder_idcancel key, and mixed responses; every wire name is pinned explicitly. - String-typed numerics —
packagePrice: "10",max_weight: "0.3": the SDK takesdecimaland serializes the documented wire form. - Milestone-column tracking matrix — one object whose fields are milestone value/time
columns where "not happened" is
nullor""; 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
MerchantOrderReferenceonly: 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 (
nulland""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 becomesnullbut still marks the milestone reached. - Ordering:
Milestonessorts by timestamp ascending; timestamp-less milestones (onlyOnHoldScheduleis 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
trackingStatuslist 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 attachespaperflykeyto every request; the value remains overridable viaPaperflyClientOptions.PaperflyKeyin case Paperfly rotates it.
Live status of cancellation (2026-08-23): the documented endpoint
/api/v1/cancel-orderreturns 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 aPaperflyApiException(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:
- 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.
- Where Paperfly allows it, create a dedicated merchant user for API access so human panel logins and API credentials can be rotated independently.
- The SDK never logs credentials and never includes them in exception messages; the Basic
Authorizationheader is computed per request and never cached. PaperflyKeyis 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 | Versions 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. |
-
net10.0
- Microsoft.Extensions.DependencyInjection.Abstractions (>= 10.0.11)
- Microsoft.Extensions.Http (>= 10.0.11)
- Microsoft.Extensions.Options (>= 10.0.11)
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 |