Developer guide

MPLAY Connect API

Connect any platform (billing, ISP, WiFi, website, mobile app, POS, e-commerce or CRM) to MPLAY through one secure gateway with OAuth access tokens, scopes, webhooks and usage logs. MPLAY never receives or stores your payment credentials.

1. Getting Started

MPLAY Connect is the secure gateway that lets any authorised platform (Example Billing System, Example ISP, Example Website, a POS or a mobile app) work with MPLAY. You never touch MPLAY's database, video storage or payment gateways; you call the API and MPLAY stays the source of truth. Base URL: https://mplay.live/api/public/connect/v1 (also served at /api/connect/v1).

2. Create Developer Application

Sign in, open the Developer Center and apply. After an administrator approves you, create applications (one per system or environment). Each application has a client ID, a client secret (shown once), an environment, an integration type and a list of approved scopes. The integration type is only a label; permissions come from scopes.

3. Authentication

Server-to-server integrations use OAuth 2.0 Client Credentials. Exchange your client ID and secret for a short-lived access token, then send only the token on every call. Never put the secret in a browser, mobile app or URL.

4. OAuth Client Credentials

Request a token (form-encoded, JSON, or HTTP Basic auth are accepted). Ask for only the scopes you need; you'll never receive more than your approved scopes.

curl -X POST https://mplay.live/api/public/connect/v1/oauth/token \
  -d grant_type=client_credentials \
  -d client_id=mk_xxxxxxxx \
  -d client_secret=$MPLAY_CLIENT_SECRET \
  -d "scope=catalog.read packages.read"

{ "access_token": "mpat_…", "token_type": "Bearer", "expires_in": 3600,
  "scope": "catalog.read packages.read", "environment": "sandbox" }

5. Access Tokens

Tokens last 1 hour and are stored by MPLAY only as a hash. Cache the token and request a new one shortly before it expires or when you get TOKEN_EXPIRED. Rotating the secret, changing scopes, switching environment, suspending or revoking the app invalidates every token immediately.

6. API Scopes

Every endpoint needs a scope. Nothing is granted by default; an administrator approves scopes per application.

catalog.read  movies.read  series.read  collections.read  categories.read
packages.read  packages.write  customers.read
billing.transactions.write  billing.transactions.read
purchases.read  checkout.create  entitlements.read  entitlements.create  watch.create
partners.read  partner_links.create  attribution.read  commissions.read
settlements.read  settlements.write  captive_portal.create
webhooks.manage  analytics.read

7. API Requests

Send JSON over HTTPS with the bearer token. Bodies larger than 64 KB are rejected. Phone numbers are international digits without "+" (e.g. 255712345678).

curl https://mplay.live/api/public/connect/v1/movies?limit=20&page=1 \
  -H "Authorization: Bearer $TOKEN"

8. API Responses

{ "success": true, "data": { … }, "request_id": "req_8hd72k…", "api_version": "v1" }

9. Request IDs

Every response carries X-MPLAY-Request-ID and X-MPLAY-API-Version. Quote the request ID when contacting support; it links to your entry in the API logs.

10. Catalog API

GET /content            catalog.read      ?q= &category_id= &page= &limit=
GET /content/{id}       catalog.read      (series include episodes)
GET /movies             movies.read
GET /series             series.read
GET /collections        collections.read
GET /categories         categories.read

11. Packages API

GET /packages lists the MPLAY packages and your own packages (filters: kind, code, owner). POST /packages creates or updates one of your own packages. Package codes are unique across all platforms.

12. Billing API

POST /billing/purchases (billing.transactions.write) records a sale after your system has confirmed the payment. Send a unique transaction_id and idempotency_key; repeats return the original result and never create a second sale.

{ "transaction_id": "EX-10021", "idempotency_key": "EX-10021",
  "phone": "255712345678", "package_code": "WIFI-MOVIE-3000",
  "amount": 3000, "currency": "TZS", "payment_status": "successful",
  "billing_partner": "example-billing", "isp_partner": "example-isp" }

Only confirmed payments grant access. Anything else returns PAYMENT_NOT_VERIFIED and creates no entitlement.

13. Purchases API

GET /purchases and GET /purchases/{mplay_reference or your transaction_id} (purchases.read). You only ever see purchases your application created, in its own environment.

14. Entitlements API

