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
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.
https://api.appspro.dev/bdapps/smsSMS MO callback. Paste into the BDApps portal as the SMS notification URL.No Auth
https://api.appspro.dev/bdapps/ussdUSSD MO callback. Paste into the BDApps portal as the USSD URL.No Auth
https://api.appspro.dev/bdapps/notifySubscription notification callback (subscribe / unsubscribe / renewal events).No Auth
https://api.appspro.dev/bdapps/reportSMS 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:
/platform/bdapps-portal-configReturns the SMS / USSD / Notify URLs and whitelisted IPs. Useful if you want to validate the dashboard values programmatically.No Auth
{
"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 URIhttps://api.appspro.dev/api/v1 — the root for SDK calls
publishable_keyClient-side safe — used in WebSDK init code and /sdk/app-info (e.g. pk_…)
secret_keyServer-side only — Bearer token for SDK calls AND HMAC key for verifying outbound webhook signatures. Shown once on regenerate.
url_slugShort 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:
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.
Authorization: Bearer sk_<your-secret>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.
/api/v1/sdk/verify/{subscriber_id}Returns subscription validity, subscriber details, and reason if invalid.
{
"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.
/api/v1/sdk/subscribers?page=1&limit=20&status=activeReturns paginated subscriber list with total count.
{
"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.
/api/v1/sdk/app-info?publishable_key=pk_…Public app metadata. Used by the WebSDK to render the widget header.No Auth
{
"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.
/api/v1/sdk/statusLive BDApps subscription status for a phone number.
// 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.
/api/v1/sdk/otp/requestSend an OTP SMS to a phone number.
// 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.
/api/v1/sdk/otp/verifyVerify OTP and register the subscription.
// 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.
/api/v1/sdk/subscribeDirectly subscribe a phone number.
// 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.
/api/v1/sdk/unsubscribeUnsubscribe a phone number.
// 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).
/api/v1/sdk/generate-tokenIssue an embed token for the WebSDK.No Auth
// 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
- User visits
/s/{url_slug}— sees your app info - User enters their Bangladeshi mobile number
- OTP is sent via BDApps SMS
- User verifies OTP on the checkout page
- 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.
/s/{url_slug}/infoPublic app info for the checkout UI.No Auth
// CheckoutAppInfo
{
"name": "Deendar",
"description": "Daily Islamic content",
"icon_url": "https://api.appspro.dev/uploads/icons/abc.png",
"pricing_model": "subscription",
"category": "islamic"
}/s/{url_slug}/otp/requestSend OTP to a phone number. Rate limited to 10/hour per phone.No Auth
// Request
{ "phone": "01712345678" }
// Response — OTPRequestOut
{
"status_code": "S1000",
"status_detail": "Success",
"raw": { /* full BDApps response */ },
"reference_no": "bdapps_ref_abc123"
}/s/{url_slug}/otp/verifyVerify OTP and create the subscription.No Auth
// 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"
}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 registeredsubscriber.cancelledSubscriber unsubscribedsubscriber.reactivatedPreviously cancelled subscriber re-registeredsubscriber.<status>Dynamic events for BDApps subscription_status (e.g. subscriber.registered, subscriber.unregistered)subscriber.unknown.<status>Fallback for unrecognised BDApps statusesInbound messaging (from BDApps webhooks)
sms.receivedIncoming SMS from a subscriberussd.receivedIncoming USSD messageHosted checkout
checkout.otp.requestedOTP requested via public hosted checkoutcheckout.otp.verify.failedOTP verify failed via public hosted checkoutSubscription Event Payload
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
{
"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.
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)
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)
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)
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)
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)
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
Installation & Usage
1. Include the script
<script src="https://appspro.dev/sdk/v1/appspro.js"></script>2. Add a container and initialize
<!-- 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>Options
SDK init
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', ...)
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
| Event | Payload | Description |
|---|---|---|
| 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.
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.
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.
/embed/subscribeIframe-friendly subscribe page. Query: publishable_key, token (from /sdk/generate-token), theme, locale, button_text, button_color, compact, hide_header.No Auth
<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.
# 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.