Skip to content
RayanErfanPublic

About

Personal HTTP API for Blu Bank

Resources

Stars

19 stars

Watchers

0 watching

Forks

Repository files navigation

blu-api

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"])

Why a proxy

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.

How auth works

Recovered from the PWA bundle and verified live:

  1. 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 a kid.
  2. The raw ECDH shared secret is used directly as two keys, with no KDF: an AES-256-GCM key and an HMAC-SHA256 key.
  3. POST /openapi/v0/session/login — the password is sealed with AES-256-GCM (nonce ‖ ciphertext ‖ tag) and sent as a protobuf field.
  4. Every authenticated call carries an x-jws-signature: a detached-payload JWS (RFC 7797, b64:false, crit:["b64"]) whose requestId header claim is bound to the x-request-id header, over the exact request body. A Bearer token alone is not enough — the gateway answers 401.
  5. The token is an RS256 JWT from WSO2 API Manager with a 900 s TTL.

Session persistence

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 (mode 0600), 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.

Layout

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

Endpoints

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.

Finding a transaction

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.

Business profile (حساب تجاری)

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.

Payment links (لینک پرداخت)

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.

Dynamic second password

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.

CLI

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"}'

Thanks

Business-profile and payment-link testing ran on an account lent by @DevURANIUM. See CONTRIBUTORS.md.

Notes

  • 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.
  • identities is 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.

About

Personal HTTP API for Blu Bank

Resources

Stars

19 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages