reefki / laravel-flip
Fluent Laravel SDK for the Flip for Business (flip.id) payment API.
Requires
- php: ^8.2
- guzzlehttp/guzzle: ^7.5
- illuminate/contracts: ^10.0|^11.0|^12.0|^13.0
- illuminate/http: ^10.0|^11.0|^12.0|^13.0
- illuminate/support: ^10.0|^11.0|^12.0|^13.0
Requires (Dev)
- orchestra/testbench: ^8.0|^9.0|^10.0|^11.0
- pestphp/pest: ^2.0|^3.0|^4.0
- pestphp/pest-plugin-laravel: ^2.0|^3.0|^4.0
- phpunit/phpunit: ^10.0|^11.0|^12.0
README
A fluent, fully-tested Laravel SDK for the Flip for Business payment API.
Covers everything Flip publishes:
- General — balance, supported banks, maintenance probe
- Disbursement (v2 + v3) — money transfer, list, lookup by id / idempotency key, bank account inquiry, special money transfer (PJP)
- Reference data (v2-only) — city, country, combined lists
- Accept Payment (v3-only) — create / list / read / edit bill, list payments per bill, list all payments, settlement report
- International Disbursement (v2-only) — exchange rates, form data, list, find, C2C/C2B + B2C/B2B create
- Webhook signature validation
Accept Payment v2 is deprecated; bill/payment/settlement-report resources are pinned to v3 and ignore FLIP_VERSION. Multi-version resources (disbursement, special disbursement) follow the configured default and can be overridden per call.
Install
composer require reefki/laravel-flip
The service provider and Flip facade are auto-discovered.
Publish the config (optional):
php artisan vendor:publish --tag=flip-config
Configure
Set these in your .env:
FLIP_SECRET_KEY=your-secret-key FLIP_VALIDATION_TOKEN=your-callback-validation-token FLIP_ENVIRONMENT=sandbox # or production FLIP_VERSION=v3 # default API version: v2 or v3
config/flip.php exposes everything (timeout, retries, base URLs).
The default version applies to the disbursement family (money transfer, special money transfer). Everything else is pinned because Flip only ships each endpoint on one version: bank account inquiry, city/country lists, exchange rates and the international transfer family are v2-only; accept payment (bill, payment listing, settlement report) is v3-only. Pinned resources ignore the config default — see "Versioning" below.
Quickstart
use Reefki\Flip\Facades\Flip; // Always check the deposit balance first $balance = Flip::balance()->get(); // ['balance' => 49656053] // Find a recipient bank $bca = Flip::banks()->find('bca'); // operational status, fee, queue // Verify the account holder name (cached → sync; uncached → callback) $inquiry = Flip::disbursement()->inquiry('5465327020', 'bca'); // Create a disbursement (idempotency key is required by Flip) $tx = Flip::disbursement()->create( payload: [ 'account_number' => '1122333300', 'bank_code' => 'bni', 'amount' => 10000, 'remark' => 'Salary - April', ], idempotencyKey: 'salary-april-user-123', );
Resources
Balance
Flip::balance()->get(); // ['balance' => int]
Banks
Flip::banks()->list(); // all banks Flip::banks()->list('bca'); // filter by code Flip::banks()->find('bca'); // single entry, or null
Maintenance
Flip::maintenance()->check(); // ['maintenance' => bool] Flip::maintenance()->isUnderMaintenance(); // bool
Disbursement (Money Transfer)
$tx = Flip::disbursement()->create( payload: [ 'account_number' => '1122333300', 'bank_code' => 'bni', 'amount' => 10000, 'remark' => 'Refund', 'recipient_city' => 391, 'beneficiary_email'=> 'user@example.com', ], idempotencyKey: 'refund-order-987', timestamp: now()->toIso8601String(), // optional X-TIMESTAMP ); Flip::disbursement()->list(['pagination' => 50, 'page' => 2, 'status' => 'DONE']); Flip::disbursement()->find('1234567890123456800'); Flip::disbursement()->findByIdempotencyKey('refund-order-987');
Bank Account Inquiry
inquiry() is pinned to v2 — Flip has not shipped a v3 of this endpoint.
$inquiry = Flip::disbursement()->inquiry( accountNumber: '5465327020', bankCode: 'bca', inquiryKey: 'order-987', // optional, for matching async callbacks );
If the result isn't cached, status will be PENDING and Flip will hit your configured callback URL with the final result.
Special Money Transfer (PJP)
Flip::specialDisbursement()->create( payload: [ 'account_number' => '1122333300', 'bank_code' => 'bni', 'amount' => 10000, 'sender_name' => 'John Doe', 'sender_address' => 'Jl. Example 123', 'sender_country' => 100252, 'sender_job' => 'entrepreneur', 'direction' => 'DOMESTIC_SPECIAL_TRANSFER', ], idempotencyKey: 'special-1', );
Accept Payment (Bills)
$bill = Flip::bill()->create([ 'title' => 'Coffee Table', 'type' => 'SINGLE', 'amount' => 30000, 'expired_date' => '2026-12-30 15:50', 'step' => 'checkout', // checkout | checkout_seamless | direct_api 'reference_id' => 'order-1234', ]); Flip::bill()->list(); Flip::bill()->find($billId); Flip::bill()->update($billId, ['status' => 'INACTIVE']);
Payments
Flip::payment()->forBill($billId, ['start_date' => '2026-01-01']); Flip::payment()->list(['reference_id' => 'order-1234']);
Settlement Report
$report = Flip::settlementReport()->generate('2026-01-01', '2026-01-09'); $status = Flip::settlementReport()->checkStatus($report['request_id']);
International Disbursement (v2)
$rates = Flip::internationalDisbursement()->exchangeRates('C2C', 'USA'); $form = Flip::internationalDisbursement()->formData('C2C', 'USA'); Flip::internationalDisbursement()->createConsumer($payload, 'idem-c2c-1'); Flip::internationalDisbursement()->createBusiness($payload, 'idem-b2b-1'); Flip::internationalDisbursement()->find($transactionId); Flip::internationalDisbursement()->list(['pagination' => 25]);
Reference Data
Flip::reference()->cities(); // ['102' => 'Kab. Bekasi', ...] Flip::reference()->countries(); // ['100000' => 'Afghanistan', ...] Flip::reference()->citiesAndCountries(); // both, merged
Versioning
Flip ships some endpoints on both v2 and v3, others on only one. The package handles that for you:
- Configurable default:
config('flip.version')(defaultv3) applies to all multi-version resources. - Per-call override:
Flip::disbursement()->withVersion('v2')->list()returns a clone with the version forced. - Global override:
Flip::useVersion('v2')->disbursement()->list()for a one-off facade chain. - Pinned endpoints: these endpoints ignore the configured default:
- v2-only (Flip does not publish v3): bank account inquiry, city/country lists, exchange rates, form data, every international-disbursement endpoint.
- v3-only (v2 is deprecated): accept-payment bills, payment listings, settlement report.
// Force a v2 disbursement create even if FLIP_VERSION=v3 Flip::disbursement()->withVersion('v2')->create($payload, 'idem-1'); // Or for an entire facade chain Flip::useVersion('v2')->disbursement()->list();
Webhooks
Every Flip callback POSTs application/x-www-form-urlencoded with two fields: data (JSON-encoded payload) and token (your validation token). Verify it with one call:
use Illuminate\Http\Request; use Reefki\Flip\Facades\Flip; use Reefki\Flip\Exceptions\InvalidWebhookSignatureException; Route::post('/webhooks/flip/disbursement', function (Request $request) { try { $payload = Flip::webhook()->verify($request); } catch (InvalidWebhookSignatureException) { abort(403); } // $payload is the decoded `data` array // ['id' => '1234567890123456789', 'status' => 'DONE', 'amount' => '10000', ...] Disbursement::where('flip_id', $payload['id'])->update([ 'status' => $payload['status'], 'reason' => $payload['reason'] ?? null, 'time_served' => $payload['time_served'] ?? null, ]); return response()->noContent(); // Flip retries non-200s 5x at 2-min intervals });
Or just check whether a token is valid:
if (Flip::webhook()->isValid($request->input('token'))) { // ... }
Errors
Flip's documented error responses map to typed exceptions:
| HTTP / Cause | Exception |
|---|---|
| 401 | Reefki\Flip\Exceptions\AuthenticationException |
| 404 | Reefki\Flip\Exceptions\NotFoundException |
| 422 | Reefki\Flip\Exceptions\ValidationException |
| 503 | Reefki\Flip\Exceptions\MaintenanceException |
| Network / DNS | Reefki\Flip\Exceptions\ConnectionException |
| ★ any other | Reefki\Flip\Exceptions\FlipException (base class) |
Webhook verification raises its own pair:
| Cause | Exception |
|---|---|
| Validation token mismatch | Reefki\Flip\Exceptions\InvalidWebhookSignatureException |
Signature OK but data field isn't valid JSON |
Reefki\Flip\Exceptions\MalformedWebhookPayloadException |
All inherit from FlipException, which exposes the response body and Flip's errors[] array:
use Reefki\Flip\Exceptions\ValidationException; try { Flip::disbursement()->create($payload, 'idem-1'); } catch (ValidationException $e) { foreach ($e->errors() as $err) { logger()->warning('flip.validation', $err); // ['attribute' => ..., 'code' => ..., 'message' => ...] } throw $e; }
Testing your code
The package uses Laravel's HTTP client under the hood, so you can fake requests in your own tests with Http::fake():
use Illuminate\Support\Facades\Http; use Reefki\Flip\Facades\Flip; Http::fake([ 'bigflip.id/big_sandbox_api/v3/disbursement' => Http::response([ 'id' => 1234567890123456800, 'status' => 'PENDING', ], 200), ]); $result = Flip::disbursement()->create($payload, 'idem-1');
Running the package's own test suite
composer install vendor/bin/pest
Pest covers every resource, both versions, error mapping, and webhook signature validation.
License
MIT.