MiniTaskHub Developers

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.

Keep your keys secret. Never commit them to a public repo, expose them in frontend/browser code, or share them in support tickets. If a key leaks, regenerate it immediately from Settings — the old one is invalidated the instant you do.

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 historyReads/writes a dedicated sandbox wallet only
A created task needs admin approval, exactly like the botA created task is instantly active — no waiting on a human
A withdrawal follows the real payout queueA withdrawal is instantly marked paid
Affected by platform maintenance modeNever 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
{
  "success": true,
  "data": { ... },
  "meta": { "request_id": "req_9f2a..." }
}
List / paginated success
{
  "success": true,
  "data": [ { ... }, { ... } ],
  "pagination": { "page": 1, "per_page": 20, "total": 47, "total_pages": 3 },
  "meta": { "request_id": "req_9f2a..." }
}
Error
{
  "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

HTTPCodeMeaning
400method_not_allowedThat path exists but not with this HTTP method.
401invalid_api_keyMissing, invalid, or revoked API key.
403forbiddenKey is valid but not allowed to perform this action.
404not_foundUnknown endpoint, or a resource that doesn't exist / doesn't belong to you.
422validation_errorRequest body failed validation — see the message.
429rate_limitedToo many requests. Back off and retry.
503maintenance_modePlatform (live only) is temporarily under maintenance.
503rate_unavailableUSD/NGN quote is temporarily uncached.
500internal_errorSomething 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

GET/v1/status

Returns platform maintenance status. The only endpoint that does not require an API key.

Response fields
status"live", "limited", or "full"
financial_actions_availableboolean — false during limited/full maintenance
messagehuman-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);
Response
{
  "success": true,
  "data": {
    "status": "live",
    "financial_actions_available": true,
    "message": null
  }
}

Balance

GET/v1/balance

Returns your current wallet balance breakdown.

Response fields (live)
currencyalways "USD"
balance_availablewithdrawable balance
balance_pendingawaiting confirmation
balance_reservedreserved against active created tasks
total_earned / total_withdrawn / referral_earningslifetime totals
In sandbox, only 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"];
Response
{
  "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

GET/v1/user

Returns your account profile. Identity is shared across both environments — it isn't sandboxed.

username, first_name, last_namefrom your Telegram profile / bot settings
email, phone_numberas set in Settings
country{code, name} or null
statusaccount status
member_sinceaccount creation date
curl https://minitaskhub.name.ng/api/v1/user \
  -H "Authorization: Bearer mtb_live_xxxx"
$r = api_get('/user'); // see Getting Started helper
Response
{
  "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

GET/v1/rate

The current USD/NGN rate used for Nigeria bank-transfer deposits. Public market data — identical in both environments.

paire.g. "USD/NGN"
rateapplied rate
updated_at / age_secondswhen the cached rate was last refreshed
curl https://minitaskhub.name.ng/api/v1/rate \
  -H "Authorization: Bearer mtb_live_xxxx"
Response
{
  "success": true,
  "data": { "pair": "USD/NGN", "rate": "1610.00", "updated_at": "2026-09-26 08:00:00", "age_seconds": 620 }
}

Supported Countries

GET/v1/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

GET/v1/transactions

Your full wallet ledger, paginated, newest first.

Query param
pagedefault 1
per_pagedefault 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

GET/v1/transactions/{id}

Returns a single transaction. A transaction belonging to another user returns 404 not_found — the same as a nonexistent ID.

Deposit History

GET/v1/deposits

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

GET/v1/deposits/{id}

Returns a single deposit. Not found / not yours → 404 not_found.

Create Task

POST/v1/tasks

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.

FieldType
category_idrequiredinta task category ID
titlerequiredstring
descriptionoptionalstring
instructionsoptionalstringlive only
target_urloptionalstring
max_completionsrequiredinthow many people can complete it
proof_typesoptionalarraylive only — what proof is required
expiry_daysoptionalintlive 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);
Response — 201 Created
{
  "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

GET/v1/tasks

Tasks you've created, paginated, newest first.

Get Task by ID

GET/v1/tasks/{id}

Returns a single task you created.

Withdrawal Methods

GET/v1/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.

Every key listed under 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: ....
Response
{
  "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

POST/v1/withdrawals

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.

Call Withdrawal Methods first to get a valid withdrawal_method_id and its exact required_destination_fields — don't guess the destination shape.
FieldType
withdrawal_method_idrequiredintfrom Withdrawal Methods
amountrequirednumberbefore fees
destinationrequiredobjectkeys 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"
    }
  }'
Response — 201 Created
{
  "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

GET/v1/withdrawals

Paginated, newest first.

Get Withdrawal by ID

GET/v1/withdrawals/{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.

EventFires when
task.approvedyour live task passes admin review and goes active
task.createdsandbox only — a sandbox task is created (instantly active)
withdrawal.paida withdrawal is paid out
withdrawal.rejecteda withdrawal is rejected, cancelled, or fails
deposit.confirmeda 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.

A delivery that doesn't return a 2xx response is retried with backoff (30s, 2m, 10m, 30m, 1h) up to 6 attempts before being marked failed.

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]);
Respond with a 2xx status as soon as verification and any quick bookkeeping is done. If your event handling can be slow (calling other APIs, sending emails), hand it off to a queue/background job and return 200 immediately — a slow response is indistinguishable from a failure and will be retried, which can cause you to process the same event more than once. Design your handler to be safe to run twice for the same data.id (idempotent), since retries and rare duplicate deliveries are both possible.

Get Current Webhook

GET/v1/webhooks

Returns the webhook registered for the environment your key belongs to, or {"registered": false}.

Register / Update Webhook

PUT/v1/webhooks
FieldType
urlrequiredstringmust be https://
eventsrequiredarraye.g. ["withdrawal.paid","deposit.confirmed"], or ["*"] for everything
The 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": ["*"] }'
Response — 201 Created (first time)
{
  "success": true,
  "data": {
    "registered": true, "url": "https://yourapp.com/hooks/minitaskhub",
    "events": ["*"], "status": "enabled",
    "secret": "whsec_9f2a1b7c..."
  }
}

Remove Webhook

DELETE/v1/webhooks

Removes the webhook registered for this environment. Returns 404 not_found if none exists.