GET /entitlements?phone=255712345678 (entitlements.read) returns active access for a customer your application has sold to. Other customers return an empty list.

15. Playback API

Playback stays inside MPLAY. Send customers to their MPLAY link (returned with each sale) or the captive portal session; video storage credentials are never shared. watch.create is reserved for approved player partners.

16. Partner API

Partner and campaign links use the existing MPLAY partner programme (/go/{code}, QR campaigns). Commission is always calculated by MPLAY's commission ledger. Scopes partners.read and partner_links.create are approved individually.

17. Attribution API

Pass partner_code (and campaign where supported) on sales and portal sessions; MPLAY attributes the purchase and commission. attribution.read is approved on request.

18. Settlement API

GET  /settlements/outstanding                          settlements.read
GET  /settlements/partners/{partner_code}/outstanding  settlements.read
GET  /settlements/invoices/{partner}                   settlements.read
POST /settlements/payments                             settlements.write
GET  /settlements/payments/{reference}                 settlements.read

Money owed to MPLAY and commission owed to partners are kept separate and never offset.
Submitted payments are only applied after verification.

19. Captive Portal API

POST /captive-portal/session (captive_portal.create) returns a short-lived, single-use redirect that carries partner and campaign attribution. Send only what's needed; a browser-supplied MAC or IP is never treated as identity.

20. Webhooks

POST   /webhooks        { "url": "https://example.com/mplay", "events": ["purchase.*", "settlement.paid"] }
GET    /webhooks
DELETE /webhooks/{id}
POST   /webhooks/test

Events: content.*, customer.*, purchase.*, entitlement.*, commission.*, settlement.*, watch.*. Each delivery body is { id, type, api_version, created_at, reference, data }. The signing secret is returned once at creation.

21. Webhook Security

Check the signature, reject timestamps older than 5 minutes, and store X-MPLAY-Event-ID so duplicates are ignored. Reply 2xx within 8 seconds. Failed deliveries retry with exponential backoff up to 8 times, then show as failed in your dashboard.

// Node.js
import crypto from "node:crypto";
const ts = req.headers["x-mplay-timestamp"];
const sig = req.headers["x-mplay-signature"].split("v1=")[1];
const expected = crypto.createHmac("sha256", process.env.MPLAY_WEBHOOK_SECRET)
  .update(`${ts}.${rawBody}`).digest("hex");
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
if (!fresh || !crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) return res.status(401).end();

22. Rate Limits

Limits are set per application (and optionally per endpoint). Responses include X-RateLimit-Limit and X-RateLimit-Reset; if you go over, you get 429 RATE_LIMITED with Retry-After.

23. Errors

{ "success": false,
  "error": { "code": "INSUFFICIENT_SCOPE", "message": "…" },
  "request_id": "req_…" }

Codes: UNAUTHORIZED, INVALID_CLIENT, INVALID_TOKEN, TOKEN_EXPIRED, INSUFFICIENT_SCOPE, APPLICATION_SUSPENDED, RESOURCE_NOT_FOUND, VALIDATION_ERROR, DUPLICATE_REQUEST, PAYMENT_NOT_VERIFIED, RATE_LIMITED, WEBHOOK_ERROR, PAYLOAD_TOO_LARGE, INTERNAL_ERROR. Older endpoints keep their original body format for compatibility.

24. Sandbox

New applications start in sandbox. Sandbox sales are processed and visible to you but create no real commission or settlement debt, and sandbox tokens can't read production purchases.

25. Production

Switch an application to live from the Developer Center once testing passes (live mode requires an approved application). Tokens issued in sandbox stop working after the switch; request a new one.

26. API Logs

The Developer Center shows 7-day call and error counts and your latest calls (endpoint, status, time, request ID). Secrets, tokens and payment details are never logged.

27. Security Best Practices

Keep secrets on your server, rotate them regularly and immediately if exposed, request least-privilege scopes, allow-list your server IPs, verify every webhook, use idempotency keys for money operations, and never send customer PINs or passwords to MPLAY.

28. Migrating from signed requests

Existing integrations that sign each request with X-MPLAY-Key-Id / X-MPLAY-Signature keep working unchanged. To migrate, use the same key ID as client_id and your secret as client_secret to get a token, then replace the signature headers with Authorization: Bearer. Signed requests will only be retired through a versioned, announced process.

Reference

Signed requests and billing details

