Kanal Partner API
Server-to-server REST API for partner storefronts to provision Kanal customer accounts. Integrate with your signup flow so your customers land in Kanal instantly — no manual steps.
Overview
Current version
v1
Protocol
HTTPS / REST
Auth scheme
Bearer Token
The Kanal Partner API lets trusted integrators create customer accounts on behalf of a courier company. Every API key is scoped to a single Kanal tenant. Accounts created via the API receive a branded welcome email with their temporary password, and immediately appear in the courier's customer list with a unique customer number.
Base URL
https://kanal.cyrusfieldtech.com/apiAll Partner API paths are relative to this base. For example, the customer creation endpoint is at /api/partner/v1/customers.
Authentication
All Partner API requests require a Bearer token in the Authorization header. Tokens are issued by your Kanal super-admin from the Companies page and follow the format kanal_pk_XXXXXXXXXX.YYYYYY.
Authorization: Bearer kanal_pk_YOUR_KEY_HERERate Limits
Partner API requests share the per-tenant rate limit tier. Limits apply per rolling minute window:
| Plan | Requests / minute |
|---|---|
| Starter | 60 |
| Pro | 300 |
| Scale | 1,000 |
Exceeding the rate limit returns 429 Too Many Requests.
Create Customer
/partner/v1/customersProvisions a new customer account under the tenant associated with your API key. The new customer receives a branded welcome email with their temporary password and a link to log in. A unique customer number is auto-assigned and returned for use in shipment bookings.
Request body
| Field | Type | Description |
|---|---|---|
emailrequired | string (email) | Valid email address. Must be unique within this courier's tenant. Used as the customer's login. |
namerequired | string | Customer's full name. Min 1 character. |
countryrequired | string (ISO-2) | ISO 3166-1 alpha-2 country code (e.g. JM, US). Defaults to JM. |
phone | string | Customer's phone number including country code (e.g. +18765551234). |
address | string | Customer's shipping address. Stored as the default address on their recipient profile. |
trn | string | Jamaica Tax Registration Number — exactly 9 digits (dashes allowed: 123-456-789). Only accepted when country=JM. |
Example request
curl -X POST https://kanal.cyrusfieldtech.com/api/partner/v1/customers \
-H "Content-Type: application/json" \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE" \
-d '{
"email": "alice@example.com",
"name": "Alice Brown",
"phone": "+18765551234",
"country": "JM",
"address": "12 Hope Road, Kingston 6, Jamaica",
"trn": "123456789"
}'Response — 200 OK
{
"ok": true,
"customer_number": "SW460329",
"user_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"email": "alice@example.com",
"already_existed": false
}| Field | Description |
|---|---|
ok | Always true on success. |
customer_number | Stable, auto-assigned identifier for this customer within the courier (e.g. SW460329). Use this when booking shipments. |
user_id | Internal UUID for the newly created user. |
email | Lowercased, normalised email as stored. |
already_existed | Always false for a successful creation. Reserved for future idempotency behavior. |
Get Customer Profile
/partner/v1/customers/{identifier}Retrieve a customer's account and recipient profile. The identifier path segment accepts either an email address or a customer number (returned from the create endpoint).
Lookup by email
curl https://kanal.cyrusfieldtech.com/api/partner/v1/customers/alice%40example.com \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE"Lookup by customer number
curl https://kanal.cyrusfieldtech.com/api/partner/v1/customers/SW460329 \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE"Response — 200 OK
{
"user_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479",
"email": "alice@example.com",
"name": "Alice Brown",
"phone": "+18765551234",
"customer_number": "SW460329",
"country": "JM",
"address": "12 Hope Road, Kingston 6, Jamaica",
"trn": "123456789",
"account_status": "active",
"email_verified": true,
"created_at": "2026-01-15T14:30:00Z"
}| Field | Description |
|---|---|
user_id | UUID of the customer's Kanal login account. null if the customer has packages but no login. |
email | Customer's email address (lowercased). |
name | Customer's display name. |
phone | Phone number, or null if not provided. |
customer_number | Stable alphanumeric identifier (e.g. SW460329). Use this in shipment bookings. |
country | ISO-2 country code recorded at signup. |
address | Default shipping address, or null. |
trn | Jamaica TRN (9 digits), or null. Returned unmasked to the issuing partner. |
account_status | One of active, pending_verification, blocked, or recipient_only (shipment record exists but no login account). |
email_verified | true once the customer has clicked the welcome email link. |
created_at | ISO 8601 timestamp of account creation. |
List Customer Packages
/partner/v1/customers/{identifier}/packagesReturns a paginated list of all shipments associated with the customer, ordered newest first. Accepts the same email-or-customer-number identifier as the profile endpoint.
Query parameters
| Param | Type | Description |
|---|---|---|
limit | integer | Page size — 1 to 100. Default: 20. |
offset | integer | Number of records to skip. Default: 0. |
status | string | Filter to a specific status: pending · in_transit · ready_for_pickup · delivered · cancelled · return_to_sender |
Example — all packages by customer number
curl "https://kanal.cyrusfieldtech.com/api/partner/v1/customers/SW460329/packages?limit=10&offset=0" \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE"Example — delivered packages by email
curl "https://kanal.cyrusfieldtech.com/api/partner/v1/customers/alice%40example.com/packages?status=delivered&limit=5" \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE"Response — 200 OK
{
"total": 2,
"limit": 10,
"offset": 0,
"items": [
{
"package_id": "a1b2c3d4-0001-0000-0000-000000000001",
"tracking_number": "KN1029384756",
"status": "delivered",
"shipment_method": "air",
"description": "Electronics",
"weight_kg": 1.2,
"sender_name": "Cyber Store Ltd",
"sender_address": "11420 Fortune Cir Unit 34, Wellington FL 33414",
"created_at": "2026-01-10T09:00:00Z",
"updated_at": "2026-01-18T14:22:00Z",
"delivered_at": "2026-01-18T14:22:00Z"
},
{
"package_id": "a1b2c3d4-0002-0000-0000-000000000002",
"tracking_number": "KN9876543210",
"status": "in_transit",
"shipment_method": "sea",
"description": "Clothing",
"weight_kg": 3.5,
"sender_name": "Fashion Depot",
"sender_address": "200 Fashion Ave, New York NY 10001",
"created_at": "2026-01-20T11:00:00Z",
"updated_at": "2026-01-22T08:00:00Z",
"delivered_at": null
}
]
}| Field | Description |
|---|---|
total | Total count of matching packages (before pagination). |
limit / offset | Echo of the pagination params supplied in the request. |
items[].package_id | Internal UUID of the package. |
items[].tracking_number | KN-prefixed tracking code (e.g. KN1029384756). Use for public tracking at /c/{slug}/track/{number}. |
items[].status | Current delivery status. |
items[].shipment_method | air · sea · ground. |
items[].weight_kg | Package weight in kilograms. |
items[].sender_name | Name of the US/origin sender or store. |
items[].delivered_at | ISO 8601 delivery timestamp, or null if not yet delivered. |
List Customer Invoices
/partner/v1/customers/{identifier}/invoicesReturns a paginated list of invoices for the given customer, ordered newest first. Accepts the same email-or-customer-number identifier as the profile endpoint.
Query parameters
| Param | Type | Description |
|---|---|---|
limit | integer | Page size — 1 to 100. Default: 20. |
offset | integer | Records to skip. Default: 0. |
status | string | unpaid · paid · overdue · cancelled |
Example — all invoices by customer number
curl "https://kanal.cyrusfieldtech.com/api/partner/v1/customers/SW460329/invoices?limit=10&offset=0" \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE"Example — unpaid invoices by email
curl "https://kanal.cyrusfieldtech.com/api/partner/v1/customers/alice%40example.com/invoices?status=unpaid" \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE"Response — 200 OK
{
"total": 2,
"limit": 10,
"offset": 0,
"items": [
{
"invoice_id": "b2c3d4e5-0001-0000-0000-000000000001",
"invoice_number": "INV-00042",
"customer_name": "Alice Brown",
"customer_email": "alice@example.com",
"amount": 1750.00,
"currency": "JMD",
"status": "unpaid",
"due_date": "2026-09-20T00:00:00Z",
"paid_at": null,
"payment_method": null,
"package_id": "a1b2c3d4-0001-0000-0000-000000000001",
"notes": "Air shipment 2 lbs",
"created_at": "2026-09-07T10:00:00Z"
},
{
"invoice_id": "b2c3d4e5-0002-0000-0000-000000000002",
"invoice_number": "INV-00038",
"customer_name": "Alice Brown",
"customer_email": "alice@example.com",
"amount": 3850.00,
"currency": "JMD",
"status": "paid",
"due_date": "2026-08-15T00:00:00Z",
"paid_at": "2026-08-12T14:30:00Z",
"payment_method": "wipay",
"package_id": "a1b2c3d4-0002-0000-0000-000000000002",
"notes": "Air shipment 5 lbs",
"created_at": "2026-08-08T09:00:00Z"
}
]
}| Field | Description |
|---|---|
invoice_id | Internal UUID of the invoice. |
invoice_number | Human-readable reference displayed on the invoice PDF (e.g. INV-00042). |
customer_name | Name as recorded on the invoice. |
customer_email | Email the invoice is addressed to. |
amount | Total invoice amount in the stated currency. |
currency | ISO 4217 currency code (e.g. JMD, USD). |
status | One of unpaid · paid · overdue · cancelled. |
due_date | ISO 8601 payment due date, or null. |
paid_at | ISO 8601 timestamp when payment was confirmed, or null. |
payment_method | e.g. wipay · stripe · cash · bank_transfer, or null if unpaid. |
package_id | UUID of the linked package, or null for standalone invoices. |
notes | Free-text note added by the dispatcher, or null. |
created_at | ISO 8601 invoice creation timestamp. |
Get Shipping Rates
/partner/v1/ratesReturns the courier's live shipping rate table authenticated with your partner key — no customer JWT required. Use this to display up-to-date rates on your storefront. The response always reflects the latest rates as configured by the courier admin.
Example request
curl https://kanal.cyrusfieldtech.com/api/partner/v1/rates \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE"Response — 200 OK
{
"currency": "JMD",
"per_lb_rate": 600,
"rate_table": [
{ "lbs": 1, "price": 600 },
{ "lbs": 2, "price": 1000 },
{ "lbs": 3, "price": 1400 },
{ "lbs": 4, "price": 1750 },
{ "lbs": 5, "price": 2100 },
...
{ "lbs": 100, "price": 35350 }
]
}| Field | Description |
|---|---|
currency | ISO 4217 currency code for all prices in this response (e.g. JMD, USD). |
per_lb_rate | Base rate for 1 lb — convenience field matching rate_table[0].price. |
rate_table | Ordered array of weight tiers, each with lbs (integer) and price (number in the stated currency). Use lbs as an exact lookup key for the package weight. |
const { rate_table, currency } = await fetch('/api/partner/v1/rates', {
headers: { Authorization: `Bearer ${KANAL_PARTNER_KEY}` }
}).then(r => r.json());
const tier = rate_table.find(t => t.lbs === packageWeightLbs);
const price = tier?.price ?? null; // null = weight not in tableAdd Newsletter Subscriber
/partner/v1/subscribersAdds an email address to the courier's newsletter subscriber list (visible in the Communication → Subscribers tab). Idempotent — re-submitting an address that was previously unsubscribed will reactivate it; submitting an already-active address returns ok: true silently.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Valid email address. Stored in lowercase. |
name | string | No | Subscriber's display name. Shown in the dashboard. |
source | string | No | Origin label (e.g. "kyoown_platform", "mobile_app"). Defaults to "partner". |
Example request
curl -X POST https://kanal.cyrusfieldtech.com/api/partner/v1/subscribers \
-H "Content-Type: application/json" \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE" \
-d '{
"email": "alice@example.com",
"name": "Alice Brown",
"source": "kyoown_platform"
}'Response — 201 Created (new subscriber)
{
"ok": true,
"resubscribed": false
}Response — 201 (reactivated unsubscribed email)
{
"ok": true,
"resubscribed": true
}Unsubscribe an Email
/partner/v1/subscribers/{email}Marks a subscriber as inactive so they no longer appear in the active subscriber list and are excluded from future campaigns. Idempotent — calling on a non-existent or already-inactive address returns ok: true silently. URL-encode the email address (replace @ with %40).
Example request
curl -X DELETE "https://kanal.cyrusfieldtech.com/api/partner/v1/subscribers/alice%40example.com" \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE"Response — 200 OK
{
"ok": true,
"unsubscribed": true
}| Field | Description |
|---|---|
ok | Always true when the request was well-formed and authorised. |
unsubscribed | true if the record was just deactivated; false if it was already inactive or not found. |
reason | Omitted on success. "already_inactive" or "not_found" when unsubscribed is false. |
Create a Courier Tenant
/partner/v1/tenantsRegisters a new courier company (tenant) and provisions its first company_admin account. Designed for platforms that onboard couriers externally (e.g. KYoown) and need a matching Kanal tenant created immediately after their own signup flow. A verification email is dispatched to the admin's inbox — the tenant can log in once the email address is confirmed.
Request body
| Field | Type | Required | Description |
|---|---|---|---|
company_name | string | Yes | Display name for the courier company. Must be unique across the platform. |
admin_name | string | Yes | Full name of the company admin account to provision. |
admin_email | string | Yes | Email address for the admin account. Must not already exist on the platform. |
admin_password | string | Yes | Initial password for the admin account. Minimum 6 characters. |
phone | string | No | Company contact phone number. |
address | string | No | Company street address. |
country | string | No | ISO 3166-1 alpha-2 country code. Defaults to "JM" (Jamaica). |
plan | string | No | Subscription plan: "starter", "pro", or "scale". Defaults to "starter". |
website | string | No | Company website URL. |
Example request
curl -X POST https://kanal.cyrusfieldtech.com/api/partner/v1/tenants \
-H "Content-Type: application/json" \
-H "Authorization: Bearer kanal_pk_YOUR_KEY_HERE" \
-d '{
"company_name": "KYo Couriers Ltd",
"admin_name": "Marcus Reid",
"admin_email": "marcus@kyocouriers.com",
"admin_password": "secure-temp-pass",
"phone": "+1-876-555-0100",
"address": "12 Harbour St, Kingston",
"country": "JM",
"plan": "starter"
}'Response — 201 Created
{
"ok": true,
"company_id": "a1b2c3d4-...",
"company_name": "KYo Couriers Ltd",
"admin_email": "marcus@kyocouriers.com",
"admin_user_id": "u1u2u3u4-...",
"public_slug": "ky-7f3a12",
"role": "company_admin",
"account_locked": false,
"email_verified": false,
"verification_required": true,
"customer_signup_url": "https://kanal.cyrusfieldtech.com/c/ky-7f3a12/signup",
"created_at": "2026-10-02T00:00:00Z",
"message": "Tenant created. The admin must verify their email before logging in."
}| Field | Description |
|---|---|
company_id | UUID of the newly created tenant company. |
admin_user_id | UUID of the provisioned company_admin user account. |
public_slug | URL-safe slug for the tenant's public customer signup page. |
customer_signup_url | Full URL customers can use to self-register with this courier. |
verification_required | Always true. The admin must click the verification link in their inbox before logging in. |
Error Reference
All errors return JSON with a detail field. Standard HTTP status codes apply.
| Code | Status | Meaning |
|---|---|---|
| 400 | Bad Request | Validation failed — e.g., TRN provided without country=JM, or country code not exactly 2 letters. |
| 401 | Unauthorized | Missing, malformed, or revoked API key. |
| 402 | Payment Required | The owning courier's subscription invoice is overdue. Resolve outstanding invoices to re-enable API access. |
| 409 | Conflict | A customer with this email already exists in the courier's tenant. |
| 422 | Unprocessable Entity | Request body failed Pydantic schema validation (invalid email format, missing required field, etc.). |
| 429 | Too Many Requests | Per-tenant rate limit exceeded. Retry after the rolling window resets (~60 seconds). |
| 500 | Internal Server Error | Unexpected server error. Contact support if this persists. |
409 — Duplicate email
{
"detail": "Email already registered for this tenant"
}401 — Invalid key
{
"detail": "Invalid or revoked partner API key"
}402 — Locked tenant
{
"detail": "Owning tenant account is locked — resolve outstanding invoices to re-enable partner API access"
}422 — Validation error
{
"detail": [
{
"loc": ["body", "email"],
"msg": "value is not a valid email address",
"type": "value_error.email"
}
]
}Getting an API Key
Contact your Kanal admin
API keys are issued per courier tenant by the Kanal super-admin. Email your account manager or contact us at dev@cyrusfieldtech.com to request access for your integration.
Key is minted on the Companies page
The super-admin navigates to Companies → selects your courier → opens the Partner API Keys panel → clicks 'Generate key'. The plaintext key is shown exactly once at creation time.
Store it securely
Copy the key immediately and store it in your server environment variables (e.g., KANAL_API_KEY). It is never shown again — if lost, revoke and generate a new one.
Start creating customers
Use the key in the Authorization header as shown in the examples above. Each request creates a Kanal customer account scoped to your courier's tenant.
Questions or custom integrations?
Reach out at dev@cyrusfieldtech.com — we can help with server-side auth flows, webhooks, and white-label integrations.