tangentopay / tangentopay-php
Official PHP SDK for the TangentoPay API
Requires
- php: >=8.1
- guzzlehttp/guzzle: ^7.0
Requires (Dev)
- guzzlehttp/promises: ^2.0
- phpunit/phpunit: ^10.0
README
Official PHP SDK for the TangentoPay API — accept payments, issue refunds, manage wallets, and verify webhooks with a clean, fully-typed interface.
Table of contents
- Requirements
- Installation
- Quick start
- Authentication
- Token expiry and refresh
- Test mode
- Resources
- Provider status
- Currency and provider guide
- Service wallet operations (B2B2C)
- Payouts
- Merchant wallet top-up
- Payment methods
- Error handling
- Webhook verification
- Contributing
- Security
- License
Requirements
- PHP 8.1 or later
- Guzzle 7 (
guzzlehttp/guzzle) - A TangentoPay account — sign up
Installation
composer require tangentopay/tangentopay-php
Quick start
use TangentoPay\ServiceClient; use TangentoPay\MerchantClient; // ── Storefront: create a Stripe checkout session ────────────────────────────── $service = new ServiceClient(['serviceKey' => getenv('TANGENTOPAY_SERVICE_KEY')]); $session = $service->checkout->create([ 'products' => [['name' => 'Pro Plan', 'price' => 49.99, 'quantity' => 1]], 'currency_code' => 'USD', 'customer_email'=> 'buyer@example.com', 'return_url' => 'https://myshop.com/thank-you', ]); // redirect your customer to $session['redirect_url'] // ── Backend: manage payments with an API token ──────────────────────────────── $merchant = new MerchantClient(['apiToken' => getenv('TANGENTOPAY_API_TOKEN')]); $payments = $merchant->payments->list(['perPage' => 20]); $balance = $merchant->wallets->mainBalance();
Authentication
TangentoPay has two credential types:
| Credential | Where it goes | Client to use |
|---|---|---|
Service Key (pk_live_… / pk_test_…) |
X-Service-Key header |
ServiceClient |
| API Token (Bearer) | Authorization: Bearer … |
MerchantClient |
Never expose an API token in browser or mobile code. Use ServiceClient on the frontend and MerchantClient only on your server.
ServiceClient
$service = new ServiceClient([ 'serviceKey' => getenv('TANGENTOPAY_SERVICE_KEY'), // optional: 'baseUrl' => 'https://api.tangentopay.com/api/v1', 'timeoutS' => 30, 'maxRetries' => 3, ]);
MerchantClient
$merchant = new MerchantClient(['apiToken' => getenv('TANGENTOPAY_API_TOKEN')]);
Obtain an API token programmatically:
$client = new MerchantClient(); $client->auth->login(['email' => $email, 'password' => $password]); $token = $client->auth->verifyOtp(['email' => $email, 'otp' => $otp]); $merchant = new MerchantClient(['apiToken' => $token->accessToken]);
Token expiry and refresh
Catch AuthenticationException and re-authenticate in place:
use TangentoPay\Exceptions\AuthenticationException; try { $payments = $merchant->payments->list(); } catch (AuthenticationException $e) { $merchant->auth->login(['email' => $email, 'password' => $password]); $token = $merchant->auth->verifyOtp(['email' => $email, 'otp' => $otp]); $merchant->setToken($token->accessToken); // updates all resources in place $payments = $merchant->payments->list(); // retry }
Test mode
Pass a pk_test_… service key to use Stripe test mode:
$service = new ServiceClient(['serviceKey' => 'pk_test_<your_test_key>']); var_dump($service->testMode); // bool(true)
Test-mode sessions go through Stripe's sandbox and never move real money.
Resources
ServiceClient resources
| Property | Description |
|---|---|
$service->checkout |
Create Stripe-hosted checkout sessions; poll payment status |
$service->topups |
Collect money from a customer's MoMo account into the service wallet |
$service->withdrawals |
Send money from the service wallet to a customer's MoMo account |
$service->providerStatus |
Real-time health for MTN MoMo, Orange Money, and Stripe |
MerchantClient resources
| Property | Description |
|---|---|
$merchant->auth |
Login, OTP verification, profile, logout |
$merchant->payments |
View and search your incoming payment history |
$merchant->refunds |
Issue refunds on completed payments |
$merchant->topups |
Top up your main wallet via card or MoMo |
$merchant->payouts |
Send funds out (bank, MoMo, TP wallet, debit card) |
$merchant->wallets |
Main and service wallet balances |
$merchant->services |
View services; manage enabled payment methods per service |
$merchant->customers |
Create and manage customer records |
$merchant->analytics |
Payment summaries, revenue, and volume over time |
$merchant->logs |
Per-service API request logs |
$merchant->transfers |
Internal wallet transfer history |
$merchant->providerStatus |
Real-time health for MTN MoMo, Orange Money, and Stripe |
Note on service administration: Creating services, rotating API keys, updating webhooks, and other one-time setup tasks are done from the TangentoPay Dashboard. These operations are intentionally not exposed in the SDK.
Provider status
Check provider health before initiating any collection or disbursement — this lets you show users a clear error message instead of a silent payment failure.
$status = $merchant->providerStatus->get(); // or: $service->providerStatus->get() // $status is an associative array keyed by provider slug: // [ // 'mtn_momo' => ['slug' => 'mtn_momo', 'name' => 'MTN Mobile Money', 'status' => 'operational', ...], // 'orange_money' => ['slug' => 'orange_money', 'name' => 'Orange Money', 'status' => 'degraded', ...], // 'stripe' => ['slug' => 'stripe', 'name' => 'Stripe', 'status' => 'operational', ...], // ] if ($status['mtn_momo']['status'] === 'down') { return response()->json([ 'message' => 'MTN Mobile Money is currently unavailable. Try Orange Money or pay by card.', ], 503); }
Possible status values:
| Value | Meaning |
|---|---|
"operational" |
Fully functional — proceed normally |
"degraded" |
Partial outage — expect higher failure rates |
"down" |
Provider unreachable — do not attempt payments |
Currency and provider guide
| Provider | Supported currencies | Notes |
|---|---|---|
| MTN Mobile Money | XAF only | Cameroon; USSD push via Fapshi. Min 100 XAF, max 500 000 XAF. |
| Orange Money | XAF only | Cameroon; USSD push via Fapshi. Min 100 XAF, max 500 000 XAF. |
| Stripe | USD, EUR, GBP, and more | Multi-currency card checkout and instant payouts. |
When a customer pays via MoMo the transaction currency is XAF. When they pay via Stripe card the currency is whatever currency_code you pass to checkout->create().
Use $merchant->wallets->mainBalance() to get per-currency balances — the response includes a balances array showing only currencies with a non-zero funded amount, which you can use to build a currency-selector UI in your withdrawal flow.
Service wallet operations (B2B2C)
The service wallet is funded when customers pay through your service's checkout flow.
// ── Collect from a customer's MoMo ─────────────────────────────────────────── $topup = $service->topups->create([ 'amount' => 5000, // XAF 'customer_phone' => '237XXXXXXXXX', 'external_ref' => 'ORDER-001', 'notify_url' => 'https://yourapp.com/webhooks/momo', ]); // pending — wallet credited after Fapshi webhook confirms // ── Disburse to a customer's MoMo ──────────────────────────────────────────── $withdrawal = $service->withdrawals->create([ 'amount' => 4000, // XAF 'recipient_phone' => '237XXXXXXXXX', 'external_ref' => 'PAYOUT-001', ]);
Payouts
Two-step flow: initiate → confirm.
// Step 1 — initiate $initiation = $merchant->payouts->initiate([ 'amount' => 50_000, 'currency_code' => 'XAF', 'recipient_type' => 'tangentopay_wallet', 'recipient_details' => ['wallet_address' => 'user@example.com'], 'note' => 'Freelance payment', ]); // Step 2 — confirm with payout PIN $merchant->payouts->confirm($initiation->payoutRef, ['pin' => getenv('PAYOUT_PIN')]);
Virtual card payout (USD, Instant Payout)
// Option A: use a saved card $merchant->payouts->initiate([ 'amount' => 100, 'currency_code' => 'USD', 'recipient_type' => 'virtual_card', 'recipient_details' => ['payout_method_id' => 'pm_...'], ]); // Option B: one-time Stripe.js token (card never stored) $merchant->payouts->initiate([ 'amount' => 100, 'currency_code' => 'USD', 'recipient_type' => 'virtual_card', 'recipient_details' => ['stripe_token_id' => 'tok_...'], ]);
Bulk payout
$batch = $merchant->payouts->bulk->initiate([ 'csv_file' => fopen('payouts.csv', 'r'), 'default_recipient_type' => 'tangentopay_wallet', ]); $merchant->payouts->bulk->confirm($batch->batchRef, ['pin' => getenv('PAYOUT_PIN')]);
Merchant wallet top-up
// Via MoMo $topup = $merchant->topups->create([ 'amount' => 100_000, // XAF 'phone' => '237XXXXXXXXX', 'provider' => 'mtn_momo', ]); // Via card (Stripe-hosted page) $cardTopup = $merchant->topups->createCardTopup([ 'amount' => 200, 'currency_code' => 'USD', 'return_url' => 'https://dashboard.yourapp.com/wallet', ]);
Payment methods
$methods = $merchant->services->listPaymentMethods($serviceId); // [['slug' => 'mtn_momo', 'name' => 'MTN Mobile Money', 'enabled' => true, 'locked' => false, ...], ...] // Disable Orange Money if provider is down $status = $merchant->providerStatus->get(); if ($status['orange_money']['status'] === 'down') { $merchant->services->setPaymentMethod($serviceId, 'orange_money', false); } // Replace entire set (card must always be included) $merchant->services->setPaymentMethods($serviceId, ['card', 'mtn_momo']);
Environment configuration
TANGENTOPAY_SERVICE_KEY=pk_live_<your_public_key> TANGENTOPAY_SECRET_KEY=sk_live_<your_secret_key> TANGENTOPAY_WEBHOOK_SECRET=whsec_<your_webhook_secret>
Error handling
use TangentoPay\Exceptions\AuthenticationException; // 401 use TangentoPay\Exceptions\PermissionException; // 403 use TangentoPay\Exceptions\NotFoundException; // 404 use TangentoPay\Exceptions\ValidationException; // 422 — has ->errors array use TangentoPay\Exceptions\RateLimitException; // 429 — has ->retryAfter seconds use TangentoPay\Exceptions\ServerException; // 5xx use TangentoPay\Exceptions\NetworkException; // connection-level failure use TangentoPay\Exceptions\TangentoPayException; // base class try { $merchant->payouts->initiate([...]); } catch (ValidationException $e) { print_r($e->errors); // field-level validation messages } catch (RateLimitException $e) { echo "Retry after {$e->retryAfter}s"; } catch (TangentoPayException $e) { throw $e; }
Webhook verification
use TangentoPay\Webhook; use TangentoPay\Exceptions\WebhookSignatureException; $payload = file_get_contents('php://input'); $signature = $_SERVER['HTTP_X_TANGENTOPAY_SIGNATURE'] ?? ''; try { $event = Webhook::constructEvent($payload, $signature, getenv('TANGENTOPAY_WEBHOOK_SECRET')); } catch (WebhookSignatureException $e) { http_response_code(400); exit('Bad signature'); } if ($event->event === 'transaction.payment_completed') { fulfillOrder($event->payload); } http_response_code(200); echo json_encode(['received' => true]);
Contributing
Pull requests are welcome. For major changes please open an issue first.
composer install vendor/bin/phpunit
Security
Please report security vulnerabilities to security@tangentopay.com rather than opening a public issue.