A personal HTTP API for Blu Bank (com.samanpr.blu). It speaks the bank's own
gateway protocol directly — no browser, no app, no scraping.
npm install
BLU_PROXY=socks5h://127.0.0.1:1080 npm run login # once
BLU_PROXY=socks5h://127.0.0.1:1080 npm start
data/ is created on first login and holds the session. See .env.example for
the available settings.
Then call it:
curl -s localhost:8088/balance
curl -s -X POST localhost:8088/transfers -H 'content-type: application/json' \
-d '{"amount":100000,"to":"6104335551234567"}'
example/ has a Python script per endpoint and a recorded response for each,
with the account details replaced by fake ones. See example/README.md.
There is also a published Python client, bluapi:
import bluapi
with bluapi.Client() as blu:
print(blu.balance()["total"])openapi.sdb247.com is split-horizon DNS: outside Iran it resolves to a dead
German address. All traffic must leave from an Iranian IP. Point BLU_PROXY at
any SOCKS5 or HTTP proxy that exits there — socks5h is preferred so DNS is
resolved remotely.
The gateway also only speaks HTTP/2. An HTTP/1.1 ClientHello is dropped at
the handshake, so the transport is http2.connect with explicit
:method/:path/:scheme/:authority pseudo-headers.
Recovered from the PWA bundle and verified live:
POST /openapi/v0/session/key-exchange— the client sends an ECDH P-256 public key (DER SPKI). The server answers with its own SPKI and akid.- The raw ECDH shared secret is used directly as two keys, with no KDF: an AES-256-GCM key and an HMAC-SHA256 key.
POST /openapi/v0/session/login— the password is sealed with AES-256-GCM (nonce ‖ ciphertext ‖ tag) and sent as a protobuf field.- Every authenticated call carries an
x-jws-signature: a detached-payload JWS (RFC 7797,b64:false,crit:["b64"]) whoserequestIdheader claim is bound to thex-request-idheader, over the exact request body. A Bearer token alone is not enough — the gateway answers 401. - The token is an RS256 JWT from WSO2 API Manager with a 900 s TTL.
The device id is a random UUID that is written to data/sessions.json and reused
forever. Once a device has completed an OTP it is trusted, and login returns
the token straight away with no OTP. That is what makes the tool usable:
- the token, its expiry, the device id, the ECDH private key and the credentials
live in
data/sessions.json(mode0600), one slot per username; ensure()hands back a live token, and silently re-logs-in when it has run out — no OTP, no user interaction;- a 401 on any call triggers one renewal and a retry;
- concurrent callers share a single renewal round trip.
So the session survives restarts and renews itself for as long as the bank keeps
trusting the device. npm run logout wipes it.
| file | role |
|---|---|
src/proto.js |
protobuf wire encode/decode + JSON renderer |
src/crypto.js |
ECDH, AES-256-GCM seal/open, detached JWS signer |
src/client.js |
device identity, key exchange, login, OTP, HTTP/2 transport |
src/session.js |
on-disk session store |
src/api.js |
one method per gateway route |
src/derive.js |
balance / source-card extraction |
server.js |
HTTP API |
cli.js |
login, OTP, status, raw calls |
example/ |
Python examples, one per endpoint, plus captured responses |
python/ |
the bluapi package published to PyPI |
Set BLU_API_KEY to require an x-api-key header on everything but /health.
| method | path | notes |
|---|---|---|
| GET | /health |
liveness |
| GET | /status |
login state, token TTL |
| POST | /login |
{username,password}; reports otpRequired |
| POST | /otp |
{code} to finish a fresh-device login |
| POST | /logout |
drop the stored session |
| GET | /profile |
identity, phone, party id |
| GET | /profiles |
all profiles; ?types=1,2,3 filters locally |
| GET | /profiles/summary |
parsed profiles plus openable account kinds |
| GET | /accounts |
every account on the party, business included |
| GET | /balance |
summed available balance |
| GET | /balances |
every deposit with available/current |
| GET | /cards |
recent transfer cards |
| GET | /cards/all |
own cards; ?account= for one profile |
| GET | /cards/processed |
processed card orders |
| GET | /cards/:card/otp |
dynamic second password (رمز دوم پویا) for a card |
| GET | /owner/:identifier |
owner lookup for a card or IBAN |
| GET | /transactions |
history; ?account=&page=&size= |
| GET | /transactions/detail/:id |
one transaction by row id |
| GET | /transactions/find/:code |
one transaction by receipt number (کد پیگیری) |
| GET | /transactions/categories |
merchant categories |
| GET | /transactions/hashtags |
hashtags |
| GET | /payment-links |
business links and payments; ?count= |
| GET | /payment-links/general |
permanent «لینک عمومی» of the business |
| POST | /payment-links/create |
{amount,description?} on the business profile |
| POST | /payment-links/validate |
{id,amount?} payer-side check |
| GET | /payment-links/:id/receipt |
receipt of a paid link |
| GET | /transfers/recent |
recent transfer destinations |
| GET | /transfers/scheduled |
scheduled transfer contracts |
| GET | /transfers/:id |
one transfer |
| POST | /transfers/:id/cancel |
cancel a transfer |
| POST | /transfers |
{amount,to,bank?,method?,source?} |
| GET | /charge/recent-phones |
recent top-up numbers |
| GET | /internet/recent-phones |
recent internet packages |
| GET | /bill/favorites |
saved bills |
| GET | /banks |
bank directory |
| GET | /banners |
dashboard banners |
| GET | /onboarding |
onboarding slides |
| GET | /junior |
junior accounts |
| GET | /identities |
identity traits |
| GET | /cashback/categories |
cashback categories |
| GET | /cashback/vendors |
cashback vendors |
| GET | /cheques |
cheque registry |
| GET | /contacts |
recent contacts |
| GET | /raw/{path} |
escape hatch, ?req=&resp= message types |
Transfer methods: 24 card-to-card, 25 instant (Pol), 26 Paya, 27 Satna.
The default is 24.
Every receipt carries a کد پیگیری (receipt number). The gateway has no lookup
by it, so /transactions/find/:code walks the history a page at a time and
returns the first row whose receipt matches:
curl -s localhost:8088/transactions/find/249042
{"receipt":"249042","type":"انتقال به کارت","amount":"۱۰٬۰۰۰ ریال", ...}
?pages= bounds the walk (default 50, max 500). The receipt also appears in the
row's description as شماره پیگیری, next to the شماره مرجع reference number.
A Blu party can hold several profiles at once. Each has its own account number, IBAN and card, and the same login acts for all of them:
| type | label | notes |
|---|---|---|
1 |
حساب شخصی | the ordinary personal profile, id main |
2 |
حساب تجاری / حساب کسبوکار | business profile, advertised with a higher transfer ceiling |
3 |
حساب حقوقی | legal entity |
GET /profiles/summary lists them and says which kinds the user may still
open:
curl -s localhost:8088/profiles/summary
{"profiles":[
{"id":"main","type":"1","kind":"حساب شخصی","accountNumber":"611828001111111101","card":"6219865551234568"},
{"id":"...","type":"2","kind":"حساب کسبوکار","accountNumber":"611828002222222202","card":"6219865559876543"}],
"kinds":[{"type":"2","label":"حساب کسبوکار","available":false,"reason":"سقف ساخت حساب کسبوکار پر شده"}]}
Switching profile in the app is navigation only — profile/switch returns a
blubank://…&profile-id=… deep link, not a new token. The data routes are
scoped by the account number in the query, so no re-login is needed:
curl -s "localhost:8088/cards/all?account=611828002222222202"
curl -s "localhost:8088/transactions?account=611828002222222202&size=20"
A plain /transactions shows the active profile only. Asking /accounts (by
party id) is what surfaces the business balance.
A business profile has a «لینک پرداخت» section with a permanent «لینک عمومی» and a «لینکها و پرداختها» tab. That tab is one list: every link created for a fixed amount, and one row per payment once a link is paid. The toolkit exposes it as-is:
curl -s "localhost:8088/payment-links?count=20"
{"links":[
{"id":"…","title":"لینک پرداخت ۲","amount":{"value":"2500000","currency":"IRR"},
"status":"2","statusLabel":"پرداختشده","type":"2","typeLabel":"لینک اختصاصی",
"link":"https://blubiz.sb24.com/s/…","transactionId":"…","details":{…}}],
"generalLink":{"title":"لینک عمومی","url":"https://blubiz.sb24.com/s/…","status":"1","statusLabel":"فعال"},
"creationRestriction":{"minimum":{"value":"1000000","currency":"IRR"},
"maximum":{"value":"100000000","currency":"IRR"}}}
status is 1 waiting for payment, 2 paid, 3 settled, 4 expired, 5
canceled; type is 1 general and 2 unique. A new link must sit inside
creationRestriction (1,000,000–100,000,000 IRR on a fresh business profile):
curl -s -X POST localhost:8088/payment-links/create \
-H 'content-type: application/json' \
-d '{"amount":1000000,"description":"سفارش ۱۲۳"}'
The server assigns the title («لینک پرداخت N») and a 24-hour expiry. Payer-side checks and receipts:
curl -s -X POST localhost:8088/payment-links/validate \
-H 'content-type: application/json' -d '{"id":"…","amount":1000000}'
curl -s localhost:8088/payment-links/<id>/receipt
These routes select the business profile for the call and restore the previous
one afterwards, so /balance and friends are unaffected. If the business has
no gateway contract yet, the links are still created but validate answers
with an invalid reason (امکان پرداخت این لینک وجود ندارد) and the general
link carries a notice saying gateway payment is disabled.
Card payments ask for a رمز دوم پویا (dynamic second password) instead of the static CVV2. It is six digits, single use, and the server starts a two-minute clock the moment it is issued:
curl -s localhost:8088/cards/6104335551234567/otp
{"code":"123456","otpRequest":"73443","validFrom":1793282709,"expiresIn":120,"expiresAt":1793282829000}
The card is the full PAN — /cards/all carries it, /cards only the masked
one. The password comes back sealed under the session key and is opened locally,
so it never crosses the wire in clear. Fetch it right before paying, not in
advance.
node cli.js login [user] [pass]
node cli.js otp <code>
node cli.js status
node cli.js balance
node cli.js profiles
node cli.js accounts
node cli.js cards [account]
node cli.js transactions [account]
node cli.js card-otp <card>
node cli.js payment-links [count]
node cli.js payment-link-create <amount> [description]
node cli.js payment-link-validate <id> [amount]
node cli.js payment-link-receipt <id>
node cli.js transaction-find <receipt>
node cli.js logout
node cli.js call account/deposit/list/all '{"req":"solutions.saman.bluebank.services.AccountListAllRequest","resp":"...Response"}'
Business-profile and payment-link testing ran on an account lent by @DevURANIUM. See CONTRIBUTORS.md.
- Request bodies are raw protobuf with no
.proto; fields are addressed by number. Most routes take the shared app block plus route fields. - Payment routes carry a
blubank://payment-service/<name>URI nested two levels deep in field 3. identitiesis the one JSON route.- The gateway is rate-sensitive; a burst of unknown routes can return 500s for a minute.
Built for personal use.
example/ has a Python script per endpoint and a recorded response for each,
so the shape of every route is readable without running anything.