binderbyte / binderpay-php
Official BinderPay SNAP API SDK for PHP (Virtual Account & QRIS)
Requires
- php: >=8.1
- ext-curl: *
- ext-json: *
- ext-openssl: *
Requires (Dev)
- pestphp/pest: ^3.0
Suggests
None
Provides
None
Conflicts
None
Replaces
None
README
Official BinderPay SNAP API SDK for PHP — Virtual Account & QRIS.
Implements RSA-SHA256 (SNAP Bank Indonesia) signature automatically on every request and provides callback verification helpers.
Installation
composer require binderbyte/binderpay-php
Requires PHP >= 8.1.0, ext-json, ext-openssl, ext-curl (for HTTP).
Configuration
use BinderPay\BinderPay; $client = new BinderPay([ 'partnerId' => '170041', // X-PARTNER-ID 'privateKey' => file_get_contents('private.pem'), // RSA private key (PEM) 'channelId' => 'BCA', // CHANNEL-ID 'isProduction' => false, // default sandbox; true = production ]);
| Option | Type | Default | Description |
|---|---|---|---|
partnerId |
string | — | Registered partner ID (required) |
privateKey |
string | — | RSA private key PEM (required) |
channelId |
string | — | Bank channel code, e.g. BCA (required) |
isProduction |
bool | false |
false = sandbox, true = production |
baseUrl |
string | — | Override base URL |
Default base URLs: Sandbox https://api-sandbox.binderpay.id, Production https://api.binderpay.id.
Virtual Account
// Create VA (Service 27) $client->virtualAccount->create([ 'customerNo' => '000003212', 'virtualAccountName' => 'Chus Pandi', 'trxId' => 'INV-000000023212', 'totalAmount' => ['value' => '25000.00', 'currency' => 'IDR'], 'virtualAccountTrxType' => 'C', // C | O | R 'expiredDate' => '2023-09-05T19:30:14+07:00', 'additionalInfo' => ['channel' => 'CIMB'], ]); // Inquiry active VA (Service 30) $client->virtualAccount->inquiry([ 'trxId' => 'INV-000000023212', 'additionalInfo' => ['contractId' => 'ci302a21c9'], ]); // VA payment status (Service 26) $client->virtualAccount->status([ 'virtualAccountNo' => '2269141693898987', 'trxId' => 'INV-000000023212', 'additionalInfo' => ['contractId' => 'ci302a21c9', 'channel' => 'BCA'], ]); // Delete VA (Service 31) $client->virtualAccount->delete([ 'trxId' => 'INV-000000023212', 'virtualAccountNo' => '2269141693898987', 'additionalInfo' => ['channel' => 'BCA', 'contractId' => 'ci302a21c9'], ]);
VA types: C (one-off), O (open recurring), R (close recurring).
Channels: BRI, BNI, MANDIRI, MANDIRIPC, PERMATA, BSI, MUAMALAT, BCA, CIMB, SINARMAS, BNC, MAYBANK.
QRIS
// Generate QRIS (Service 47) $client->qris->generate([ 'partnerReferenceNo' => 'INV-000000023212', 'amount' => ['value' => '45000.00', 'currency' => 'IDR'], 'validityPeriod' => '2024-01-11T17:00:00+07:00', // required if isStatic = false 'additionalInfo' => ['isStatic' => false], ]); // Query status (Service 51) $client->qris->query([ 'originalPartnerReferenceNo' => 'INV-000000023212', 'serviceCode' => '47', 'additionalInfo' => ['contractId' => 'ci302a21c9'], ]); // Cancel (Service 77) $client->qris->cancel([ 'originalPartnerReferenceNo' => 'INV-000000023212', 'reason' => 'cancel order', 'additionalInfo' => ['contractId' => 'ci302a21c9'], ]);
Webhook / Callback Validation
Callbacks from BinderPay are sent with the X-TIMESTAMP, X-SIGNATURE, and X-PARTNER-ID headers. Verify the signature with the BinderPay public key (download from https://binderpay.id/docs/binderpay-public.pem, not your private key).
There are three separate path concepts:
- Merchant callback route: your application-owned route, for example
/api/binderpay/callback. - Signed callback path: the exact path BinderPay includes in the callback string-to-sign:
/v1.0/transfer-va/paymentfor VA or/v1.0/qr/qr-mpm-notifyfor QRIS. - Outbound API endpoint: an SDK request path such as
/v1.0/transfer-va/create-va.
use BinderPay\Webhook; // Laravel example Route::post('/api/binderpay/callback', function (Request $request) { $rawBody = $request->getContent(); // exactly as received; do not re-serialize // Load BinderPay public key (PEM format) from file or environment variable $binderpayPublicKey = file_get_contents(storage_path('keys/binderpay-public.pem')); $valid = Webhook::verifyCallbackSignature( $request->headers->all(), $rawBody, $binderpayPublicKey, ); if (! $valid) { return response()->json(['message' => 'Cannot verify signature'], 401); } $callback = Webhook::parseCallback($request->all()); // auto-detects VA or QRIS // ... process payment idempotently ... // Return the matching SNAP success code for the detected callback type. return response()->json(Webhook::successResponse($callback['type'])); });
parseCallback() inspects the payload and validates the required fields of the detected type — a VA callback (has trxId) or a QRIS callback (has originalReferenceNo). It throws ValidationException with the detected type in the message when a required field is missing, and rejects payloads that match neither type. Use Webhook::successResponse($callback['type']) to acknowledge — it returns 2002500 for va and 2005200 for qris.
For a standard VA or QRIS callback, use the same Webhook::verifyCallbackSignature(...) method. It automatically checks /v1.0/transfer-va/payment and /v1.0/qr/qr-mpm-notify, and accepts Laravel/Symfony header arrays. For non-standard integrations, use Webhook::verifyCallbackSignatureForPath(...) with an explicit path. You can also use Webhook::validatePublicKey($publicKey) to validate public key presence and RSA format upfront.
Important:
$rawBodymust be exactly as received by the server; do not decode and re-serialize it before verification.$binderpayPublicKeyis strictly validated; passing an empty/missing key or invalid PEM throws aValidationException.- The merchant route remains application-owned; the unified helper selects the BinderPay signed callback path automatically.
- Replay prevention and idempotent transaction handling remain the merchant application's responsibility.
- Return HTTP 200 with the appropriate BinderPay response code after successful processing.
Error Handling
use BinderPay\Exceptions\BinderPayException; use BinderPay\Exceptions\ValidationException; try { $client->virtualAccount->create([...]); } catch (ValidationException $e) { // invalid input on the SDK side } catch (BinderPayException $e) { if ($e->responseCode === '4002701') { // field format mismatch } // $e->status (HTTP status), $e->responseBody (raw body) }
Testing
composer test # Pest
License
This project is licensed under the MIT License - see the LICENSE file for details.