virusphp/bridging-bpjs
A Laravel package for integrating BPJS Kesehatan bridging APIs, offering convenient configuration and wrappers for common endpoints so PHP applications can connect to BPJS services more easily and consistently.
Install and publish configuration:
composer require virusphp/bridging-bpjs
php artisan vendor:publish --provider="Virusphp\BridgingBpjs\BridgingBpjsServiceProvider"
The config file is the first thing to open — it holds the credentials and base URLs that every other call depends on:
BPJS_CONS_ID=xxxxx
BPJS_SECRET_KEY=xxxxx # used for X-signature (HMAC)
BPJS_USER_KEY=xxxxx # used as the user_key header
BPJS_VCLAIM_URL=https://new-api.bpjs-kesehatan.go.id/vclaim-rest/
BPJS_ANTROL_URL=https://new-api.bpjs-kesehatan.go.id/antreanrs/
After the config is set, the fastest end-to-end validation is a participant lookup by card number (peserta / Peserta resource). It touches every moving part — signature generation, LZ-String compression, AES decrypt — with a single request and no side effects, so if it returns a decoded array you know your credentials, clock, and decryption are all correct.
Where to look first, in order:
config/bridging-bpjs.php — credential names and the two base URLs (VClaim and Antrol are separate services with separate keys).new.peserta, rujukan, sep, monitoring, antrean, etc.).Wrap it, don't call it from controllers. Inject the facade/service into your own BpjsClaimService or VclaimGateway class. BPJS method names, response envelopes, and error codes will change; your controllers shouldn't.
Handle the V2 envelope in one place. VClaim V2 returns roughly:
{
"metaData": { "code": "200", "message": "OK" },
"response": "<lz-string encrypted base64 blob>"
}
The package decrypts response into a JSON string; you then json_decode(). Build a single unwrap() helper that returns ['code' => ..., 'data' => ...] or throws a domain exception, and use it everywhere. Never let a raw envelope reach your views.
Branch on metaData.code, not on HTTP status. BPJS returns HTTP 200 with a populated error code for business failures (201 = data not found, 202 = invalid credential, and so on). Treat 000/non-200 codes as application-level outcomes, not exceptions.
Cache reference data aggressively. Poli lists, dokter lists, and faskes lookups change rarely. A 24-hour cache keyed by faskes + endpoint removes most of your latency and protects you when BPJS is slow or down. Do not cache peserta or SEP checks.
SEP creation is a workflow, not a call. The day-to-day shape is:
rujukan lookup → validate the referral exists and is still validsep create → capture the returned SEP numberSteps 2 and 3 must be reconcilable. Store the raw decrypted response alongside the SEP number so a failed DB write can be replayed without re-issuing a SEP.
Antrol is a separate track. Booking, calling, and canceling queue numbers (antrean) uses different credentials and often a different base URL. Treat Antrol as its own service class even if the package exposes both through one facade — you'll want to fake one and test the other.
Log a correlation id per request. Compose something like noKartu-timestamp-endpoint and log it with the raw encrypted response before decryption. BPJS dispute resolution and your own debugging both become possible; without it, a decryption failure is unreproducible.
Test with Http::fake(). Because the package ultimately uses the Laravel HTTP client against the configured base URLs, you can stub the endpoints in feature tests and keep the real decryptor in the loop (or stub that too, with a fixture of a known-good encrypted payload).
Clock drift silently breaks everything. X-timestamp is UNIX seconds and feeds the X-signature computation. A few minutes of drift returns an auth error with no hint about time. Run NTP on every app server; never cache a timestamp across requests.
Secret key and user key are different values. X-signature = base64(sha256(cons_id + "&" + timestamp, secret_key)), while user_key is sent as its own header. Swapping them produces a generic 401 that looks like a network problem. This is the single most common setup mistake.
The response field is triple-encoded. Decrypt → LZ-String decompress → JSON string → json_decode. If a method hands you back a string that looks like JSON, you're one json_decode short. If it hands you back gibberish, you're one decompress short. Keep the two layers separable so you can test them independently.
Error responses are often not encrypted. When BPJS rejects the request outright, response is null and the useful message is in metaData.message. Guard every decrypt call with a null/empty check or you'll throw a confusing LZ-String exception on top of the real error.
Not every endpoint is encrypted on the way out. Some VClaim V2 methods take a plain query string, others take an encrypted body. Check each method's signature in the source rather than assuming symmetry — a wrong assumption usually surfaces as a 202 invalid-credential error, which sends you chasing the wrong bug.
Never retry SEP creation blindly. Retry-with-backoff is correct for rujukan, monitoring, and reference lookups. For SEP insertion and Antrol booking, a timeout may still have created the record server-side. Check locally first, and consider the SEP number itself as your idempotency key.
UTF-8 handling in LZ-String. The PHP port operates on character positions; if you round-trip payloads through a non-UTF-8-aware column or a json_encode with escaped unicode, decompression can silently produce wrong bytes. Store and log as UTF-8 end to end.
Antrol usually needs IP whitelisting. Requests from a dev laptop will fail even with correct keys. Get the production egress IP registered early, and plan for it in staging.
Pin the exact version. A 41-star package with a 2025 release date is actively churning against a moving government API. Use "virusphp/bridging-bpjs": "x.y.z" (not ^x.y), and keep a fork or a local patch path ready — you will eventually need to fix a decryptor edge case or add an endpoint before the maintainer does.
Useful extension points: the service provider's bindings (swap the client for a fake), the config file (per-environment base URLs), and the decryptor class (unit-test it against fixtures from the official BPJS encryption tool). If the package doesn't expose a decrypt method publicly, wrap or extend the class rather than duplicating the algorithm — LZ-String + AES with a subtly wrong key derivation is very hard to debug from the outside.
How can I help you explore Laravel packages today?