Detailed payloads for billing, packages, settlements and the captive portal. Every endpoint below also accepts an OAuth bearer token instead of the signature headers.

1. Credentials

Sign in and apply on the developer access page. Once an MPLAY administrator approves your application you receive a Key ID and a Secret. The secret is shown once. Every connection starts in test mode, where sales are processed but create no real commission.

2. Signing a request

Every call carries three headers. The signature is HMAC-SHA256 over the timestamp, a dot, and the exact raw body (empty string for GET requests). Requests older than the configured window are rejected.

X-MPLAY-Key-Id: <your key id>
X-MPLAY-Timestamp: <unix seconds>
X-MPLAY-Signature: hex( hmac_sha256(secret, timestamp + "." + rawBody) )
Content-Type: application/json

3. Discover and create packages

GET /api/public/connect/v1/packages returns the official MPLAY packages plus the packages your system created (owner: mplay or self). Filters: ?kind=internet|content|bundle, ?code=CODE, ?owner=mine|mplay|all, ?include_inactive=1. Sign over an empty body.

POST /api/public/connect/v1/packages creates or updates (by code) one of your own packages, active immediately. Fields: code, name, kind (internet | content | bundle), price_tzs, validity_hours, internet_value_tzs, entertainment_value_tzs, entertainment_type (movie | series), selection_quantity, series_scope, data_limit_mb, speed_mbps, device_limit, status, attributes (up to 30 extra key/values). Codes owned by MPLAY or another partner return 409 code_taken. Sales using your package code must come from your own key. Internet-only packages do not grant movie passes.

4. Report a confirmed sale

POST /api/public/connect/v1/billing/purchases. Only confirmed payments are accepted. Retries are safe: transaction_id is the idempotency key, and a repeat returns the original result.

{
  "transaction_id": "NB-2026-0001",
  "phone": "255712345678",
  "package_code": "INTERNET_MOVIE_3000",
  "amount": 3000,
  "currency": "TZS",
  "billing_partner": "nanobill",
  "isp_partner": "mkinguzinet",
  "partner_code": "testpartner",
  "payment_status": "successful",
  "paid_at": "2026-01-05T10:15:00Z"
}
201 Created
{
  "success": true,
  "status": "processed",
  "mplay_reference": "cnx_...",
  "entitlement": {
    "type": "MOVIE_PASS",
    "allowed_selections": 1,
    "used_selections": 0,
    "remaining_selections": 1
  },
  "commission": { "rate": 20, "amount": 300, "currency": "TZS", "sandbox": false }
}

5. Errors

  • 401 invalid_signature / stale_timestamp / invalid_key — check the secret and clock.
  • 403 ip_not_allowed — your server IP is not on the allowlist.
  • 422 unknown_package / amount_mismatch / payment_not_confirmed / invalid_phone
  • 429 rate_limited — slow down and retry.
  • 503 integration_disabled — the programme is switched off.

Every response carries an x-request-id header. Quote it when contacting support.

6. Check a sale or a pass

GET /api/public/connect/v1/status?transaction_id=NB-2026-0001 returns the sale we recorded and the passes it created. GET /api/public/connect/v1/status?entitlement=ent_… returns how many picks are left on a pass. Called with no query it is a signed health check.

7. What the customer sees

The customer signs in with the same phone number and picks their titles from the normal catalogue. Watching, downloads and progress work exactly as with a direct purchase.

Partner settlement

Outstanding balances & payments

When a bundle is sold, the internet provider collects the customer's money. Out of the movie share of that price the provider keeps its agreed commission and the remainder becomes a balance the provider owes MPLAY. The billing partner controls that invoice: it issues it and collects the payment on our behalf. A balance stays in accruing until it reaches the provider's collection threshold, then becomes payable. This is the opposite direction to partner commission, which MPLAY pays out to you and is never netted off here.

Signing is identical to the rest of the Connect API. Your connection additionally needs the settlements.read and/or settlements.write scope, and may be limited to a list of partner codes.

Payload changes

Phone numbers are international format without the plus sign (255712345678, 254712345678). A leading + and Tanzanian local formats such as 0712345678 are still accepted, and responses always return the number as international digits with no plus. Billing purchases now require two partner identifiers: billing_partner, the organisation that collected the money and therefore settles with MPLAY, and isp_partner, the internet provider whose network carried the customer (used for reporting). partner_code remains the optional marketing partner who earns commission. A sale missing either identifier is rejected with 422 missing_partner_identifiers. Administrators can switch this requirement off in Admin → Connect → Settings ("Require billing + ISP partner") for older integrations.

