CourierGuard.

API & integrations

Server-side tenant API · WordPress · Laravel · Node.js

WordPress pluginNode.js clientLaravel clientDownload API guide

1. Base URL and credentials

Use your deployed HTTPS origin, for example https://checks.example.com. All routes below begin with /api. Local development: http://localhost:3000 via Next.js, or http://127.0.0.1:4000 for the API. A remote WordPress/Laravel server cannot reach this computer's localhost. Deployment and a public HTTPS hostname are still required for remote stores.

Tenant owner: API access → Generate API key. Copy the key once, store it on your server, and send Authorization: Bearer cg_.... Keys expire after 90 days and can be revoked immediately. Never send your courier credentials or super admin password to this API; all integrations use the tenant key. No tenant ID is accepted from clients. Browser JavaScript must call your own authenticated server route; never embed the tenant key in HTML, localStorage, frontend bundles, or public environment variables.

curl --request POST "$COURIERGUARD_URL/api/search" \
  --header "Authorization: Bearer $COURIERGUARD_KEY" \
  --header "Content-Type: application/json" \
  --data '{"phone":"01712345678"}'

2. Single search

POST /api/search · JSON body: { "phone": "01712345678" }.

Local, 880..., +880..., spaces and hyphens normalize to the same Bangladesh mobile number. HTTP 201 means the search completed at application level; inspect status and each provider's available. It does not mean every courier returned data.

{
  "phone": "01712345678",
  "status": "complete",
  "demo": false,
  "durationMs": 38,
  "charged": false,
  "providers": [
    {
      "provider": "paperfly",
      "available": true,
      "demo": false,
      "total": 11,
      "delivered": 7,
      "returned": 4,
      "partial": 0,
      "customerStatus": "Good",
      "fetchedAt": "2026-10-31T10:00:00.000Z",
      "source": "cache",
      "stale": false
    }
  ]
}

This example's latency is illustrative, not an SLA. Optional fields can be absent. Additional response fields may be added without changing these field meanings.

FieldMeaning
statuscomplete: all enabled providers usable; partial: some usable; unavailable: none usable
demotrue means synthetic results; do not use for customer decisions
chargedwhether this search reserved a new daily credit and produced usable data
providers[].availablefalse means an error/unavailable, not zero orders
total, delivered, returnedNonnegative counts when supplied/normalized; not all couriers supply counts
partialPaperFly partial deliveries, separate from delivered/returned
customerStatusProvider rating/status; preserve it even if counts are absent
sourcelive, cache, or database
fetchedAtTime of original courier fetch, not the current request
staletrue means old fallback data; show its timestamp
errorCode, messageOptional provider error, safe to display as text
ordered, cancelled, fraudCount, successRateCarryBee original statistics retained separately

CarryBee display mapping requested by this platform: total=ordered, delivered=ordered-cancelled, returned=cancelled. This is not a separate courier-certified delivered field. PaperFly partial counts are not silently added. Pathao may supply ratings without counts. A confirmed customer-not-found result can have all counts zero and available:true; zero history does not establish that a customer is safe.

3. Usage and plans

GET /api/me returns user, tenant (including daily_limit, bulk_limit, status and subscription_until), used, day, demo, providers. GET /api/plans lists membership plans.

Every successful search request costs one credit, including repeated numbers and cached results. A usable zero-history result costs one credit. Completely unavailable searches are refunded; partial usable results cost one credit. Daily quota resets at midnight Asia/Dhaka. Monthly limits also apply. Each concurrent request reserves separately under the tenant row lock. Bulk input is deduplicated within each batch; a new batch charges its successful items again.

4. Bulk search

POST /api/bulk → HTTP 201:

{ "phones": ["01712345678", "+8801812345678"] }

Response: { "id": "<uuid>", "count": 2, "status": "queued", "message": "..." }.

GET /api/bulk/:id returns batch status and items:[{phone,status,result}]; GET /api/bulk lists the latest 20 batches. Batch states: queued, processing, completed. Item states: pending, completed, failed. Failed items can contain {phone,error} or an unavailable SearchResult. A completed batch can contain failed items. Batch records and results are tenant scoped.

Duplicate phone formats are removed. Maximum batch size is the smaller of 1000 and the tenant plan's bulk_limit. One invalid number rejects the submission (400). Up to three unfinished batches per tenant. Credits are checked per number when processed; over-quota items fail without charge. Whole-batch quota is not reserved at submission. Bulk completion is asynchronous and not subject to the two-second search target. Poll every 3–5 seconds while pending; stop on completed. No automatic client-side POST retries: resubmitting a bulk request creates another batch.

5. Errors and retries

Non-2xx responses use { "statusCode": 401, "message": "...", "error": "Unauthorized" } (some infrastructure responses have only message).

HTTPAction
400Fix phone/body, batch size or validation; do not retry unchanged
401Key invalid, expired, revoked or tenant membership inactive; check key/subscription
403Daily quota, subscription or permissions; inspect message
404Batch does not exist in your tenant
409Number is already processing; retry single lookup after a short delay
429Request limit; honor Retry-After if present, otherwise wait at least one minute
503No providers configured or service unavailable; retry later with backoff

HTTP 201 with status:unavailable is distinct from an HTTP error. Inspect provider messages. Never convert authentication errors, timeouts or arbitrary 404s to zero-history. Escape messages before HTML rendering. Avoid logging keys or full customer responses.

Current general rate limit is 120 requests/minute/IP per API process, including polling. Membership daily quota is additional and globally enforced in PostgreSQL. Apply distributed gateway limits before scaling to multiple API nodes. For network timeouts use 12 seconds in integrations, bounded retry/backoff for single requests only, and never an unbounded loop.

