RedX.Courier 1.0.0

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

RedX.Courier

ci NuGet License: MIT

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

A .NET SDK (net10.0) for the RedX OpenAPI: typed clients for parcels (create, info, track, update, cancel), delivery areas, pickup stores and charge calculation — with static bearer-token authentication and typed webhook payload models. Zero third-party runtime dependencies beyond BCL + Microsoft.Extensions.* abstractions.

Status: v0.1.0-alpha — feature-complete for the 11 documented operations plus the webhook payload contract: the core layer (bespoke API-ACCESS-TOKEN bearer auth, typed errors, tolerant converters), the delivery-areas and charge-calculator clients, the parcels client (create / info / track / update / cancel), the typed webhook payload models, the pickup-stores client, and the unified RedXCourierClient facade with DI registration and the multi-merchant factory below. A runnable console demo lives in examples/RedX.Courier.ConsoleDemo. See TRACKER.md for the phase-by-phase history.

Install

dotnet add package RedX.Courier --prerelease

Targets net10.0 only. Nightly-friendly: CI builds and unit tests run on every push; sandbox integration tests run on a manual-dispatch and nightly workflow.

Endpoint coverage

API endpoint SDK surface Notes
Header auth (API-ACCESS-TOKEN: Bearer) attached by the SDK to every request bespoke header, not Authorization
GET /areas Areas.ListAsync typed DeliveryArea list
GET /areas?post_code= Areas.GetByPostCodeAsync several areas can share a post code
GET /areas?district_name= Areas.GetByDistrictNameAsync name is URL-escaped for you
GET /charge/charge_calculator Pricing.CalculateAsync camelCase charge fields mapped; read-only
POST /parcel Parcels.CreateAsync singular path; curl-mirroring body (see below)
GET /parcel/info/{trackingId} Parcels.GetInfoAsync full parcel details incl. pickup location
GET /parcel/track/{trackingId} Parcels.TrackAsync bilingual (EN/BN) event log
PATCH /parcels Parcels.UpdateAsync / Parcels.CancelAsync plural path; generic entity/property envelope
POST /pickup/store PickupStores.CreateAsync response is the store at root, without created_at
GET /pickup/stores PickupStores.ListAsync pickup_stores[] wrapper; tolerant ids
GET /pickup/store/info/{pickupStoreId} PickupStores.GetAsync pickup_store{} wrapper
Webhook callback payload RedX.Webhooks.ParcelStatusWebhook + RedXWebhookJsonContext models only — no receiver/signature (see below)

Creating a client

One RedXCourierClient (or its IRedXCourierClient facade) serves one RedX merchant account. Direct construction — fine for scripts, tools and tests:

using RedXCourierClient client = new(new RedXClientOptions
{
    BaseUrl = RedXUrls.Sandbox,     // https://sandbox.redx.com.bd/v1.0.0-beta (Production is the default)
    AccessToken = Environment.GetEnvironmentVariable("REDX_ACCESS_TOKEN")!,
});

// client.Parcels, client.Areas, client.PickupStores, client.Pricing

When constructed without an HttpClient, the client owns its transport (honoring RedXClientOptions.Timeout) and disposes it; inject one and its lifecycle stays with you.

DI — single account (ASP.NET Core / generic host)

builder.Services.AddRedXCourier(options =>
{
    options.AccessToken = builder.Configuration["RedX:AccessToken"];
    // options.BaseUrl defaults to RedXUrls.Production; point at RedXUrls.Sandbox for integration tests
});

// anywhere: IRedXCourierClient is registered transient, over IHttpClientFactory
public class CheckoutService(IRedXCourierClient redx)
{
    public async Task<string> ShipAsync(...) => (await redx.Parcels.CreateAsync(...)).TrackingId;
}

Options are validated at host start (ValidateOnStart) — a missing AccessToken or an invalid BaseUrl fails fast. The HTTP pipeline is registered with IHttpClientFactory under the name RedXCourierServiceCollectionExtensions.HttpClientName ("RedXCourier"), so you can append handlers/retry policy once for every client:

builder.Services.AddHttpClient(RedXCourierServiceCollectionExtensions.HttpClientName)
    .AddStandardResilienceHandler();   // e.g. Microsoft.Extensions.Http.Resilience

DI — multi-merchant factory

Serving many merchants, each with their own dashboard-issued token? Register the factory and create clients at request time — the token is stateless (attached per request), so created clients are cheap and hold no session state:

builder.Services.AddRedXCourierFactory();