POST /api/public/connect/v1/billing/purchases
{
  "transaction_id": "TXN-1001",
  "phone": "255712345678",
  "package_code": "INTERNET_MOVIE_3000",
  "amount": 3000,
  "currency": "TZS",
  "payment_status": "successful",
  "billing_partner": "nanobill",     // required — controls and collects the provider invoice
  "isp_partner": "mkinguzinet",      // required — internet provider; keeps its share, owes MPLAY the rest
  "partner_code": "testpartner"      // optional — earns commission
}
422 Unprocessable Entity
{
  "success": false,
  "error": "missing_partner_identifiers",
  "message": "Both billing_partner and isp_partner are required.",
  "details": [{ "field": "isp_partner", "issue": "required" }]
}

Endpoints

GET  /api/public/connect/v1/settlements/outstanding
GET  /api/public/connect/v1/settlements/outstanding/{partner_code}
GET  /api/public/connect/v1/settlements/outstanding/isp/{isp_partner_code}
GET  /api/public/connect/v1/settlements/invoices/{partner_code}?page=1&limit=20
POST /api/public/connect/v1/settlements/payments
GET  /api/public/connect/v1/settlements/payments/{payment_reference}
GET /settlements/outstanding
{
  "success": true,
  "currency": "TZS",
  "total_outstanding": 1250000,
  "partner_count": 3,
  "isp_count": 2,
  "partners": [ { "partner_code": "nanobill", "outstanding_amount": 750000 } ],
  "isp_partners": [
    { "isp_partner_code": "mkinguzinet", "isp_partner_name": "MkinguziNet",
      "outstanding_amount": 900000, "partner_count": 2 }
  ]
}
curl -X POST https://mplay.live/api/public/connect/v1/settlements/payments \
  -H "X-MPLAY-Key-Id: $KEY_ID" \
  -H "X-MPLAY-Timestamp: $TS" \
  -H "X-MPLAY-Signature: $SIG" \
  -H "Content-Type: application/json" \
  -d '{
    "partner_code": "nanobill",
    "invoice_reference": "INV-20260210-001",
    "amount": 150000,
    "currency": "TZS",
    "payment_reference": "MPESA-QW12345",
    "idempotency_key": "pay-2026-02-10-01"
  }'
201 Created
{
  "success": true,
  "payment_reference": "MPESA-QW12345",
  "status": "verified",
  "amount_paid": 150000,
  "previous_outstanding": 150000,
  "remaining_outstanding": 0,
  "settlement_status": "paid",
  "invoice_reference": "INV-20260210-001",
  "currency": "TZS"
}

Verification, full payment and idempotency

  • MPLAY matches your payment reference against its own confirmed money trail before reducing a balance.
  • A reference we cannot match is stored as pending_verification and changes nothing until finance approves it.
  • Partial payments are NOT accepted: amount must equal the invoice’s full outstanding balance, otherwise the call returns 422 PARTIAL_PAYMENT_NOT_ALLOWED.
  • A repeated payment_reference returns 409 DUPLICATE_PAYMENT; a repeated idempotency_key with the same body replays the original result.
  • Reversals and refunds raise the balance again and are written to the audit trail.

Webhooks

Set a webhook address and secret on your connection to receive settlement events. Signature is HMAC-SHA256 over timestamp + "." + body, sent as X-MPLAY-Signature. Retries use exponential backoff, so treat event_id as an idempotency key.

Events: settlement.created, settlement.updated, settlement.payment_received,
        settlement.partially_paid, settlement.paid, settlement.overdue,
        settlement.payment_reversed

{
  "event": "settlement.paid",
  "event_id": "evt_9f2c1b7a4d33",
  "occurred_at": "2026-02-10T09:14:00Z",
  "partner_code": "nanobill",
  "invoice_reference": "INV-20260210-001",
  "currency": "TZS",
  "remaining_outstanding": 0,
  "status": "paid"
}

Errors

