Pathao.Courier 1.0.0

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

Pathao.Courier

CI NuGet License: MIT

An unofficial, community-maintained .NET SDK (net10.0) for the Pathao Courier Merchant API — typed clients for stores, orders, pricing and locations with automatic OAuth 2.0 token handling.

Disclaimer: This project is not affiliated with, endorsed by, or sponsored by Pathao. "Pathao" is a trademark of its respective owner. This is an independent, open-source SDK released under the MIT license.

Status

🚀 v0.1.0-alpha — feature-complete for v1: all 11 documented Merchant API endpoints are covered (OAuth 2.0 token lifecycle, city/zone/area reference data, delivery price estimation, merchant store management, consignment order creation — single, bulk and status info) plus the ASP.NET Core DI experience (single-account registration + per-merchant factory) and a runnable console demo. The public API may still change without notice until v1.0.

Installation

dotnet add package Pathao.Courier --prerelease

or reference the package directly (version 0.1.0-alpha.1 or later):

<PackageReference Include="Pathao.Courier" Version="0.1.0-alpha.1" />

Requires net10.0. Runtime dependencies are BCL-only apart from Microsoft.Extensions.Http/Microsoft.Extensions.Options (DI convenience); JSON is handled by source-generated System.Text.Json (trimming/AOT friendly).

Quickstart

using var client = new PathaoCourierClient(new PathaoClientOptions
{
    BaseUrl = PathaoUrls.Sandbox,          // or PathaoUrls.Production
    ClientId = "...",
    ClientSecret = "...",
    Username = "...",
    Password = "..."
});

Authentication & token lifecycle

The SDK implements the full Pathao OAuth 2.0 story; you normally never touch tokens yourself:

  • Lazy acquisition — the first API call (and only the first) requests an access token via the password grant against /aladdin/api/v1/issue-token.
  • Persistence — tokens are kept in an ITokenStore. The default is a thread-safe in-memory store; supply your own implementation (e.g. database-backed) to survive restarts or share tokens across processes. Tokens are keyed by a stable hash of the base URL, client id and username, so clients created with the same credentials reuse the same token.
  • Proactive refresh — the SDK tracks absolute expiry from the issued expires_in value (documented as 432000 s / 5 days; the live sandbox currently grants 90 days) and refreshes via the refresh-token grant 5 minutes before real expiry.
  • Single-flight — under concurrent requests, only one caller performs an acquire/refresh; the rest wait and reuse the result.
  • Reactive retry — if the API still answers HTTP 401, the token is refreshed once and the request retried once (request bodies are buffered so resending is safe). A second 401 surfaces as a PathaoApiException.
  • Errors — token-endpoint failures throw PathaoAuthException (a specialization of PathaoApiException) carrying the HTTP status, best-effort business message and raw body.

Explicit control is available when you need it:

PathaoToken token = await client.Auth.IssueTokenAsync();    // password grant
PathaoToken fresh = await client.Auth.RefreshTokenAsync();  // refresh grant

Security note: credentials and tokens are only ever sent in the token request body and the Authorization header respectively — never in URLs, never logged.

Dependency injection (single account)

For ASP.NET Core (or any Microsoft.Extensions.DependencyInjection host), register the client once; options are validated at host start-up and every resolution gets a fresh HttpClient from IHttpClientFactory (named PathaoCourier — append handlers to that name to extend the pipeline):

builder.Services.AddPathaoCourier(options =>
{
    options.BaseUrl = PathaoUrls.Production;
    options.ClientId = "...";
    options.ClientSecret = "...";
    options.Username = "...";
    options.Password = "...";
});

// inject anywhere
public class ShippingService(IPathaoCourierClient pathao)
{
    public Task<PagedResult<StoreInfo>> MyStoresAsync() => pathao.Stores.ListAsync();
}

Multi-merchant factory (per-tenant credentials)

When every merchant brings its own Pathao account, register the factory instead and create clients from per-merchant credentials at request time:

builder.Services.AddPathaoCourierFactory();

