AppsPro Developer Guide

AppsPro is a subscription management platform built on top of the Dialog Axiata BDApps telecom API. This guide covers what you need to integrate your app with AppsPro: configuring the BDApps portal, verifying subscriptions from your backend, and receiving signed webhook events.

How it works

1Sign up on appspro.dev, create an app, and link your BDApps credentials (applicationId + password).
2Paste three flat URLs into your BDApps developer portal so BDApps can deliver SMS / USSD / subscription events to AppsPro.
3End-users subscribe through the hosted checkout page — OTP verification is handled for you.
4Verify subscriptions and list subscribers from your backend via the SDK (Bearer auth with your secret_key). Receive signed webhook events for real-time updates.
language
SDK Base URI: https://api.appspro.dev/api/v1. SDK routes use Authorization: Bearer <secret_key>. The Client ID, Client Secret, Signing Key, and Public Key are all visible in your app's API tab in the dashboard.

BDApps Integration

BDApps delivers SMS, USSD, and subscription notifications by POSTing to URLs you configure in the BDApps developer portal. Paste the three flat URLs below — they are identical for every AppsPro customer. AppsPro routes each delivery to the correct app using the BDApps applicationId in the payload.

Portal URLs

The same values appear in the BDApps Portal Configuration card on your app's Subscription tab.

POSThttps://api.appspro.dev/bdapps/sms

SMS MO callback. Paste into the BDApps portal as the SMS notification URL.No Auth

POSThttps://api.appspro.dev/bdapps/ussd

USSD MO callback. Paste into the BDApps portal as the USSD URL.No Auth

POSThttps://api.appspro.dev/bdapps/notify

Subscription notification callback (subscribe / unsubscribe / renewal events).No Auth

POSThttps://api.appspro.dev/bdapps/report

SMS delivery report callback.No Auth

How routing works

You do not pass an app_id in the URL. Every BDApps payload includes an applicationId (your bdapps_app_id), which AppsPro uses to look up the right app. The same URL serves every customer.

You can also read the configured URLs from the platform endpoint:

GET/platform/bdapps-portal-config

Returns the SMS / USSD / Notify URLs and whitelisted IPs. Useful if you want to validate the dashboard values programmatically.No Auth

json
{
  "sms_url":    "https://api.appspro.dev/bdapps/sms",
  "ussd_url":   "https://api.appspro.dev/bdapps/ussd",
  "notify_url": "https://api.appspro.dev/bdapps/notify",
  "whitelisted_ips": ["217.15.160.79"]
}

Whitelisted IPs

AppsPro's outbound traffic to BDApps originates from 217.15.160.79. Add this address to the IP whitelist in your BDApps portal so outbound SMS, charging, and OTP requests are not blocked.

Quickstart

1. Sign up & create an app

Create an account at appspro.dev and add a new app from the dashboard. In the app's settings, enter your bdapps_app_id (applicationId) and bdapps_password from your BDApps developer portal (developer.bdapps.com). The password is encrypted at rest and never returned over the API.

2. Copy your API credentials

Open your app's API tab. You'll find:

Base URI

https://api.appspro.dev/api/v1 — the root for SDK calls

publishable_key

Client-side safe — used in WebSDK init code and /sdk/app-info (e.g. pk_…)

secret_key

Server-side only — Bearer token for SDK calls AND HMAC key for verifying outbound webhook signatures. Shown once on regenerate.

url_slug

Short share handle for the hosted checkout URL: appspro.dev/s/<slug>. Stable across credential rotations.

3. Configure your BDApps portal

Open your app's Subscription tab to view the BDApps Portal Configuration card. Paste the SMS URL, USSD URL, and Subscription Notification URL into the matching fields in your BDApps developer portal, and whitelist AppsPro's outbound IP. See BDApps Integration for the full list of URLs.

4. Make your first API call

List your current subscribers with your secret_key:

bash
curl -H "Authorization: Bearer YOUR_SECRET_KEY" \
  "https://api.appspro.dev/api/v1/sdk/subscribers?limit=20"

SDK Guide

Authentication

Every authenticated SDK call uses an API key in a Authorization: Bearer header. The key is your app's secret_key (starts with sk_) — copy it once from the API tab after regenerating.

http
Authorization: Bearer sk_<your-secret>
warning

Keep credentials secure

Never expose your secret_key in client-side code. It's the Bearer token for SDK calls AND the HMAC key for webhook signature verification — both server-side concerns.

Verify Subscription