401 UNAUTHORIZED           bad signature, key or timestamp
403 INSUFFICIENT_SCOPE     missing settlements scope, or partner not allowed
404 INVALID_PARTNER        unknown partner code
404 INVOICE_NOT_FOUND      unknown or cancelled invoice
404 PAYMENT_NOT_FOUND      unknown payment reference
409 DUPLICATE_PAYMENT      reference already recorded
409 IDEMPOTENCY_CONFLICT   key reused with a different payload
409 ALREADY_PAID           nothing outstanding on this invoice
422 INVALID_AMOUNT         zero, negative or more than outstanding
422 INVALID_CURRENCY       currency does not match the invoice
429 RATE_LIMITED           slow down and retry

Reconciliation tip: read /settlements/outstanding/{partner_code} after each payment and compare it with your own ledger; the value is always recomputed from invoices, adjustments and verified payments.

Captive portal integration

MikroTik / WiFi hotspot

A hotspot login page can show MPLAY titles before the visitor is online, and can hand the visitor to MPLAY with partner or campaign tracking attached. Preview never unlocks anything: paid titles still go through the normal sign-in, payment and entitlement rules.

Architecture

MikroTik hotspot -> preview API -> movie cards -> customer taps a title
  -> MPLAY -> login/register -> payment -> entitlement -> watch

MikroTik -> partner/campaign link -> MPLAY -> attribution -> purchase -> commission ledger

Preview API — PUBLIC

GET /api/connect/v1/captive-portal/movies (mirrored at /api/public/connect/v1/captive-portal/movies). No key required, cached for two minutes, rate limited per IP. Default limit 10, maximum 50.

GET /api/connect/v1/captive-portal/movies?page=1&limit=10&type=movie&category=action&featured=1

{
  "success": true,
  "data": [
    {
      "id": "PUBLIC_CONTENT_ID",
      "title": "MUTINY",
      "type": "movie",
      "poster_url": "https://...",
      "price": 1000,
      "currency": "TZS",
      "is_free": false,
      "episodes": null,
      "year": 2024,
      "preview_url": "https://mplay.live/title/PUBLIC_CONTENT_ID"
    }
  ],
  "meta": { "page": 1, "limit": 10, "total": 50 }
}

Movie card example

<a class="movie-card" href="PREVIEW_URL">
  <img src="POSTER_URL" alt="MOVIE TITLE">
  <div class="movie-card-info"><strong>MOVIE TITLE</strong></div>
</a>

Partner & campaign tracking — PUBLIC

/connect/<PARTNER_CODE> and /c/<CAMPAIGN_CODE> are aliases of the existing entry points /a/<CODE> and /api/public/go/<CODE>. Attribution survives click, landing, sign-up, login, payment and purchase, and feeds the one existing commission ledger.

Session API — SERVER-TO-SERVER

POST /api/connect/v1/captive-portal/session, signed with the same three headers as every other Connect call. Returns a short-lived, single-use, non-guessable redirect. IP and MAC may be sent for your own correlation but are never stored and are never treated as proof of identity.

{
  "partner_code": "PARTNER001",
  "campaign_code": "WIFI001",
  "router_ref": "ROUTER001",
  "session_ref": "SESSION123",
  "return_url": "https://mplay.live/"
}
201 Created
{
  "success": true,
  "session_token": "...",
  "expires_at": "2026-01-05T10:30:00.000Z",
  "redirect_url": "https://mplay.live/connect/session/..."
}

Walled garden configuration

Keep it to three lines. MPLAY itself, plus one wildcard for every poster and image served by the CDN — nothing else is needed.

/ip hotspot walled-garden
add dst-host=mplay.live
add dst-host=www.mplay.live
add dst-host=*.b-cdn.net

Do not add individual pull-zone hostnames, media aliases or extra CDN domains: *.b-cdn.net covers all MPLAY artwork. Only add your own billing platform host if the portal page calls it directly.

Security, limits and errors

  • No Bunny credentials, playback URLs, payment secrets or customer data in any portal response.
  • Session tokens are random, signed, single-use, short-lived and replay-protected.
  • Rate limit and allowed portal web addresses are set by an administrator.
  • 503 captive_portal_disabled, 429 rate_limited, 422 invalid_request, 401 invalid_signature / stale_timestamp, 403 attribution_disabled.

Troubleshooting

  • Blank cards: check the walled garden allows MPLAY and the poster hostname.
  • Empty list: the portal API may be switched off, or “featured only” may be enabled.
  • Redirect lands without tracking: the session link is single-use — mint a new one per visitor.
  • 401 responses: check the shared secret and the router clock.