public class FulfillmentService(IPathaoCourierClientFactory factory)
{
    public async Task CreateConsignmentAsync(MerchantCredentials creds)
    {
        using var client = factory.Create(new PathaoClientOptions
        {
            BaseUrl = PathaoUrls.Production,
            ClientId = creds.ClientId,
            ClientSecret = creds.ClientSecret,
            Username = creds.Username,
            Password = creds.Password
        });

        await client.Orders.CreateAsync(BuildOrder());
    }
}

Both registrations share one ITokenStore singleton: clients created with the same credentials (across the factory, the DI container, or both) reuse the same cached token — tokens are keyed by a stable hash of base URL + client id + username, never by the secret.

Token persistence (database-backed ITokenStore)

The SDK performs no persistence of its own. Register your own store before calling AddPathaoCourier/AddPathaoCourierFactory and every client picks it up:

builder.Services.AddSingleton<ITokenStore>(new DbTokenStore(connectionString));

// sketch of a database-backed implementation
public sealed class DbTokenStore(string connectionString) : ITokenStore
{
    public async Task<PathaoToken?> GetAsync(string credentialKey, CancellationToken ct = default)
    {
        // SELECT token_json FROM pathao_tokens WHERE credential_key = @credentialKey
        // deserialize and return, or null when absent
    }

    public async Task SaveAsync(string credentialKey, PathaoToken token, CancellationToken ct = default)
    {
        // UPSERT pathao_tokens SET token_json = @token WHERE credential_key = @credentialKey
    }
}

Store the serialized token (access, refresh, type, expires_in, acquired_at_utc) and treat credentialKey as an opaque string. With a shared store, tokens survive restarts and are deduplicated across processes; the SDK refreshes them proactively (5-minute skew) and reactively (401 → refresh once → retry) as needed.

Locations

City, zone and area reference data — the building blocks for addresses, stores and orders:

IReadOnlyList<City> cities = await client.Locations.GetCitiesAsync();
City dhaka = cities.First(c => c.CityName == "Dhaka");

IReadOnlyList<Zone> zones = await client.Locations.GetZonesAsync(dhaka.CityId);
IReadOnlyList<Area> areas = await client.Locations.GetAreasAsync(zones[0].ZoneId);
// areas[0].HomeDeliveryAvailable / areas[0].PickupAvailable tell you what service is possible there

Pricing

Estimate the delivery price of a prospective consignment before creating an order:

PricePlan plan = await client.Pricing.EstimateAsync(new PricePlanRequest
{
    StoreId = 150728,
    ItemType = ItemType.Parcel,
    DeliveryType = DeliveryType.Normal,
    ItemWeight = 0.5m,
    RecipientCity = dhaka.CityId,
    RecipientZone = zones[0].ZoneId
});
// plan.Price, plan.Discount, plan.CodEnabled, plan.CodPercentage, plan.FinalPrice, ...

Quirk handled for you: the price-plan endpoint sends item_weight as a JSON number, while the order endpoints send it as a string — the SDK serializes each form correctly for the endpoint in use. cod_enabled arrives from the API as 1/0 and is normalized to a boolean. item_weight is pre-validated (0.5–10 kg) before any HTTP call.

Stores

Create merchant stores and list the merchant's current stores:

StoreCreated created = await client.Stores.CreateAsync(new CreateStoreRequest
{
    Name = "Demo Store",
    ContactName = "Test Merchant",
    ContactNumber = "01700000000",
    Address = "House 123, Road 4, Sector 10, Uttara, Dhaka-1230, Bangladesh",
    CityId = dhaka.CityId,
    ZoneId = zones[0].ZoneId,
    AreaId = areas[0].AreaId,
    SecondaryContact = "01500000000",   // optional; omitted from the payload when null
    OtpNumber = null                    // optional; omitted from the payload when null
});

Newly created stores require about one hour of Pathao approval before they can be used for orders. Listing is paginated Laravel-style; the SDK normalizes the response into a PagedResult<StoreInfo>:

PagedResult<StoreInfo> page1 = await client.Stores.ListAsync();       // page defaults to 1
PagedResult<StoreInfo> page2 = await client.Stores.ListAsync(page: 2);
// page1.Items / .Page / .PerPage / .TotalPages / .TotalCount

Requests are pre-validated offline before any HTTP call: Name/ContactName 3–50 characters, Address 15–120 characters, and phone fields (ContactNumber, and SecondaryContact/ OtpNumber when supplied) exactly 11 digits. is_active/is_default_store/ is_default_return_store arrive from the API as 1/0 (or booleans) and are normalized.

Orders

Create consignments, submit them in bulk, and read back short status info:

OrderCreationResult created = await client.Orders.CreateAsync(new CreateOrderRequest
{
    StoreId = 150728,
    MerchantOrderId = "M-42",                     // optional; omitted from the payload when null
    RecipientName = "Demo Recipient",
    RecipientPhone = "01700000000",
    RecipientAddress = "House 123, Road 4, Sector 10, Uttara, Dhaka-1230, Bangladesh",
    RecipientCity = dhaka.CityId,                 // optional — the API derives it from the
    RecipientZone = zones[0].ZoneId,              //           address when omitted
    DeliveryType = DeliveryType.Normal,           // 48 Normal / 12 OnDemand
    ItemType = ItemType.Parcel,                   // 1 Document / 2 Parcel
    ItemQuantity = 1,
    ItemWeight = 0.5m,                            // serialized as "0.5" per the order API
    ItemDescription = "this is a Cloth item, price- 3000",
    AmountToCollect = 900                         // defaults to 0 for non-COD orders
});
// created.ConsignmentId / .MerchantOrderId / .OrderStatus / .DeliveryFee

OrderInfo info = await client.Orders.GetInfoAsync(created.ConsignmentId);
// info.OrderStatus, .OrderStatusSlug, .UpdatedAt, .InvoiceId, ...

Requests are pre-validated offline before any HTTP call: RecipientName 3–100 characters, RecipientAddress 10–220 characters, phone fields exactly 11 digits, ItemWeight 0.5–10 kg, and AmountToCollect non-negative. Optional fields (RecipientCity/RecipientZone/ RecipientArea, MerchantOrderId, RecipientSecondaryPhone, SpecialInstruction, ItemDescription) are omitted from the payload when null — the API requires nulls to be absent, not sent. Identifier fields that PHP APIs sometimes emit unquoted (consignment_id, merchant_order_id, invoice_id) are normalized to strings.

Bulk quirk: CreateBulkAsync answers HTTP 202 with acceptance only — no consignment ids are returned and creation completes asynchronously, so per-order results must be reconciled later. Use single-order creation when you need the ids immediately:

BulkOrderAccepted accepted = await client.Orders.CreateBulkAsync([order1, order2]);

API coverage

All 11 endpoints documented for the Pathao Courier Merchant API:

# Endpoint SDK surface
1 POST /aladdin/api/v1/issue-token (password grant) Auth.IssueTokenAsync + automatic via the token lifecycle
2 POST /aladdin/api/v1/issue-token (refresh grant) Auth.RefreshTokenAsync + automatic refresh/retry
3 POST /aladdin/api/v1/stores Stores.CreateAsync
4 POST /aladdin/api/v1/orders Orders.CreateAsync
5 POST /aladdin/api/v1/orders/bulk Orders.CreateBulkAsync
6 GET /aladdin/api/v1/orders/{id}/info Orders.GetInfoAsync
7 GET /aladdin/api/v1/city-list Locations.GetCitiesAsync
8 GET /aladdin/api/v1/cities/{id}/zone-list Locations.GetZonesAsync
9 GET /aladdin/api/v1/zones/{id}/area-list Locations.GetAreasAsync
10 POST /aladdin/api/v1/merchant/price-plan Pricing.EstimateAsync
11 GET /aladdin/api/v1/stores Stores.ListAsync