Check whether a subscriber has an active subscription. The {subscriber_id} path segment is the BDApps subscriber ID returned by /sdk/subscribers (e.g. tel:8801XXXXXXXXX), not the internal AppsPro UUID.

GET/api/v1/sdk/verify/{subscriber_id}

Returns subscription validity, subscriber details, and reason if invalid.

json
{
  "valid": true,
  "subscriber": {
    "id": "550e8400-e29b-41d4-a716-446655440000",
    "bdapps_subscriber_id": "tel:8801712345678",
    "phone_masked": "01712***678",
    "status": "active",
    "subscription_type": "bdapps",
    "frequency": "daily",
    "subscribed_at": "2026-05-11T10:30:00Z",
    "cancelled_at": null,
    "created_at": "2026-05-11T10:30:00Z"
  },
  "reason": null
}

List Subscribers

Paginated list of all subscribers for your app. Filter by status.

GET/api/v1/sdk/subscribers?page=1&limit=20&status=active

Returns paginated subscriber list with total count.

json
{
  "subscribers": [
    {
      "id": "...",
      "bdapps_subscriber_id": "tel:8801712345678",
      "phone_masked": "01712***678",
      "status": "active",
      "subscription_type": "bdapps",
      "frequency": "daily",
      "subscribed_at": "2026-05-11T10:30:00Z",
      "cancelled_at": null,
      "created_at": "2026-05-11T10:30:00Z"
    }
  ],
  "total": 42,
  "page": 1,
  "limit": 20
}

App Info (Public)

Retrieve public metadata for your app. No authentication — only the publishable_key.

GET/api/v1/sdk/app-info?publishable_key=pk_…

Public app metadata. Used by the WebSDK to render the widget header.No Auth

json
{
  "name": "Deendar",
  "description": "Daily Islamic content",
  "category": "islamic",
  "pricing_model": "subscription",
  "icon_url": "https://api.appspro.dev/uploads/icons/abc.png",
  "publishable_key": "pk_..."
}

Subscription Status (by phone)

Query upstream BDApps for a phone number's subscription state. Unlike /sdk/verify (which reads local DB and is keyed by subscriber_id), this hits BDApps live and accepts a raw 01XXXXXXXXX phone. Useful when you don't have the subscriber id yet, or want truth even before our webhook lands.

POST/api/v1/sdk/status

Live BDApps subscription status for a phone number.

json
// Request
{ "phone": "01712345678" }

// Response — SubscriptionStatusOut
{
  "subscription_status": "REGISTERED",  // or UNREGISTERED, etc.
  "status_code": "S1000",
  "status_detail": "Success",
  "raw": { /* full BDApps response */ }
}

Request OTP

Send a one-time password to the phone number. BDApps delivers the OTP as a real SMS. The returned reference_no must be passed back to /sdk/otp/verify. Rate-limited to 10 requests / hour / phone / app.

POST/api/v1/sdk/otp/request

Send an OTP SMS to a phone number.

json
// Request
{ "phone": "01712345678" }

// Response — OTPRequestOut
{
  "reference_no": "bdapps_ref_abc123",
  "status_code": "S1000",
  "status_detail": "Success",
  "raw": { /* full BDApps response */ }
}

Verify OTP

Verify the OTP entered by the user. On success the subscriber is created locally and a subscriber.created (or subscriber.reactivated) webhook fires.

POST/api/v1/sdk/otp/verify

Verify OTP and register the subscription.

json
// Request
{ "reference_no": "bdapps_ref_abc123", "otp": "1234" }

// Response — OTPVerifyOut
{
  "subscription_status": "REGISTERED",
  "subscriber_id": "tel:8801712345678",
  "local_subscriber_id": "550e8400-...",
  "status_code": "S1000",
  "status_detail": "Success",
  "raw": { /* full BDApps response */ }
}

Subscribe (no OTP)