public class FulfillmentService(IRedXCourierClientFactory factory, MerchantStore merchants)
{
    public async Task<string> ShipAsync(string merchantId, ...)
    {
        MerchantCredentials credentials = await merchants.GetRedXCredentialsAsync(merchantId);
        IRedXCourierClient merchantClient = factory.Create(new RedXClientOptions
        {
            BaseUrl = RedXUrls.Production,
            AccessToken = credentials.AccessToken,
        });

        return (await merchantClient.Parcels.CreateAsync(...)).TrackingId;
    }
}

Factory-created clients draw their transport from the same named RedXCourier pipeline (or own it when no IHttpClientFactory is available); you can also pass an explicit HttpClient to Create when you need full control. Both registrations compose — AddRedXCourier for your own platform account plus AddRedXCourierFactory for your merchants.

Managing credentials

The access token is a secret: it authenticates every RedX API call for the merchant. In order of preference:

  1. Environment variables / user-secrets in dev (REDX_ACCESS_TOKEN, dotnet user-secrets), and GitHub/secrets-manager variables in CI — never in source control.
  2. Configuration (IConfiguration) for single-account hosts, sourced from a secret store (Azure Key Vault, AWS Secrets Manager, etc.) rather than appsettings.json.
  3. An integration layer for multi-tenant hosts: keep per-merchant tokens in your database or secret store and materialize them into RedXClientOptions via IRedXCourierClientFactory.Create at request time — the SDK deliberately holds no persistence and no token lifecycle (D6).

The SDK never logs the token (it logs nothing), but avoid echoing options or exception data into your own logs. If a token is compromised, revoke/rotate it from the RedX merchant dashboard.

Areas

RedX addresses parcels by flat delivery areas (no city/zone hierarchy) — the Id is the delivery_area_id/pickup_area_id the rest of the API speaks in, so resolve it before creating parcels or pricing them:

IReadOnlyList<DeliveryArea> all = await client.Areas.ListAsync();          // cache this — it is large and stable
IReadOnlyList<DeliveryArea> byPostCode = await client.Areas.GetByPostCodeAsync(1206);
IReadOnlyList<DeliveryArea> inDhaka = await client.Areas.GetByDistrictNameAsync("Dhaka");

Each DeliveryArea carries Id, Name, PostCode, DivisionName and ZoneId. The {"areas":[...]} envelope is unwrapped for you (a bare root array is tolerated), and a non-areas payload surfaces as RedXApiException with the raw body.

Parcels

Creating a parcel needs a delivery area (both its Id and its exact Name — resolve them via Areas.ListAsync first). Optional fields are simply omitted from the request when null:

ParcelCreated created = await client.Parcels.CreateAsync(new CreateParcelRequest
{
    CustomerName = "Test Customer",
    CustomerPhone = "01987654321",              // 11-digit 01XXXXXXXXX
    DeliveryArea = area.Name,                   // must match DeliveryAreaId
    DeliveryAreaId = area.Id,
    CustomerAddress = "House 1, Road 2, Mirpur DOHS",
    MerchantInvoiceId = "ACBD1234TEST",        // optional
    CashAmount = 13293m,                        // BDT to collect on delivery
    ParcelWeight = 500,                         // grams — always whole grams, never kg
    Value = 1500m,                              // declared value
    Instruction = "Handle with care",           // optional
    // Type = "reverse",                        // optional, mainly for reverse shipments
    // IsClosedBox = true,                      // optional, provisional field
    // PickupStoreId = 7,                       // optional
    // ParcelDetails = [new ParcelDetailItem("T-shirt", "clothing", 1000)],  // optional
});

Parcel info = await client.Parcels.GetInfoAsync(created.TrackingId);
// info.Status, info.Charge, info.CashAmount, info.PickupLocation, ...

IReadOnlyList<TrackingEvent> events = await client.Parcels.TrackAsync(created.TrackingId);
// events[0].MessageEn / MessageBn / Time — bilingual chronological log

Info vs track: GetInfoAsync returns the parcel's current snapshot (charges, status, addresses, pickup location); TrackAsync returns the chronological event log with English and Bengali messages. Unknown status/delivery-type strings map to ParcelStatus.Unknown / DeliveryType.Unknown instead of throwing — RedX's vocabulary is open-ended and its own docs already return statuses (pickup-pending) missing from the webhook table.

Request wire format: RedX's parameter table and its curl sample disagree on field types; the SDK serializes exactly like the curl sample — cash_collection_amount as a JSON string, parcel_weight/value/pickup_store_id as JSON numbers, is_closed_box as a string — so you never think about it. The sandbox E2E test locks this format against reality. Responses are parsed tolerantly either way (money and ids from a JSON number or a numeric string — the info sample returns value as the string "0").