API quirks the SDK normalizes

  • Envelope unwrapping — every business response is { message, type, code, data }; the SDK unwraps data and surfaces message on typed errors (PathaoApiException with status, API code/message and the raw body; undocumented error shapes never crash parsing).
  • snake_case wire JSON mapped explicitly via source generation — no runtime reflection.
  • Nulls are omitted, not sent — optional fields (recipient_city/recipient_zone/ recipient_area, secondary_contact, otp_number, recipient_secondary_phone, special_instruction, item_description, merchant_order_id) are absent from payloads when null.
  • item_weight dual typing — a string ("0.5") for order creation, a number (0.5) for price plans; the SDK serializes the correct form per endpoint (typed decimal either way).
  • Bulk 202 — bulk creation answers HTTP 202 with acceptance only and no consignment ids; per-order results must be reconciled later. Use single-order creation when you need ids now.
  • Laravel pagination on store lists normalized into PagedResult<T>.
  • PHP-style booleans/identifiers — 1/0 flags and unquoted id fields are read tolerantly.
  • Offline pre-validation — documented field ranges (name/address lengths, 11-digit phones, 0.5–10 kg weight, non-negative amounts) are checked before any HTTP call and throw PathaoValidationException.

Console demo

examples/Pathao.Courier.ConsoleDemo is a runnable, menu-driven tour of the whole SDK against the sandbox: issue a token, browse cities → zones → areas, estimate a delivery price, create an order with a synthetic recipient and poll its status.

cd examples/Pathao.Courier.ConsoleDemo
dotnet run

It reads the same environment variables as the sandbox test suite (PATHAO_SANDBOX__CLIENT_ID, __CLIENT_SECRET, __USERNAME, __PASSWORD, optional __STORE_ID) or user secrets (prefix Pathao:Sandbox:, e.g. dotnet user-secrets set Pathao:Sandbox:ClientId ...).

Security notes

  • Credentials and tokens are only ever sent in the token-request body and the Authorization header — never in URLs, never logged by the SDK.
  • Never log PathaoClientOptions, PathaoToken values or Authorization headers in your own pipelines either; the console demo masks tokens deliberately.
  • Supply credentials from a secret store (env vars, user secrets, key vault) — never hard-code them or commit them.
  • Tokens live in an ITokenStore you control; use a database-backed store to persist across restarts and revoke by clearing the store.

Sandbox integration tests

The live sandbox suite ([Category("Sandbox")]) is excluded from CI and regular dotnet test runs. Set these environment variables to execute it:

PATHAO_SANDBOX__CLIENT_ID
PATHAO_SANDBOX__CLIENT_SECRET
PATHAO_SANDBOX__USERNAME
PATHAO_SANDBOX__PASSWORD
PATHAO_SANDBOX__STORE_ID        // optional; pins the store for the price-plan and order tests
                                // (the order tests otherwise fall back to the first listed store)
PATHAO_SANDBOX__ALLOW_CREATE    // optional; "true" enables the store creation test (side effects)

Note: the order tests create real consignments in the sandbox merchant account — that is their purpose and they run ungated once credentials are present.

dotnet test --filter "Category=Sandbox"

Releasing (maintainers)

Releases are git tags (MinVer derives the package version — prefix v, e.g. v0.1.0-alpha.1). Pushing a tag runs release.yml: build + unit tests → dotnet pack → push .nupkg + .snupkg to NuGet.org → GitHub Release with generated notes and the packages attached. The one-time prerequisite is a NuGet API key (scoped to the Pathao.Courier package id) stored as the NUGET_API_KEY secret in the GitHub repo.

Contributing

Issues and pull requests are welcome at boniky-dev/pathao-courier-sdk. Keep builds warning-free (dotnet build -c Release treats warnings as errors), run the unit suite (dotnet test) before opening a PR, and never commit sandbox credentials — live-sandbox tests are opt-in via environment variables. For behavioral changes, cover them with unit tests against mocked HTTP before touching the live sandbox.

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.0 107 8/21/2026
0.1.0-alpha.1 65 8/21/2026