Directly subscribe a phone number, bypassing OTP. Use for trusted server-to-server flows (e.g. you've already verified the user via your own auth). For end-user signups, prefer the OTP flow above.

POST/api/v1/sdk/subscribe

Directly subscribe a phone number.

json
// Request
{ "phone": "01712345678" }

// Response — BDAppsResponse
{
  "status_code": "S1000",
  "status_detail": "Success",
  "raw": { /* full BDApps response */ }
}

Unsubscribe

Unsubscribe a phone number. On success the local subscriber moves to CANCELLED and a subscriber.cancelled webhook fires. Counted as success when BDApps returns eitherstatusCode: S1000 orsubscriptionStatus: UNREGISTERED.

POST/api/v1/sdk/unsubscribe

Unsubscribe a phone number.

json
// Request
{ "phone": "01712345678" }

// Response — BDAppsResponse
{
  "status_code": "S1000",
  "status_detail": "Success",
  "raw": { /* full BDApps response, may include "subscriptionStatus": "UNREGISTERED" */ }
}

Generate Token

Short-lived (5-minute) JWT used by the WebSDK's iframe flow. The token is scoped to the requesting origin (Origin/Referer header).

POST/api/v1/sdk/generate-token

Issue an embed token for the WebSDK.No Auth

json
// Request
{ "publicKey": "..." }

// Response
{ "token": "eyJhbGciOi...", "expires_in": 300 }

Checkout Flow

Hosted Checkout Page

AppsPro provides a hosted checkout page for end-user subscriptions. Share or embed the checkout URL in your app. Users subscribe via OTP — no integration work needed on your end.

https://appspro.dev/s/{url_slug}

Flow

  1. User visits /s/{url_slug} — sees your app info
  2. User enters their Bangladeshi mobile number
  3. OTP is sent via BDApps SMS
  4. User verifies OTP on the checkout page
  5. Subscription is created — user is redirected to the configured checkout_redirect_url (set in the app's settings tab in the dashboard)

Checkout Endpoints

These endpoints power the hosted checkout page. No authentication required.

GET/s/{url_slug}/info

Public app info for the checkout UI.No Auth

json
// CheckoutAppInfo
{
  "name": "Deendar",
  "description": "Daily Islamic content",
  "icon_url": "https://api.appspro.dev/uploads/icons/abc.png",
  "pricing_model": "subscription",
  "category": "islamic"
}
POST/s/{url_slug}/otp/request

Send OTP to a phone number. Rate limited to 10/hour per phone.No Auth

json
// Request
{ "phone": "01712345678" }

// Response — OTPRequestOut
{
  "status_code": "S1000",
  "status_detail": "Success",
  "raw": { /* full BDApps response */ },
  "reference_no": "bdapps_ref_abc123"
}
POST/s/{url_slug}/otp/verify

Verify OTP and create the subscription.No Auth

json
// Request
{ "reference_no": "bdapps_ref_abc123", "otp": "1234" }

// Response — CheckoutOTPVerifyOut
{
  "status_code": "S1000",
  "status_detail": "Success",
  "raw": { /* full BDApps response */ },
  "subscription_status": "REGISTERED",
  "subscriber_id": "tel:8801712345678",
  "local_subscriber_id": "550e8400-...",
  "redirect_url": "https://yourapp.com/welcome"
}
info

Phone number formats

Accepts 01XXXXXXXXX, 8801XXXXXXXXX, or +8801XXXXXXXXX — with or without spaces and dashes.

Webhooks

Configure a webhook URL in your app settings to receive real-time events. Subscribe to only the events you care about — only events in events: [...] are delivered.

Event Catalog

Subscription lifecycle

subscriber.createdOTP verified — new subscriber registered
subscriber.cancelledSubscriber unsubscribed
subscriber.reactivatedPreviously cancelled subscriber re-registered
subscriber.<status>Dynamic events for BDApps subscription_status (e.g. subscriber.registered, subscriber.unregistered)
subscriber.unknown.<status>Fallback for unrecognised BDApps statuses

Inbound messaging (from BDApps webhooks)

sms.receivedIncoming SMS from a subscriber
ussd.receivedIncoming USSD message

Hosted checkout

checkout.otp.requestedOTP requested via public hosted checkout
checkout.otp.verify.failedOTP verify failed via public hosted checkout

Subscription Event Payload

json
POST https://your-server.com/webhooks/appspro

Headers:
  Content-Type: application/json
  X-Event-Type: subscriber.created
  X-Signature: 7c1f...     // hex HMAC-SHA256, NO "sha256=" prefix

Body (canonical JSON — keys sorted):
{
  "data": {
    "applicationId": "BDAPPS_123",
    "frequency": "daily",
    "internal_subscriber_id": "550e8400-...",
    "status": "REGISTERED",
    "subscriberId": "tel:8801712345678",
    "timeStamp": "2026-05-11T10:30:00Z"
  },
  "event": "subscriber.created"
}

SMS Event Payload

json
{
  "event": "sms.received",
  "data": {
    "applicationId": "BDAPPS_123",
    "sourceAddress": "tel:8801712345678",
    "message": "Hello from subscriber",
    "requestId": "req_xyz",
    "encoding": "0"
  }
}

Signature Verification

Every webhook request includes an X-Signature header containing an HMAC-SHA256 hex digest of the request body, signed with your app's secret_key (the same key you use as the SDK Bearer token). The signed payload is canonical JSON: json.dumps(body, sort_keys=True). Verifiers must canonicalise the same way before hashing — comparing against the raw request bytes only works if your framework leaves them untouched, which most do not.

python
import hmac, hashlib, json

def verify_signature(parsed_body: dict, signature: str, secret_key: str) -> bool:
    canonical = json.dumps(parsed_body, sort_keys=True)
    expected = hmac.new(
        secret_key.encode(),
        canonical.encode(),
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Code Examples

Verify Subscription (Python)

python
import requests

response = requests.get(
    "https://api.appspro.dev/api/v1/sdk/verify/tel:8801712345678",
    headers={"Authorization": f"Bearer {SECRET_KEY}"},
)

data = response.json()
if data["valid"]:
    print(f"Active subscriber: {data['subscriber']['id']}")
else:
    print(f"Invalid: {data['reason']}")

Verify Subscription (JavaScript)

javascript
const res = await fetch(
  "https://api.appspro.dev/api/v1/sdk/verify/tel:8801712345678",
  { headers: { Authorization: `Bearer ${secretKey}` } }
);

const data = await res.json();
if (data.valid) {
  console.log("Active subscriber:", data.subscriber.id);
} else {
  console.log("Invalid:", data.reason);
}

Handle Webhook (Node.js / Express)

javascript
import crypto from "crypto";
import express from "express";

const app = express();
app.use(express.json());  // we re-canonicalise from the parsed body

app.post("/webhooks/appspro", (req, res) => {
  const signature = req.headers["x-signature"];
  const eventType = req.headers["x-event-type"];

  // Canonicalise: keys sorted, same as backend's json.dumps(..., sort_keys=True)
  const canonical = JSON.stringify(req.body, Object.keys(req.body).sort());
  const expected = crypto
    .createHmac("sha256", process.env.SIGNING_KEY)
    .update(canonical)
    .digest("hex");

  if (
    !signature ||
    signature.length !== expected.length ||
    !crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))
  ) {
    return res.status(401).send("Invalid signature");
  }

  switch (eventType) {
    case "subscriber.created":
      console.log("New subscriber:", req.body.data.subscriberId);
      break;
    case "subscriber.cancelled":
      console.log("Unsubscribed:", req.body.data.subscriberId);
      break;
    case "subscriber.reactivated":
      console.log("Reactivated:", req.body.data.subscriberId);
      break;
    case "sms.received":
      console.log("SMS from:", req.body.data.sourceAddress);
      break;
    case "debit.charged":
      console.log("Charged:", req.body.data);
      break;
  }
  res.sendStatus(200);
});

Handle Webhook (Python / FastAPI)

python
import json, hmac, hashlib, os
from fastapi import FastAPI, Request, HTTPException

app = FastAPI()
SIGNING_KEY = os.environ["SIGNING_KEY"]

@app.post("/webhooks/appspro")
async def receive(req: Request):
    raw = await req.body()
    body = json.loads(raw)

    canonical = json.dumps(body, sort_keys=True)
    expected = hmac.new(SIGNING_KEY.encode(), canonical.encode(), hashlib.sha256).hexdigest()
    sig = req.headers.get("x-signature", "")
    if not hmac.compare_digest(expected, sig):
        raise HTTPException(status_code=401, detail="Invalid signature")

    event = req.headers.get("x-event-type") or body.get("event")
    data = body.get("data", {})
    if event == "subscriber.created":
        ...  # handle new subscriber
    elif event == "debit.charged":
        ...  # mark order paid
    return {"ok": True}

List Subscribers (cURL)

bash
curl -H "Authorization: Bearer YOUR_SECRET_KEY" \
  "https://api.appspro.dev/api/v1/sdk/subscribers?page=1&limit=20&status=active"

WebSDK

Overview

Embed a subscription widget directly in your website or mobile app without redirecting users to a separate page. The widget is injected as plain HTML into your container — no iframe, no postMessage. This makes it reliable in all mobile WebViews (Android/iOS) as well as standard browsers.

How it works

1Your page loads appspro.js from https://appspro.dev/sdk/v1/appspro.js
2AppsPro(publicKey) creates an SDK instance
3sdk.elements.create('subscribe', options) returns a SubscribeElement
4element.mount('#container') fetches /sdk/app-info and injects the widget inline
5Phone → OTP → success runs in the page; events fire on the element

Installation & Usage

1. Include the script

html
<script src="https://appspro.dev/sdk/v1/appspro.js"></script>

2. Add a container and initialize

html
<!-- In your HTML -->
<div id="subscribe-box"></div>

<script>
  const sdk = AppsPro('YOUR_PUBLIC_KEY', {
    baseUrl: 'https://api.appspro.dev',  // required in production
  });

  const el = sdk.elements.create('subscribe', {
    buttonText:  'Subscribe Now',
    buttonColor: '#6366f1',
    theme:       'dark',     // 'dark' | 'light'
    compact:     false,
    hideHeader:  false,
    borderRadius: '12px',
  });

  el.mount('#subscribe-box');

  el.on('ready',   ()     => console.log('Widget ready'));
  el.on('otp-sent',(data) => console.log('OTP sent to', data.phone));
  el.on('success', (data) => {
    console.log('Subscribed!', data.subscriberId);
    // Verify on your server:
    // GET /api/v1/sdk/verify/<data.subscriberId>
  });
  el.on('payment-redirect', (data) => location.href = data.url);
  el.on('error',   (err)  => console.error('Error:', err.message));
</script>
play_circleLive DemoInteractive

Options

SDK init

javascript
AppsPro(publicKey, {
  baseUrl: string,   // required: 'https://api.appspro.dev'
                     // the SDK file is on appspro.dev but the API lives on
                     // api.appspro.dev, so this must be set explicitly
})

elements.create('subscribe', ...)

javascript
sdk.elements.create('subscribe', {
  buttonText:   string,   // default: 'Subscribe Now'
  buttonColor:  string,   // default: '#6366f1' (CSS hex)
  theme:        string,   // 'dark' | 'light', default: 'dark'
  compact:      boolean,  // default: false — tighter padding
  hideHeader:   boolean,  // default: false — hide app-name header
  borderRadius: string,   // default: '12px'
})

Events

EventPayloadDescription
ready{}Widget has rendered in the DOM
otp-sent{ phone }OTP was sent to the phone
success{ subscriberId, localSubscriberId, redirectUrl }User subscribed successfully
payment-redirect{ url }Redirect URL is available (forwarded from OTP verify response)
error{ message }An error occurred in the flow

Checkout Helpers

The SDK also exposes shortcuts for hosted checkout flows, in case you don't want to embed the widget.

javascript
const sdk = AppsPro('YOUR_PUBLIC_KEY', {
  baseUrl: 'https://api.appspro.dev',
});

// Just build a URL
const url = sdk.createCheckoutUrl({
  redirectUrl: 'https://your-site.com/success',
});

// Open in a popup window
sdk.openCheckout({
  redirectUrl: 'https://your-site.com/success',
  width: 460,
  height: 600,
});

// Update an already-mounted element's options
const el = sdk.elements.create('subscribe', { theme: 'dark' });
el.mount('#box');
el.update({ buttonColor: '#10b981' });
el.unmount();

Flutter / WebView Integration

For Flutter apps, load the WebSDK inside a WebView using the webview_flutter package. The SDK automatically calls a JavaScript channel named AppsPro with every event, so no extra wiring is needed on the JS side.

dart
import 'dart:convert';
import 'package:webview_flutter/webview_flutter.dart';

class SubscribeWebView extends StatefulWidget {
  final String publicKey;
  const SubscribeWebView({required this.publicKey, super.key});

  @override
  State<SubscribeWebView> createState() => _SubscribeWebViewState();
}

class _SubscribeWebViewState extends State<SubscribeWebView> {
  late final WebViewController _controller;

  @override
  void initState() {
    super.initState();
    _controller = WebViewController()
      ..setJavaScriptMode(JavaScriptMode.unrestricted)
      ..addJavaScriptChannel(
        'AppsPro',  // SDK calls window.AppsPro.postMessage() automatically
        onMessageReceived: (msg) {
          final data = jsonDecode(msg.message) as Map<String, dynamic>;
          if (data['type'] == 'success') {
            final subscriberId = data['data']['subscriberId'];
            Navigator.pop(context, subscriberId);
          }
        },
      )
      ..loadHtmlString(_buildHtml());
  }

  String _buildHtml() => """
<!DOCTYPE html><html><head>
<meta name="viewport" content="width=device-width,initial-scale=1">
<script src="https://appspro.dev/sdk/v1/appspro.js"></script>
</head><body style="margin:0;background:#0f0f13">
<div id="sub"></div>
<script>
  const sdk = AppsPro('${widget.publicKey}', { baseUrl: 'https://api.appspro.dev' });
  const el = sdk.elements.create('subscribe', { hideHeader: true });
  el.mount('#sub');
</script></body></html>""";

  @override
  Widget build(BuildContext context) => WebViewWidget(controller: _controller);
}

Embed

A standalone HTML checkout page hosted by AppsPro, useful when you need an iframe rather than the inline WebSDK injection. Returns HTML.

GET/embed/subscribe

Iframe-friendly subscribe page. Query: publishable_key, token (from /sdk/generate-token), theme, locale, button_text, button_color, compact, hide_header.No Auth

html
<iframe
  src="https://api.appspro.dev/embed/subscribe?publishable_key=YOUR_PUBLISHABLE_KEY&theme=dark&button_text=Subscribe"
  style="border:0;width:100%;height:600px"
  allow="clipboard-write"
></iframe>

AI Agent / LLM Docs

Copy the markdown below and paste it into your AI assistant (Claude, ChatGPT, Cursor, etc.) to give it full context about the AppsPro API.

docs.md
# AppsPro API — Customer Reference for AI Agents

AppsPro is a subscription-management platform built on top of Dialog Axiata BDApps.
This document covers the **customer-facing** API surface only — what a developer
integrating their app with AppsPro can call. Internal dashboard endpoints are
not included.

Base URL: https://api.appspro.dev
SDK Base URI (shown in your dashboard): https://api.appspro.dev/api/v1

---

## Credentials (issued per app in the AppsPro dashboard)

- publishable_key — client-side safe (e.g. "pk_..."). Used in WebSDK init
                    code (AppsPro('pk_...')) and as a query param on the
                    /sdk/app-info and /embed/subscribe public endpoints.
- secret_key      — server secret (e.g. "sk_..."). The Authorization
                    Bearer token for /api/v1/sdk/* calls AND the HMAC-SHA256
                    key for verifying outbound webhook signatures. Shown
                    once on regenerate.
- url_slug        — short 10-char base62 handle (e.g. "Hd3kF9aZ2x"). Used
                    in user-facing checkout URLs: appspro.dev/s/<slug>.
                    Stable across credential rotations.

You never use an "app_id" path parameter from your side. /sdk/* endpoints
identify your app via the Bearer key; checkout URLs identify it via the
url_slug; the WebSDK identifies it via the publishable_key.

---

## Auth schemes by route group

- /api/v1/sdk/{verify,subscribers,status,otp/*,subscribe,unsubscribe}
    → Bearer (Authorization: Bearer sk_<secret>)
- /api/v1/sdk/app-info, /api/v1/sdk/generate-token
    → no auth (uses publishable_key in body / query)
- /api/v1/discover/* — no auth (public marketplace)
- /s/{url_slug}/* — no auth (hosted checkout, called by browser)
- /bdapps/*       — no auth (called by BDApps, not by you — configured in BDApps portal)
- /embed/*        — no auth (token from /sdk/generate-token instead)
- /public/unsubscribe/* — no auth (end-user UI flow, not for customer code)

---

## BDApps Portal Configuration

In your BDApps developer portal, paste these URLs:

  SMS MO URL:     https://api.appspro.dev/bdapps/sms
  USSD MO URL:    https://api.appspro.dev/bdapps/ussd
  Notify URL:     https://api.appspro.dev/bdapps/notify
  Report URL:     https://api.appspro.dev/bdapps/report

These URLs are identical for every customer. AppsPro routes each incoming
payload to your app via BDApps' applicationId field.

Whitelist this source IP in the BDApps portal so we can reach you:
  217.15.160.79

You can also read these values programmatically:
GET /platform/bdapps-portal-config
  Response: { sms_url, ussd_url, notify_url, whitelisted_ips }

---

## SDK (Bearer auth or public)

GET /api/v1/sdk/verify/{subscriber_id}     // Bearer
  subscriber_id must be the BDApps subscriber ID returned by /sdk/subscribers,
  e.g. "tel:8801712345678" — not the internal AppsPro UUID.
  Response: { valid: bool, subscriber?: SubscriberResponse, reason?: string }

GET /api/v1/sdk/subscribers?page=&limit=&status=
  Response: { subscribers: SubscriberResponse[], total, page, limit }

POST /api/v1/sdk/status                    // Bearer
  Body: { phone }   // raw "01XXXXXXXXX"
  Response: { subscription_status, status_code, status_detail, raw }
  // Queries BDApps live (not local DB) so it's truth even before notify webhook.

POST /api/v1/sdk/otp/request               // Bearer Rate limit 10/h/phone/app.
  Body: { phone }
  Response: { reference_no, status_code, status_detail, raw }
  // Sends a real SMS. Pass reference_no back to /sdk/otp/verify.

POST /api/v1/sdk/otp/verify                // Bearer
  Body: { reference_no, otp }
  Response: { subscription_status, subscriber_id, local_subscriber_id, status_code, status_detail, raw }
  // On success, registers subscriber locally and fires subscriber.created webhook.

POST /api/v1/sdk/subscribe                 // Bearer
  Body: { phone }
  Response: { status_code, status_detail, raw }
  // Direct subscribe without OTP. For trusted server-to-server use.

POST /api/v1/sdk/unsubscribe               // Bearer
  Body: { phone }
  Response: { status_code, status_detail, raw }
  // Success if statusCode == "S1000" OR raw.subscriptionStatus == "UNREGISTERED".

GET /api/v1/sdk/app-info?publishable_key=...    // no auth (legacy ?public_key= alias accepted)
  Response: { name, description, category, pricing_model, icon_url, publishable_key }

POST /api/v1/sdk/generate-token             // no auth, scoped to Origin
  Body: { publishableKey }   // accepts legacy publicKey too
  Response: { token, expires_in: 300 }

SubscriberResponse:
  { id, bdapps_subscriber_id, phone_masked, status, subscription_type, frequency, subscribed_at, cancelled_at, created_at }

---

## Hosted Checkout (no auth)

User-facing page: https://appspro.dev/s/{url_slug}

GET  /s/{url_slug}/info
  Response: { name, description, icon_url, pricing_model, category }

POST /s/{url_slug}/otp/request
  Body: { phone }
  Response: { status_code, status_detail, raw, reference_no }

POST /s/{url_slug}/otp/verify
  Body: { reference_no, otp }
  Response: { status_code, status_detail, raw, subscription_status, subscriber_id, local_subscriber_id, redirect_url }

Phone formats accepted: 01XXXXXXXXX, 8801XXXXXXXXX, or +8801XXXXXXXXX.

---

## Discover / Marketplace (no auth)

Public catalogue of apps that have been published on AppsPro. Useful if
your app is listed and you want to surface its marketplace metadata, or
if you're building an external directory page.

GET /api/v1/discover/apps?page=&limit=&search=&category=
  Response: { apps: DiscoverAppItem[], total, page, limit }
  Pagination: page >= 1, limit in [1, 100] (default 20).

GET /api/v1/discover/apps/{url_slug}
  Response: DiscoverAppItem
  404 if the slug doesn't match a published app.

GET /api/v1/discover/apps/{url_slug}/download
  302 redirect to the latest APK URL; also increments downloads_count.
  404 if the app isn't an Android app or has no published APK.

DiscoverAppItem:
  {
    name, description, icon_url, screenshots, category, pricing_model,
    url_slug, developer_name, organization, subscriber_count, app_type,
    apk_url,
    versions: [{ version_name, release_notes, created_at }],
    pricing:  [{ method_name, display_name, price, billing_period }]
  }

---

## Webhooks (your server receives POSTs from AppsPro)

Configure your webhook URL and event list in the AppsPro dashboard.

Headers on every delivery:
  X-Event-Type: <event name>
  X-Signature:  <hex HMAC-SHA256 of canonical JSON, signed with secret_key>     // NO "sha256=" prefix
  Content-Type: application/json

Canonical body: json.dumps({ "event": <type>, "data": <payload> }, sort_keys=True)
You MUST canonicalise the parsed body with sorted keys before HMAC.
Comparing raw request bytes will not work in most frameworks.

Event types:
  subscription lifecycle:
    subscriber.created, subscriber.cancelled, subscriber.reactivated,
    subscriber.<status>, subscriber.unknown.<status>
  inbound messaging (forwarded from BDApps):
    sms.received, ussd.received
  hosted checkout:
    checkout.otp.requested, checkout.otp.verify.failed

Example subscriber.created body:
  {
    "event": "subscriber.created",
    "data": {
      "applicationId": "BDAPPS_123",
      "frequency": "daily",
      "internal_subscriber_id": "550e8400-...",
      "status": "REGISTERED",
      "subscriberId": "tel:8801712345678",
      "timeStamp": "2026-05-11T10:30:00Z"
    }
  }

---

## Embed (no auth)

GET /embed/subscribe?publishable_key=&token=&theme=&locale=&button_text=&button_color=&compact=&hide_header=
  → HTML page suitable for iframe embedding
  token comes from POST /api/v1/sdk/generate-token
  (legacy ?public_key= query is still accepted)

---

## WebSDK (browser JS — appspro.js)

Embed a subscription widget directly in your website or mobile WebView.
The widget is injected as plain HTML into your container — no iframe, no
postMessage — so it works reliably in Android/iOS WebViews.

### Install

  <script src="https://appspro.dev/sdk/v1/appspro.js"></script>

  // Legacy URL /sdk/v1/texionapps.js is still served as a symlink to
  // appspro.js but is deprecated — use the new filename in new pages.

### Init

  const sdk = AppsPro(publicKey, { baseUrl });

- publicKey: your publishable_key (e.g. "pk_..."). Safe to ship to the browser.
- baseUrl  (required): the API host, "https://api.appspro.dev". The SDK does
  not auto-detect this — the script is served from appspro.dev but the API
  lives on api.appspro.dev, so it must be passed explicitly.

### Create the subscribe element

  const el = sdk.elements.create('subscribe', {
    buttonText:   string,    // default: 'Subscribe Now'
    buttonColor:  string,    // default: '#6366f1' (CSS color)
    theme:        'dark' | 'light',  // default: 'dark'
    compact:      boolean,   // default: false — tighter padding
    hideHeader:   boolean,   // default: false — hide app-name header
    borderRadius: string,    // default: '12px'
  });

NOTE: WebSDK option keys are camelCase (buttonText, hideHeader). The
/embed/subscribe REST surface uses snake_case (button_text, hide_header).
Don't mix them.

### Element API

  el.mount(selector);    // selector string, e.g. '#subscribe-box'
  el.unmount();          // remove from DOM
  el.update(opts);       // change options on a mounted element
  el.on(event, cb);      // subscribe to events (see below)

mount() fetches /api/v1/sdk/app-info under the hood to render the header.

### Events

  ready             → {}                                                — widget has rendered in the DOM
  otp-sent          → { phone }                                         — OTP SMS was dispatched
  success           → { subscriberId, localSubscriberId, redirectUrl }  — user subscribed
  payment-redirect  → { url }                                           — forwarded from OTP verify
  error             → { message }                                       — anything failed in the flow

On 'success', verify server-side before granting paid access:
  GET /api/v1/sdk/verify/{subscriberId}   // Bearer sk_...

### Checkout helpers (no widget)

If you don't want to embed the widget, the SDK can hand you a hosted-checkout
URL or open it in a popup.

  const url = sdk.createCheckoutUrl({ redirectUrl: 'https://your-site.com/welcome' });
  // → "https://appspro.dev/s/<url_slug>?redirect_url=..."

  sdk.openCheckout({
    redirectUrl: 'https://your-site.com/welcome',
    width:  460,
    height: 600,
  });
  // Opens a centered popup window named 'appspro-checkout'.

### Flutter / mobile WebView

The SDK auto-posts every event to a JavaScript channel named "AppsPro":

  window.AppsPro.postMessage(JSON.stringify({ type, data }));

In Flutter (webview_flutter), register a channel of that exact name and
parse the JSON to receive ready/otp-sent/success/payment-redirect/error
events. No extra JS wiring is needed inside the WebView.

---

## Inbound BDApps webhooks (no auth, called by BDApps — NOT by you)

POST /bdapps/sms      // SMS MO callback
POST /bdapps/ussd     // USSD MO callback
POST /bdapps/notify   // subscription notify
POST /bdapps/report   // SMS delivery report

You configure these URLs in the BDApps portal. Do not call them from your code.

---

## Public cross-app unsubscribe (end-user UI)

For end users who want to opt out across every app on the platform from
a single page, AppsPro hosts a public OTP-gated unsubscribe flow:

  https://appspro.dev/unsubscribe

Backend routes (POST /public/unsubscribe/otp/request, /otp/verify, and
/public/unsubscribe) are driven by that UI and are not intended to be
called from customer code. From your own code, use:

  POST /api/v1/sdk/unsubscribe    // Bearer sk_...

---

## Notes

- Phone numbers: 01XXXXXXXXX, 8801XXXXXXXXX, or +8801XXXXXXXXX (Robi/Airtel for BDApps).
- /api/v1/sdk/verify accepts the BDApps subscriber_id from /sdk/subscribers (e.g. tel:8801...).
- secret_key is shown only once when regenerating — store it immediately.
- For browser-side subscription UI, prefer the WebSDK (appspro.js) over
  the /embed/subscribe iframe — it works inside Android/iOS WebViews where
  iframes routinely break.
- Sending SMS or charging subscribers from your own backend is not part of the
  public API. Use the AppsPro dashboard for those operations.