All inputs are pre-validated client-side (phone digits, non-negative amounts, weight >= 1 g, positive area/store ids, non-empty required strings) and throw RedXValidationException before any HTTP call.

Updating & cancelling parcels

RedX updates parcels through a generic entity/property envelope (PATCH /parcels — note the plural path, unlike the singular create), and applies changes asynchronously: a success:true receipt ("Request Accepted") means the update was queued, not that it is already visible — confirm via GetInfoAsync/TrackAsync when it matters:

// Cancelling — the typed convenience for the documented status -> cancelled update
UpdateResult cancel = await client.Parcels.CancelAsync(created.TrackingId, "Customer changed the order");
if (cancel.Success) { /* accepted — RedX applies the cancellation asynchronously */ }

// Any other documented property, e.g. delivery_address, via the generic update
UpdateResult move = await client.Parcels.UpdateAsync(new ParcelUpdate
{
    EntityType = "parcel-tracking-id",       // the documented entity type
    EntityId = created.TrackingId,
    PropertyName = "delivery_address",
    NewValue = "House 9, Road 3, Mirpur DOHS",
    // Reason = "customer called in a change",  // optional, omitted when null
});

UpdateResult.Success == false maps a rejected request (still HTTP 200) without throwing — check it explicitly; HTTP-level failures still throw RedXApiException.

Webhooks

RedX can POST parcel status changes to a callback URL you configure in the merchant dashboard. The SDK ships typed payload models + a public source-generated JSON context so you can bind pushes without touching raw JSON — deliberately nothing more (no receiver middleware, no signature validation, because RedX documents no signing scheme):

using System.Text.Json;
using Microsoft.AspNetCore.Mvc;
using RedX.Courier.Webhooks;

// e.g. in a minimal ASP.NET Core controller
[ApiController]
[Route("redx/callback")]
public sealed class RedXCallbackController : ControllerBase
{
    [HttpPost]
    [Consumes("application/json")]
    public async Task<IActionResult> Post()
    {
        ParcelStatusWebhook? webhook = await JsonSerializer.DeserializeAsync(
            Request.Body, RedXWebhookJsonContext.Default.ParcelStatusWebhook);

        if (webhook?.TrackingNumber is not { Length: > 0 } trackingId)
        {
            return BadRequest(); // tolerant binding — validate the payload yourself
        }

        // webhook.Status (ParcelStatus), webhook.MessageEn / MessageBn,
        // webhook.Timestamp (string — RedX does not document the format; parse defensively),
        // webhook.InvoiceNumber (null when the parcel has no invoice), webhook.DeliveryType
        return Ok();
    }
}

Field names diverge from the REST API (tracking_number / invoice_number, not tracking_id / merchant_invoice_id) — mapped for you. Unrecognized status/delivery-type strings bind as Unknown, and missing string members bind as null (tolerant binding — validate TrackingNumber in your handler, as above).

Security: RedX sends credentials in the callback URL's query parameters and documents no payload signature, so your only protection is an unguessable HTTPS callback URL (a long random token in the query string, e.g. https://example.com/redx/callback?token=<128-bit-secret>). Treat the payload's contents as unauthenticated input.

Charge calculator

Price a hypothetical parcel without creating anything:

ParcelCharge charge = await client.Pricing.CalculateAsync(new ParcelChargeRequest(
    DeliveryAreaId: deliveryArea.Id,
    PickupAreaId: pickupArea.Id,
    CashAmount: 1500m,   // BDT to collect on delivery
    Weight: 500));       // grams — RedX weights are whole grams everywhere

This is the one documented endpoint whose response is camelCase (deliveryCharge/codCharge) rather than snake_case; both map to decimal and tolerate a numeric string. Requests are pre-validated offline (positive area ids, cash >= 0, weight >= 1 g) and throw RedXValidationException before any HTTP call.

Pickup stores

Parcels are collected from pickup stores. Creating one needs a delivery area id (resolve via Areas.ListAsync); the returned store id then feeds CreateParcelRequest.PickupStoreId:

PickupStore store = await client.PickupStores.CreateAsync(new CreatePickupStoreRequest
{
    Name = "Boniky Flagship",
    Phone = "8801898000999",                 // pickup-store phones carry the 880 prefix (13 digits)
    Address = "House 1, Road 2, Mohammadpur, Dhaka",
    AreaId = area.Id,
});
// store.Id, store.AreaName — note: no store.CreatedAt here (only list/details carry it)

IReadOnlyList<PickupStore> all = await client.PickupStores.ListAsync();
PickupStore details = await client.PickupStores.GetAsync(store.Id);
// details.CreatedAt, details.AreaName, ...

