├── .gitignore ├── docs └── logo.png ├── monobank ├── __init__.py ├── utils.py ├── errors.py ├── transport.py ├── signature.py └── client.py ├── pyproject.toml └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | *.pyc 2 | .venv 3 | .vscode 4 | dist 5 | test.py 6 | load_statements.py -------------------------------------------------------------------------------- /docs/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/vitalik/python-monobank/HEAD/docs/logo.png -------------------------------------------------------------------------------- /monobank/__init__.py: -------------------------------------------------------------------------------- 1 | from .client import Client, CorporateClient, access_request 2 | from .errors import Error, TooManyRequests 3 | -------------------------------------------------------------------------------- /monobank/utils.py: -------------------------------------------------------------------------------- 1 | import time 2 | 3 | 4 | def to_timestamp(dtime): 5 | "Converts datetime to utc timestamp" 6 | return int(time.mktime(dtime.timetuple())) 7 | -------------------------------------------------------------------------------- /monobank/errors.py: -------------------------------------------------------------------------------- 1 | class Error(Exception): 2 | def __init__(self, message, response): 3 | super().__init__(message) 4 | self.response = response 5 | 6 | def __str__(self): 7 | return f"{self.response.status_code}: {self.args[0]}" 8 | 9 | 10 | class TooManyRequests(Error): 11 | pass 12 | -------------------------------------------------------------------------------- /pyproject.toml: -------------------------------------------------------------------------------- 1 | [tool.poetry] 2 | name = "monobank" 3 | version = "0.4.4" 4 | description = "Monobank.ua API implementation" 5 | readme = "README.md" 6 | homepage = "https://github.com/vitalik/python-monobank" 7 | authors = ["Vitaliy Kucheryaviy "] 8 | 9 | 10 | [tool.poetry.dependencies] 11 | python = "^3.6" 12 | requests = "^2.22" 13 | ecdsa = "^0.13.2" 14 | 15 | [tool.poetry.dev-dependencies] 16 | pytest = "^3.0" 17 | 18 | [build-system] 19 | requires = ["poetry>=0.12"] 20 | build-backend = "poetry.masonry.api" 21 | -------------------------------------------------------------------------------- /monobank/transport.py: -------------------------------------------------------------------------------- 1 | import requests 2 | import monobank 3 | 4 | ENDPOINT = "https://api.monobank.ua" 5 | UAGENT = "python-monobank (https://github.com/vitalik/python-monobank, contact: ppr.vitaly@gmail.com)" 6 | 7 | 8 | def api_request(method, path, **kwargs): 9 | "Handles all HTTP requests for monobank endponts" 10 | headers = kwargs.pop("headers") 11 | headers["User-Agent"] = UAGENT 12 | url = ENDPOINT + path 13 | # print(method, url, headers) 14 | response = requests.request(method, url, headers=headers, **kwargs) 15 | 16 | if response.status_code == 200: 17 | if not response.content: # can be just empty an response, but it's fine 18 | return None 19 | return response.json() 20 | 21 | if response.status_code == 429: 22 | raise monobank.TooManyRequests("Too many requests", response) 23 | 24 | data = response.json() 25 | message = data.get("errorDescription", str(data)) 26 | raise monobank.Error(message, response) 27 | -------------------------------------------------------------------------------- /monobank/signature.py: -------------------------------------------------------------------------------- 1 | import os 2 | import ecdsa 3 | import base64 4 | import hashlib 5 | import binascii 6 | 7 | 8 | class SignKey(object): 9 | def __init__(self, priv_key): 10 | self.priv_key = priv_key 11 | self.sk = self._load() 12 | 13 | def key_id(self): 14 | "Returns monobank X-Key-Id" 15 | publicKey = self.sk.get_verifying_key() 16 | publicKeyB64 = base64.b64encode(publicKey.to_der()) 17 | 18 | uncompressedPublicKey = bytearray([0x04]) + (bytearray(publicKey.to_string())) 19 | digests = hashlib.sha1() 20 | digests.update(uncompressedPublicKey) 21 | return binascii.hexlify(digests.digest()) 22 | 23 | def sign(self, str_to_sign): 24 | "Signs string str_to_sign with private key, and hash sha256" 25 | sign = self.sk.sign(str_to_sign.encode(), hashfunc=hashlib.sha256) 26 | return base64.b64encode(sign) 27 | 28 | def _load(self): 29 | if "PRIVATE KEY-----" in self.priv_key: 30 | raw = self.priv_key 31 | elif os.path.exists(self.priv_key): 32 | with open(self.priv_key) as f: 33 | raw = f.read() 34 | else: 35 | raise Exception("Cannot load private key") 36 | return ecdsa.SigningKey.from_pem(raw) 37 | -------------------------------------------------------------------------------- /monobank/client.py: -------------------------------------------------------------------------------- 1 | import monobank 2 | from datetime import datetime, date, timedelta 3 | from monobank.utils import to_timestamp 4 | from monobank.signature import SignKey 5 | from monobank.transport import api_request 6 | 7 | 8 | class ClientBase(object): 9 | def _get_headers(self, url): 10 | raise NotImplementedError("Please implement _get_headers") 11 | 12 | def make_request(self, method, path, **kwargs): 13 | headers = self._get_headers(path) 14 | return api_request(method, path, headers=headers, **kwargs) 15 | 16 | def get_currency(self): 17 | return self.make_request("GET", "/bank/currency") 18 | 19 | def get_client_info(self): 20 | return self.make_request("GET", "/personal/client-info") 21 | 22 | def get_statements(self, account, date_from, date_to=None): 23 | if date_to is None: 24 | date_to = date_from 25 | assert date_from <= date_to, "date_from must be <= date_to" 26 | if isinstance(date_to, date): 27 | # dates converted to timestamps of the same day but 00:00 28 | # which is not very practical 29 | # in that case we moving 24 hours ahead to include desired date: 30 | date_to += timedelta(days=1) 31 | t_from, t_to = to_timestamp(date_from), to_timestamp(date_to) 32 | 33 | url = f"/personal/statement/{account}/{t_from}/{t_to}" 34 | return self.make_request("GET", url) 35 | 36 | def create_webhook(self, url): 37 | return self.make_request("POST", "/personal/webhook", json={"webHookUrl": url,}) 38 | 39 | 40 | class Client(ClientBase): 41 | "Personal API" 42 | 43 | def __init__(self, token): 44 | self.token = token 45 | 46 | def _get_headers(self, url): 47 | return { 48 | "X-Token": self.token, 49 | } 50 | 51 | 52 | class CorporateClient(ClientBase): 53 | "Corporate API" 54 | 55 | def __init__(self, request_id, private_key): 56 | self.request_id = request_id 57 | self.key = SignKey(private_key) 58 | 59 | def _get_headers(self, url): 60 | headers = { 61 | "X-Key-Id": self.key.key_id(), 62 | "X-Time": str(to_timestamp(datetime.now())), 63 | "X-Request-Id": self.request_id, 64 | } 65 | data = headers["X-Time"] + headers["X-Request-Id"] + url 66 | headers["X-Sign"] = self.key.sign(data) 67 | return headers 68 | 69 | def check(self): 70 | "Checks if user approved access request" 71 | try: 72 | self.make_request("GET", "/personal/auth/request") 73 | return True 74 | except monobank.Error as e: 75 | if e.response.status_code == 401: 76 | return False 77 | raise 78 | 79 | 80 | def access_request(permissions, private_key, callback_url=None): 81 | "Creates an access request for corporate api user" 82 | key = SignKey(private_key) 83 | headers = { 84 | "X-Key-Id": key.key_id(), 85 | "X-Time": str(to_timestamp(datetime.now())), 86 | "X-Permissions": permissions, 87 | } 88 | if callback_url: 89 | headers["X-Callback"] = callback_url 90 | path = "/personal/auth/request" 91 | sign_str = headers["X-Time"] + headers["X-Permissions"] + path 92 | headers["X-Sign"] = key.sign(sign_str) 93 | return api_request("POST", path, headers=headers) 94 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-monobank 2 | 3 | ![GitHub-issues](https://img.shields.io/github/issues/vitalik/python-monobank) 4 | 5 | ![python-monobank](https://raw.githubusercontent.com/vitalik/python-monobank/master/docs/logo.png) 6 | 7 | Python client for Monobank API (https://api.monobank.ua/docs/) 8 | 9 | ## Installation 10 | 11 | ``` 12 | pip install monobank 13 | ``` 14 | 15 | 16 | # Usage 17 | 18 | ## Personal api 19 | 20 | 1) Request your token at https://api.monobank.ua/ 21 | 22 | 2) Use that token to initialize client: 23 | 24 | ```python 25 | import monobank 26 | token = 'xxxxxxxxxxxxxxx' 27 | 28 | mono = monobank.Client(token) 29 | user_info = mono.get_client_info() 30 | print(user_info) 31 | ``` 32 | 33 | ### Methods 34 | 35 | Get currencies 36 | 37 | ```python 38 | >>> mono.get_currency() 39 | [ 40 | {'currencyCodeA': 840, 41 | 'currencyCodeB': 980, 42 | 'date': 1561686005, 43 | 'rateBuy': 25.911, 44 | 'rateSell': 26.2357}, 45 | {'currencyCodeA': 978, 46 | 'currencyCodeB': 980, 47 | 'date': 1561686005, 48 | 'rateBuy': 29.111, 49 | 'rateSell': 29.7513}, 50 | ... 51 | ``` 52 | 53 | Get client info 54 | 55 | ```python 56 | >>> mono.get_client_info() 57 | { 58 | 'name': 'Dmitriy Dubilet' 59 | 'accounts': [ 60 | { 61 | 'id': 'accidxxxxx' 62 | 'balance': 100000000, 63 | 'cashbackType': 'UAH', 64 | 'creditLimit': 100000000, 65 | 'currencyCode': 980, 66 | } 67 | ], 68 | } 69 | 70 | ``` 71 | 72 | 73 | Get statements 74 | ```python 75 | >>> mono.get_statements('accidxxxxx', date(2019,1,1), date(2019,1,30)) 76 | [ 77 | { 78 | 'id': 'iZDPhf8v32Qass', 79 | 'amount': -127603, 80 | 'balance': 99872397, 81 | 'cashbackAmount': 2552, 82 | 'commissionRate': 0, 83 | 'currencyCode': 978, 84 | 'description': 'Smartass club', 85 | 'hold': True, 86 | 'mcc': 5411, 87 | 'operationAmount': 4289, 88 | 'time': 1561658263 89 | }, 90 | ... 91 | ] 92 | ``` 93 | 94 | You can as well pass datetime objects 95 | ```python 96 | >>> mono.get_statements('accidxxxxx', datetime(2019,1,1,11,15), datetime(2019,1,2,11,15)) 97 | ``` 98 | 99 | 100 | Create a Webhook 101 | ```python 102 | >>> mono.create_webhook('https://myserver.com/hookpath') 103 | ``` 104 | 105 | 106 | 107 | ## Corporatre API 108 | 109 | Documentation is here - https://api.monobank.ua/docs/corporate.html 110 | 111 | Corporate API have the same methods as Public API, but it does not have rate limitation, and it is a recomended way if you are handling data for commercial use (or just storing lot of personal data). 112 | 113 | ### Getting access 114 | 115 | #### 1) Generate private key 116 | 117 | ``` 118 | openssl ecparam -genkey -name secp256k1 -rand /dev/urandom -out priv.key 119 | ``` 120 | 121 | This will output file **priv.key** 122 | 123 | **Warning**: do not share it with anyone, do not store it in public git repositories 124 | 125 | #### 2) Generate public key 126 | 127 | ``` 128 | openssl ec -in priv.key -pubout -out pub.key 129 | ``` 130 | 131 | This will output file **pub.key** 132 | 133 | #### 3) Request API access 134 | Send an email to api@monobank.ua - describe your project, and attach **pub.key** (!!! NOT priv.key !!!) 135 | 136 | 137 | ### Requesting permission from monobank user 138 | 139 | Once your app got approved by Monobank team you can start using corporate API: 140 | 141 | 142 | #### 1) Create monobank user access request 143 | 144 | ```python 145 | private_key = '/path/to/your/priv.key' 146 | request = monobank.access_request('ps', private_key) 147 | ``` 148 | If all fine you should recive the following: 149 | ```python 150 | print(request) 151 | {'tokenRequestId': 'abcdefg_Wg', 'acceptUrl': 'https://mbnk.app/auth/abcdefg_Wg'} 152 | ``` 153 | 154 | You should save tokenRequestId to database, and then give user the link acceptUrl 155 | 156 | Note: To be notified about user acceptance you can use web callback: 157 | 158 | ```python 159 | monobank.access_request('ps', private_key, callback_url='https://yourserver.com/callback/') 160 | ``` 161 | 162 | #### 2) Check if user accepted 163 | 164 | You can check if user accepted access request like this: 165 | 166 | 167 | ```python 168 | request_token = 'abcdefg_Wg' # the token from access_request result 169 | private_key = '/path/to/your/priv.key' 170 | 171 | mono = monobank.CorporateClient(request_token, private_key) 172 | 173 | 174 | mono.check() # returns True if user accepted, False if not 175 | 176 | ``` 177 | 178 | 179 | #### 3) Use methods 180 | 181 | Once user accepts your access-request, you can start using all the methods same ways as Public API 182 | 183 | ```python 184 | mono.get_statements(....) 185 | ``` 186 | 187 | ## Handling Errors 188 | 189 | If you use Personal API you may encounter "Too Many Requests" error. To properly catch it and retry - use *monobank.TooManyRequests* exception 190 | 191 | ```python 192 | try: 193 | mono.get_statements(....) 194 | except monobank.TooManyRequests: 195 | time.sleep(1) 196 | # try again: 197 | mono.get_statements(....) 198 | ``` 199 | 200 | You can use ratelimiter library (like https://pypi.org/project/ratelimiter/ ) to download all transactions 201 | --------------------------------------------------------------------------------