6. Speed, cache and fallback

Browser, tenant API, WordPress and SDK requests all use the same search pipeline. Redis caches successful snapshots for 15 minutes; PostgreSQL stores the last successful snapshot independently. Old snapshots return with stale:true and refresh asynchronously. Cache scope includes tenant, provider credentials/revision and environment; merchant-specific data is not shared between tenants.

First-time courier calls have a 1.4-second foreground timeout per concurrent provider; background jobs use a longer timeout. Authentication, database load and network add overhead. Cold merchant login can exceed the foreground deadline: return partial/unavailable promptly, then retry a single lookup after the background refresh has had time to finish. Cached p95 under 500ms is a target; no live-courier or production two-second guarantee has been established. See BENCHMARK.md for local-only measurements.

Send APIs from a server geographically close to your deployed backend; keep persistent HTTPS connections. Do not add a second plugin cache that disguises expiry or bypasses central usage accounting. Responses have Cache-Control:no-store so shared HTTP/CDN caches cannot mix tenant/customer data. This does not disable the server's internal tenant-scoped cache.

7. WordPress plugin

Download /integrations/courierguard-wordpress.zip, upload in WordPress Plugins and activate. Tools → CourierGuard → set API origin and tenant key. Manual staff checker works without WooCommerce. WooCommerce adds the order action “CourierGuard: check billing phone”; it records a private note and metadata without blocking/changing the order. Admin and WooCommerce managers only; nonce protected. HTTPS required for WordPress's outbound connection. Prefer COURIERGUARD_API_KEY in wp-config.php. See the included plugin README for credential storage and validation limits.

8. Node.js / custom sites

Download /integrations/node-client.zip and import courierguard.mjs on your backend:

import { CourierGuard } from './courierguard.mjs';
const cg = new CourierGuard({
  baseUrl: process.env.COURIERGUARD_URL,
  apiKey: process.env.COURIERGUARD_KEY,
});
const result = await cg.search('01712345678');
// Inspect result.status, result.demo and providers[].available before rendering.

Your browser calls an authenticated route on your own server, which calls this client. Add your site's CSRF/rate limits; do not expose an anonymous proxy to your subscription. For Express, use your existing session middleware and POST route, validate the phone, call cg.search, and send the result. For Next.js use a server route handler with your own auth; never call the client from a client component.

9. Laravel

Download /integrations/laravel-client.zip. Copy CourierGuard.php to app/Services, add services.courierguard URL/key to server config, and use its search/usage/createBulk/bulk methods from authenticated controllers. The included README provides validation/error-handling code. Keep the key out of Vite/frontend environment variables.

10. Verification and release limits

The Node client is exercised against the running API with a temporary tenant key: normalized cached search, usage, malformed key, and revoked-key rejection. WordPress and Laravel PHP files receive syntax validation here. Full WordPress/WooCommerce runtime activation and Laravel framework tests require those installations; they have not been claimed as verified. The API runs locally; production DNS, HTTPS, infrastructure, billing and deployment remain separate work. This API has no /v1 prefix yet; integrate only the documented routes and optional-field semantics.

Plan and usage summary

GET /api/usage accepts your tenant Bearer API key. It returns tenant, plan (id, name, monthlyPrice in BDT, dailyLimit, nullable monthlyLimit, bulkLimit), membership (status, active, expiresAt), and usage: - daily and monthly: used, remaining, resetsAt (ISO timestamps). Monthly remaining is null when the plan has no monthly cap. - availableSearches: smaller of remaining daily/monthly allowances; zero for inactive or expired memberships. - totalSearches: all recorded search requests, including unavailable responses; lastSearchedAt is null until the first recorded search.

Used counts include in-progress reservations; failed/unavailable searches are refunded. Every successful repeat counts again. Calendar boundaries use Asia/Dhaka. Free accounts use their Free plan allowance; there is no separate invented free/paid balance. The endpoint does not consume a search credit.

Node.js: await client.planUsage(); Laravel: $client->planUsage(). Existing usage() continues to return /api/me for compatibility.

curl https://YOUR-DOMAIN/api/usage -H "Authorization: Bearer YOUR_TENANT_API_KEY"

Membership billing supports monthly and yearly periods. /api/plans and /api/public/plans include monthly_price and yearly_price (BDT). /api/usage includes plan.yearlyPrice and membership.billingCycle. Search quotas reset on their normal daily/monthly boundaries for both billing periods. The super admin records yearly confirmed purchases with billingCycle: "yearly" and months: 12; the server charges the configured yearly price. Existing monthly requests remain compatible.

### Reading the search balance

The tenant dashboard shows the same values under API access → Plan & search balance. This is a search allowance, not a cash wallet.

const response = await fetch(`${baseUrl}/api/usage`, {
  headers: { Authorization: `Bearer ${tenantApiKey}` }
});
if (!response.ok) throw new Error('Usage request failed');
const data = await response.json();
console.log(data.plan.name);
console.log(data.usage.daily.limit, data.usage.daily.used, data.usage.daily.remaining);
console.log(data.usage.monthly.limit, data.usage.monthly.used, data.usage.monthly.remaining);
console.log(data.usage.availableSearches, data.membership.expiresAt);

usage.daily.limit and usage.monthly.limit are included directly beside their used/remaining counters. A null monthly limit/remaining means no additional monthly cap; the daily cap still applies. availableSearches accounts for both caps and membership status. Usage queries do not debit the allowance.