The three endpoints use three different response shapes — list answers {"pickup_stores":[...]}, details answer {"pickup_store":{...}}, and create returns the store body at the root — all unwrapped for you. Ids parse from a JSON number or a numeric string (RedX's own list sample renders them as placeholder strings), and CreatedAt is null on the create response by design. Inputs are pre-validated (non-empty name/address, phone 11–13 digits, positive area id) before any HTTP call.

Examples

examples/RedX.Courier.ConsoleDemo is a runnable, menu-driven sandbox tour — areas → charge calculator → create parcel → track → info → cancel:

cd examples/RedX.Courier.ConsoleDemo
dotnet user-secrets set "RedX:AccessToken" "<sandbox-token>"   # or export REDX_SANDBOX__ACCESS_TOKEN
dotnet run

Side-effect calls (create, cancel) show a summary and require typing yes before anything is sent; the token is read from REDX_SANDBOX__ACCESS_TOKEN or the RedX:AccessToken user-secret and is never printed. RedX:BaseUrl overrides the sandbox default.

API quirks normalized for you

  • Bespoke auth header: every request (GETs included) carries API-ACCESS-TOKEN: Bearer <token> — a dashboard-issued static token, never the standard Authorization header.
  • Base URLs carry the full versioned prefix (/v1.0.0-beta) — the curl samples in RedX's docs repeat the segment (double prefix), which does not exist on the wire: the sandbox answers 404 for the doubled path and 401 (auth required) for the single prefix. RedXUrls constants are verified; BaseUrl stays config-overridable.
  • CamelCase charge fields (deliveryCharge/codCharge) mapped explicitly amid otherwise snake_case JSON.
  • Tolerant money parsing: decimals read from a JSON number or numeric string.
  • Create-parcel typing conflicts (doc table vs curl sample): the SDK mirrors the curl — cash_collection_amount quoted, parcel_weight/value/pickup_store_id numeric, is_closed_box quoted and omitted when null — locked by body-snapshot tests and the sandbox E2E run.
  • Singular/plural paths: create is POST /parcel (singular) while update is PATCH /parcels (plural) — encoded exactly.
  • Pickup-store response shapes differ per endpoint: list wraps in pickup_stores[], details in pickup_store{}, create returns the store at the root — each unwrapped; ids tolerate a JSON number or numeric string (the documented list sample renders them as placeholder strings) and created_at is missing from the create response.
  • Asynchronous PATCH semantics: PATCH /parcels answers {"success":true,"message": "Request Accepted"} — an acceptance, not a synchronous application. UpdateResult surfaces this; confirm application via GetInfoAsync/TrackAsync.
  • Webhook field naming diverges from the API: the callback payload uses tracking_number/invoice_number (not tracking_id/merchant_invoice_id) — mapped explicitly on ParcelStatusWebhook.
  • Weights are grams (integers) everywhere — no kg conversion.

Status vocabulary

ParcelStatus (hyphenated lowercase on the wire; ParcelStatusExtensions.IsTerminal() / IsInFlight() categorize them):

Wire string ParcelStatus member Meaning
pickup-pending PickupPending created, not yet collected from the merchant (seen in the info sample; absent from the webhook table)
ready-for-delivery ReadyForDelivery received from the merchant at a RedX facility
delivery-in-progress DeliveryInProgress dispatched to a rider for delivery
delivered Delivered delivered (terminal)
agent-hold AgentHold on hold with the rider
agent-returning AgentReturning return in progress
returned Returned returned to the merchant (terminal)
agent-area-change AgentAreaChange an area change was requested and is in progress
paid Paid the parcel amount was paid out to the merchant (terminal)
anything else Unknown unrecognized status — mapped, never thrown

Delivery-type vocabulary

DeliveryType (same hyphenated wire format and Unknown fallback):

Wire string DeliveryType member Meaning
regular Regular regular forward delivery
reverse Reverse regular reverse delivery (return pickup)
exchange-delivery ExchangeDelivery forward exchange parcel
exchange-return ExchangeReturn reverse exchange parcel
partial-delivery PartialDelivery partial delivery parcel
partial-return PartialReturn partial return parcel
anything else Unknown unrecognized type — mapped, never thrown

Contributing

Issues and PRs are welcome at boniky-dev/redx-courier-sdk. The ground rules: builds must stay warning-free (dotnet build -c Release with warnings-as-errors), unit tests must stay green (dotnet test excludes the Sandbox category), public members need XML docs, and only endpoints documented in RedX's OpenAPI are in scope — see TRACKER.md for the current phase state and the post-v1 backlog. Never commit tokens; sandbox tests read REDX_SANDBOX__ACCESS_TOKEN from the environment.

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 117 8/23/2026
0.1.0-alpha.1 83 8/23/2026