MiniTaskHub Developer API
Build on top of MiniTaskHub's task marketplace and wallet — check balances, list transactions, create tasks, request withdrawals, and get real-time webhooks, all with one API key.
The API is organized around REST. It uses standard HTTP response codes, accepts JSON request bodies, and returns JSON responses wrapped in a consistent envelope. Every endpoint below is available in both the Live and Sandbox environments — the key you send determines which one you're talking to.
Getting Started
You'll need an API key before you can call anything. Open the bot, go to Settings → API Key, and tap Generate. You'll immediately get two keys — a Live key and a Sandbox key — shown once. Store them somewhere safe; MiniTaskHub cannot show you the full key again.
Base URL
Both environments share the exact same base URL — only the key changes:
https://minitaskhub.name.ng/api/v1/
Authentication
Send your API key as a Bearer token in the Authorization header on every request:
curl https://minitaskhub.name.ng/api/v1/balance \ -H "Authorization: Bearer mtb_live_xxxxxxxxxxxxxxxxxxxx"
$ch = curl_init("https://minitaskhub.name.ng/api/v1/balance");
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"Authorization: Bearer mtb_live_xxxxxxxxxxxxxxxxxxxx",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = json_decode(curl_exec($ch), true);A request with no key, an invalid key, or a revoked key returns 401 invalid_api_key.
Live & Sandbox Environments
There's no separate sandbox URL — the key you send determines the environment. A key starting with mtb_live_ operates on real user data and real money; a key starting with mtb_test_ operates entirely on an isolated sandbox wallet that can never affect a real balance, real transaction, real deposit, or real withdrawal.
Live (mtb_live_...) | Sandbox (mtb_test_...) |
|---|---|
| Reads/writes your real wallet and history | Reads/writes a dedicated sandbox wallet only |
| A created task needs admin approval, exactly like the bot | A created task is instantly active — no waiting on a human |
| A withdrawal follows the real payout queue | A withdrawal is instantly marked paid |
| Affected by platform maintenance mode | Never affected by maintenance mode |
Your sandbox wallet starts with a sample balance and a few sample transactions/deposits/withdrawals/tasks the first time you call any endpoint with your sandbox key, so you have something to look at immediately.
Request & Response Format
Send request bodies as JSON (Content-Type: application/json) or standard form-encoded fields — both work. Every response, success or error, is JSON with the same envelope shape:
{
"success": true,
"data": { ... },
"meta": { "request_id": "req_9f2a..." }
}{
"success": true,
"data": [ { ... }, { ... } ],
"pagination": { "page": 1, "per_page": 20, "total": 47, "total_pages": 3 },
"meta": { "request_id": "req_9f2a..." }
}{
"success": false,
"error": { "code": "validation_error", "message": "amount must be greater than zero." },
"meta": { "request_id": "req_9f2a..." }
}Include ?page= and ?per_page= (max 100) on any list endpoint. The request_id is unique per request — include it when contacting support about a specific call.
Error Codes
| HTTP | Code | Meaning |
|---|---|---|
| 400 | method_not_allowed | That path exists but not with this HTTP method. |
| 401 | invalid_api_key | Missing, invalid, or revoked API key. |
| 403 | forbidden | Key is valid but not allowed to perform this action. |
| 404 | not_found | Unknown endpoint, or a resource that doesn't exist / doesn't belong to you. |
| 422 | validation_error | Request body failed validation — see the message. |
| 429 | rate_limited | Too many requests. Back off and retry. |
| 503 | maintenance_mode | Platform (live only) is temporarily under maintenance. |
| 503 | rate_unavailable | USD/NGN quote is temporarily uncached. |
| 500 | internal_error | Something went wrong on our side. Includes a request_id — contact support with it. |
Rate Limits
Each API key is limited to 120 requests per 60 seconds, tracked independently for your live and sandbox keys. Exceeding it returns 429 rate_limited. If you need a higher limit for a production integration, contact support with your use case.
System Status
Returns platform maintenance status. The only endpoint that does not require an API key.
| status | "live", "limited", or "full" |
| financial_actions_available | boolean — false during limited/full maintenance |
| message | human-readable maintenance message, or null when live |
curl https://minitaskhub.name.ng/api/v1/status
$r = file_get_contents(
"https://minitaskhub.name.ng/api/v1/status"
);
$status = json_decode($r, true);{
"success": true,
"data": {
"status": "live",
"financial_actions_available": true,
"message": null
}
}Balance
Returns your current wallet balance breakdown.
| currency | always "USD" |
| balance_available | withdrawable balance |
| balance_pending | awaiting confirmation |
| balance_reserved | reserved against active created tasks |
| total_earned / total_withdrawn / referral_earnings | lifetime totals |
balance_available and total_earned are returned — there's no pending/reserved concept in the simplified sandbox ledger.curl https://minitaskhub.name.ng/api/v1/balance \ -H "Authorization: Bearer mtb_live_xxxx"
$ch = curl_init("https://minitaskhub.name.ng/api/v1/balance");
curl_setopt_array($ch, [
CURLOPT_HTTPHEADER => ["Authorization: Bearer mtb_live_xxxx"],
CURLOPT_RETURNTRANSFER => true,
]);
$balance = json_decode(curl_exec($ch), true)["data"];{
"success": true,
"data": {
"currency": "USD",
"balance_available": "128.40",
"balance_pending": "0.00",
"balance_reserved": "12.00",
"total_earned": "410.90",
"total_withdrawn": "270.50",
"referral_earnings": "8.00"
}
}User Details
Returns your account profile. Identity is shared across both environments — it isn't sandboxed.
| username, first_name, last_name | from your Telegram profile / bot settings |
| email, phone_number | as set in Settings |
| country | {code, name} or null |
| status | account status |
| member_since | account creation date |
curl https://minitaskhub.name.ng/api/v1/user \ -H "Authorization: Bearer mtb_live_xxxx"
$r = api_get('/user'); // see Getting Started helper{
"success": true,
"data": {
"username": "johndoe",
"first_name": "John",
"last_name": "Doe",
"email": "john@example.com",
"phone_number": "+2348012345678",
"country": { "code": "NG", "name": "Nigeria" },
"status": "active",
"member_since": "2025-11-02 10:15:00"
}
}Current Exchange Rate
The current USD/NGN rate used for Nigeria bank-transfer deposits. Public market data — identical in both environments.
| pair | e.g. "USD/NGN" |
| rate | applied rate |
| updated_at / age_seconds | when the cached rate was last refreshed |
curl https://minitaskhub.name.ng/api/v1/rate \ -H "Authorization: Bearer mtb_live_xxxx"
{
"success": true,
"data": { "pair": "USD/NGN", "rate": "1610.00", "updated_at": "2026-09-26 08:00:00", "age_seconds": 620 }
}Supported Countries
Countries currently supported for signup/withdrawals. Public reference data.
{
"success": true,
"data": [
{ "code": "NG", "name": "Nigeria", "flag_emoji": "🇳🇬", "calling_code": "+234" },
{ "code": "KE", "name": "Kenya", "flag_emoji": "🇰🇪", "calling_code": "+254" }
]
}All Transactions
Your full wallet ledger, paginated, newest first.
| Query param | |
|---|---|
| page | default 1 |
| per_page | default 20, max 100 |
{
"success": true,
"data": [
{ "id": 5521, "type": "task_reward", "amount": "5.00", "balance_before": "100.00",
"balance_after": "105.00", "reference": "TXN-...", "description": "Task reward",
"created_at": "2026-09-25 14:02:11" }
],
"pagination": { "page": 1, "per_page": 20, "total": 47, "total_pages": 3 }
}Get Transaction by ID
Returns a single transaction. A transaction belonging to another user returns 404 not_found — the same as a nonexistent ID.
Deposit History
Paginated, newest first. Same page/per_page params as transactions.
{
"success": true,
"data": [
{ "id": 812, "reference": "DEP-...", "amount": "100.00", "currency": "USD",
"status": "confirmed", "method": "Bank Transfer (NGN)",
"created_at": "2026-09-20 09:11:04", "confirmed_at": "2026-09-20 09:14:30" }
],
"pagination": { "page": 1, "per_page": 20, "total": 6, "total_pages": 1 }
}Get Deposit by ID
Returns a single deposit. Not found / not yours → 404 not_found.
Create Task
Creates a user-created task. In live, this reserves funds and queues the task for admin approval — identical to creating one from the bot. In sandbox, the task is created instantly active against your sandbox balance.
| Field | Type | |
|---|---|---|
| category_idrequired | int | a task category ID |
| titlerequired | string | |
| descriptionoptional | string | |
| instructionsoptional | string | live only |
| target_urloptional | string | |
| max_completionsrequired | int | how many people can complete it |
| proof_typesoptional | array | live only — what proof is required |
| expiry_daysoptional | int | live only |
curl -X POST https://minitaskhub.name.ng/api/v1/tasks \
-H "Authorization: Bearer mtb_test_xxxx" \
-H "Content-Type: application/json" \
-d '{
"category_id": 3,
"title": "Follow our new page",
"description": "Follow and like our latest post",
"target_url": "https://example.com/page",
"max_completions": 50
}'$ch = curl_init("https://minitaskhub.name.ng/api/v1/tasks");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_HTTPHEADER => [
"Authorization: Bearer mtb_test_xxxx",
"Content-Type: application/json",
],
CURLOPT_POSTFIELDS => json_encode([
"category_id" => 3,
"title" => "Follow our new page",
"target_url" => "https://example.com/page",
"max_completions" => 50,
]),
CURLOPT_RETURNTRANSFER => true,
]);
$task = json_decode(curl_exec($ch), true);{
"success": true,
"data": {
"id": 991, "title": "Follow our new page", "status": "active",
"reward_per_user": "0.50", "max_completions": 50,
"created_at": "2026-09-26 10:02:00"
},
"meta": { "environment": "sandbox", "request_id": "req_..." }
}Task History
Tasks you've created, paginated, newest first.
Get Task by ID
Returns a single task you created.
Withdrawal Methods
Returns every withdrawal method currently available to you (already filtered by your account's country, the same restriction POST /v1/withdrawals itself enforces) — including exactly which fields each one requires as destination. Call this before building your withdrawal form or integration — the required fields are admin-configured per method and differ between methods (and between installs), so they can't be hardcoded from docs alone. This is why a fixed list of "field X for method Y" isn't given below: query it instead, and it will always match what the platform currently expects.
required_destination_fields for a method is mandatory in the destination object you send to POST /v1/withdrawals for that withdrawal_method_id. A missing one is exactly what causes 422 validation_error: Missing required field: ....{
"success": true,
"data": [
{
"id": 2,
"name": "Bank Transfer (NGN)",
"code": "bank_transfer_ngn",
"type": "bank",
"currency": "NGN",
"network": null,
"minimum_amount": "5.00",
"maximum_amount": "500.00",
"fixed_fee": "0.30",
"percentage_fee": 1.5,
"required_destination_fields": [
{ "key": "bank_name", "label": "Bank Name", "type": "text" },
{ "key": "account_number", "label": "Account Number", "type": "text" },
{ "key": "account_name", "label": "Account Name", "type": "text" }
]
},
{
"id": 5,
"name": "USDT (BEP20)",
"code": "usdt_bep20",
"type": "crypto",
"currency": "USDT",
"network": "BEP20",
"minimum_amount": "5.00",
"maximum_amount": "1000.00",
"fixed_fee": "1.00",
"percentage_fee": 0,
"required_destination_fields": [
{ "key": "wallet_address", "label": "Wallet Address", "type": "text" }
]
},
{
"id": 7,
"name": "Mobile Money",
"code": "mobile_money",
"type": "mobile_money",
"currency": "KES",
"network": null,
"minimum_amount": "2.00",
"maximum_amount": "300.00",
"fixed_fee": "0.10",
"percentage_fee": 1.0,
"required_destination_fields": [
{ "key": "phone_number", "label": "Phone Number", "type": "phone" }
]
}
]
}So, matching each example above, a valid destination would be built directly from that method's required_destination_fields — for method id: 2 that's {"bank_name": "...", "account_number": "...", "account_name": "..."}; for id: 5 it's just {"wallet_address": "..."}; for id: 7 it's {"phone_number": "..."}. The three examples above are the platform's common presets, but always trust what this endpoint returns for your account over any fixed example — an admin can add, remove, or rename fields at any time.
Create Withdrawal
Requests a withdrawal. In live, this runs the full validation/fee/payout pipeline — identical to withdrawing from the bot. In sandbox, it's instantly marked paid.
withdrawal_method_id and its exact required_destination_fields — don't guess the destination shape.| Field | Type | |
|---|---|---|
| withdrawal_method_idrequired | int | from Withdrawal Methods |
| amountrequired | number | before fees |
| destinationrequired | object | keys come from that method's required_destination_fields — see three worked examples below |
curl -X POST https://minitaskhub.name.ng/api/v1/withdrawals \
-H "Authorization: Bearer mtb_test_xxxx" \
-H "Content-Type: application/json" \
-d '{
"withdrawal_method_id": 2,
"amount": 20.00,
"destination": {
"bank_name": "GTBank",
"account_number": "0123456789",
"account_name": "John Doe"
}
}'curl -X POST https://minitaskhub.name.ng/api/v1/withdrawals \
-H "Authorization: Bearer mtb_test_xxxx" \
-H "Content-Type: application/json" \
-d '{
"withdrawal_method_id": 5,
"amount": 20.00,
"destination": {
"wallet_address": "0x9f2a1b7c3d4e5f60718293a4b5c6d7e8f9012345"
}
}'curl -X POST https://minitaskhub.name.ng/api/v1/withdrawals \
-H "Authorization: Bearer mtb_test_xxxx" \
-H "Content-Type: application/json" \
-d '{
"withdrawal_method_id": 7,
"amount": 10.00,
"destination": {
"phone_number": "+254712345678"
}
}'{
"success": true,
"data": {
"id": 441, "reference": "SBWD-...", "requested_amount": "20.00",
"total_fee": "0.50", "net_amount": "19.50", "status": "paid",
"created_at": "2026-09-26 10:05:00"
}
}422 validation_error: Missing required field: X means your destination object is missing (or has an empty value for) one of the exact keys Withdrawal Methods lists for that withdrawal_method_id.Withdrawal History
Paginated, newest first.
Get Withdrawal by ID
Returns a single withdrawal.
Webhooks
Register one URL per environment to receive real-time events instead of polling. Each event is a signed POST with a JSON body.
| Event | Fires when |
|---|---|
| task.approved | your live task passes admin review and goes active |
| task.created | sandbox only — a sandbox task is created (instantly active) |
| withdrawal.paid | a withdrawal is paid out |
| withdrawal.rejected | a withdrawal is rejected, cancelled, or fails |
| deposit.confirmed | a deposit is confirmed and credited |
Payload shape
Every delivery is a JSON body of this shape, regardless of event:
{
"event": "withdrawal.paid",
"environment": "live",
"data": { "id": 441, "reference": "WD-...", "status": "paid" },
"timestamp": "2026-09-26T10:05:00+00:00"
}See Receiving & Verifying (PHP) for a complete, ready-to-deploy receiver.
Receiving & Verifying Webhooks (PHP)
Every delivery includes an X-MiniTaskHub-Signature header — the HMAC-SHA256 of the exact raw request body, keyed with the secret you were shown once when you registered the webhook (Register Webhook). Recompute it yourself and compare with hash_equals() — never trust the payload before that check passes, and never compare signatures with == (not constant-time).
Below is a complete, drop-in receiver — save it as e.g. minitaskhub-webhook.php at the URL you registered, fill in your own secret, and wire in your own logic per event:
<?php
// minitaskhub-webhook.php — the exact URL you registered with PUT /v1/webhooks
declare(strict_types=1);
// Store this in an env var / secrets file in real deployments — never
// commit it. This is the `secret` you were shown ONCE when you first
// registered the webhook for this environment.
$webhookSecret = getenv('MINITASKHUB_WEBHOOK_SECRET') ?: 'whsec_your_secret_here';
// 1. Read the RAW body — do NOT use $_POST here. Signing is done over
// the exact bytes sent, and re-serializing $_POST/json_decode()'d
// data before verifying can silently change the bytes (key order,
// whitespace, numeric formatting) and make a valid signature fail.
$rawBody = file_get_contents('php://input');
// 2. Recompute the signature and compare in constant time.
$receivedSignature = $_SERVER['HTTP_X_MINITASKHUB_SIGNATURE'] ?? '';
$expectedSignature = hash_hmac('sha256', $rawBody, $webhookSecret);
if ($receivedSignature === '' || !hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(401);
echo json_encode(['error' => 'invalid_signature']);
exit;
}
// 3. Only now is it safe to trust and decode the body.
$payload = json_decode($rawBody, true);
if (!is_array($payload) || !isset($payload['event'])) {
http_response_code(400);
echo json_encode(['error' => 'invalid_payload']);
exit;
}
// 4. (Recommended) Reject deliveries that are too old, in case a
// signed request was somehow replayed later.
$timestamp = strtotime((string) ($payload['timestamp'] ?? ''));
if ($timestamp === false || abs(time() - $timestamp) > 300) {
http_response_code(400);
echo json_encode(['error' => 'stale_or_missing_timestamp']);
exit;
}
// 5. Handle the event. Keep this fast (a few hundred ms) and return
// 2xx quickly — do slow work (emails, heavy processing) via a queue
// afterward, not inline before responding. A non-2xx response, or a
// timeout, is treated as failed and retried on the schedule above.
switch ($payload['event']) {
case 'deposit.confirmed':
// $payload['data']['id'], ['reference'], ['amount'], ['status']
// e.g. mark the matching order/invoice as paid in your own system.
break;
case 'withdrawal.paid':
case 'withdrawal.rejected':
// $payload['data']['id'], ['reference'], ['status']
break;
case 'task.approved':
case 'task.created':
// $payload['data']['id'], ['title'], ['status']
break;
default:
// Unknown/future event type — acknowledge it anyway so it isn't
// retried forever; just don't act on it.
break;
}
http_response_code(200);
echo json_encode(['received' => true]);data.id (idempotent), since retries and rare duplicate deliveries are both possible.Get Current Webhook
Returns the webhook registered for the environment your key belongs to, or {"registered": false}.
Register / Update Webhook
| Field | Type | |
|---|---|---|
| urlrequired | string | must be https:// |
| eventsrequired | array | e.g. ["withdrawal.paid","deposit.confirmed"], or ["*"] for everything |
secret field is only ever present in the response the FIRST time you register a webhook for that environment. Store it then — it can't be shown again (updating the URL/events later keeps the same secret).curl -X PUT https://minitaskhub.name.ng/api/v1/webhooks \
-H "Authorization: Bearer mtb_live_xxxx" \
-H "Content-Type: application/json" \
-d '{ "url": "https://yourapp.com/hooks/minitaskhub", "events": ["*"] }'{
"success": true,
"data": {
"registered": true, "url": "https://yourapp.com/hooks/minitaskhub",
"events": ["*"], "status": "enabled",
"secret": "whsec_9f2a1b7c..."
}
}Remove Webhook
Removes the webhook registered for this environment. Returns 404 not_found if none exists.