Skip to content
This repository was archived by the owner on Oct 29, 2023. It is now read-only.
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -127,6 +127,8 @@ print(api_client.get_balance())
This is going to use the same mechanism to load configuration as the CLI tool, to specify your own configuration you can use it as:

```python
import uuid

from n26.api import Api
from n26.config import Config

Expand All @@ -135,6 +137,8 @@ conf.USERNAME.value = "[email protected]"
conf.PASSWORD.value = "$upersecret"
conf.LOGIN_DATA_STORE_PATH.value = None
conf.MFA_TYPE.value = "app"
conf.DEVICE_TOKEN.value = uuid.uuid4()

conf.validate()

api_client = Api(conf)
Expand Down
25 changes: 25 additions & 0 deletions example.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
# logger = logging.getLogger("n26")
# logger.setLevel(logging.DEBUG)
# ch = logging.StreamHandler()
# ch.setLevel(logging.DEBUG)
# formatter = logging.Formatter("%(asctime)s - %(name)s - %(levelname)s - %(message)s")
# ch.setFormatter(formatter)
# logger.addHandler(ch)

import json
import uuid

from n26.api import Api
from n26.config import Config

config = Config(validate=False)
config.USERNAME.value = "[email protected]"
config.PASSWORD.value = "$upersecret"
config.LOGIN_DATA_STORE_PATH.value = None
config.DEVICE_TOKEN.value = uuid.uuid4()
config.validate()

api = Api()
statuses = api.get_balance()
json_data = json.dumps(statuses)
print(statuses)
166 changes: 131 additions & 35 deletions n26/api.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,13 +106,18 @@ def _write_token_file(token_data: dict, path: str):
file.write(json.dumps(token_data, indent=2))
file.truncate()

# IDEA: @get_token decorator
def get_account_info(self) -> dict:
def get_account_info_old(self) -> dict:
"""
Retrieves basic account information
"""
return self._do_request(GET, BASE_URL_DE + '/api/me')

def get_account_info(self) -> dict:
"""
Retrieves basic account information
"""
return self._do_request(GET, BASE_URL_DE + f'/api/account/primary')

def get_account_statuses(self) -> dict:
"""
Retrieves additional account information
Expand Down Expand Up @@ -183,45 +188,128 @@ def get_standing_orders(self) -> dict:
"""
return self._do_request(GET, BASE_URL_DE + '/api/transactions/so')

def get_transactions(self, from_time: int = None, to_time: int = None, limit: int = 20, pending: bool = None,
categories: str = None, text_filter: str = None, last_id: str = None) -> dict:
def get_transactions(
self,
from_time: int = None, to_time: int = None,
direction: str = None,
limit: int = 20,
text_filter: str = None,
pagination_key: str = None
) -> dict:
"""
Get a list of transactions.

Note that some parameters can not be combined in a single request (like text_filter and pending) and
will result in a bad request (400) error.

:param from_time: earliest transaction time as a Timestamp > 0 - milliseconds since 1970 in CET
:param to_time: latest transaction time as a Timestamp > 0 - milliseconds since 1970 in CET
:param limit: Limit the number of transactions to return to the given amount - default 20 as the n26 API returns
only the last 20 transactions by default
:param pending: show only pending transactions
:param categories: Comma separated list of category IDs
:param text_filter: Query string to search for
:param last_id: ??
:param from_time: the earliest transaction time as a Timestamp > 0 - milliseconds since 1970 in CET
:param to_time: the latest transaction time as a Timestamp > 0 - milliseconds since 1970 in CET
:param direction: INCOMING or OUTGOING
:param limit: limits the number of transactions to return to the given amount - default 20 as the n26 API returns
only the last 20 transactions by default, can be 800 at max
:param text_filter: Query string to search for, can also contain tags, merchants and categoriess
:param pagination_key: pass this from the last response to get the next badge of results
:return: list of transactions
"""
if pending and limit:
# pending does not support limit
limit = None

return self._do_request(GET, BASE_URL_DE + '/api/smrt/transactions', {
'from': from_time,
'to': to_time,
'limit': limit,
'pending': pending,
'categories': categories,
'textFilter': text_filter,
'lastId': last_id
})
filters = []
if from_time is not None or to_time is not None:
filters.append(
{
"criteria": {
"from": from_time,
"to": to_time,
},
"type": "DATE_RANGE"
}
)

if direction is not None:
filters.append(
{
"criteria": {
"value": direction
},
"type": "DIRECTION"
}
)

# Suggestions
# => Bad Request
# result = self._do_request(
# method=POST,
# url=BASE_URL_DE + f'/api/feed/accounts/{account_id}/transactions/search/suggestions',
# headers={
# "device-token": self.config.DEVICE_TOKEN.value
# }
# )

account_info = self.get_account_info()
account_id = account_info["accountId"]

result = self._do_request(
method=POST,
url=BASE_URL_DE + f'/api/feed/accounts/{account_id}/transactions/search',
json={
"filterCriteria": {
"filters": filters
},
"searchText": text_filter,
"paginationKey": pagination_key,
},
params={
"limit": limit,
}
)

return result

def get_transactions_limited(self, limit: int = 5) -> dict:
import warnings
warnings.warn(
"get_transactions_limited is deprecated, use get_transactions(limit=5) instead",
DeprecationWarning
# TODO:
def get_insights(self) -> dict:
"""
Retrieves the Insights dashboard data
"""
return self._do_request(
method=GET,
url=BASE_URL_DE + f'/api/insights/dashboard',
)

# TODO:
def get_balance_overview(self) -> dict:
"""
Retrieves balance data
"""
return self._do_request(
method=GET,
url=BASE_URL_DE + f'/api/insights/balance-overview?page=1',
)

# TODO:
def get_recurring_payments(self) -> dict:
"""
Retrieves recurring payments
"""
return self._do_request(
method=GET,
url=BASE_URL_DE + f'/api/insights/recurring-payments',
)

# TODO:
def get_scheduled_payments(self) -> dict:
"""
TODO
"""
return self._do_request(
method=GET,
url=BASE_URL_DE + f'/api/scheduled-payments/overview',
)

# TODO:
def get_expenses_categories(self, category: str, period: str) -> dict:
"""
:param category: id of the category, f.ex. "bars_and_restaurants"
:param period: time period to fetch, f.ex. 2023-02
"""
return self._do_request(
method=GET,
url=BASE_URL_DE + f'/api/insights/balance-overview/expenses/{category}?period=2023-02',
)
return self.get_transactions(limit=limit)

def get_balance_statement(self, statement_url: str):
"""
Expand Down Expand Up @@ -294,7 +382,15 @@ def _do_request(self, method: str = GET, url: str = "/", params: dict = None,
:return: the response parsed as a json
"""
access_token = self.get_token()
_headers = {'Authorization': 'Bearer {}'.format(access_token)}
_headers = {
'Authorization': 'Bearer {}'.format(access_token),
"n26-timezone-identifier": "Europe/Paris",
"x-n26-platform": "android",
"x-n26-app-version": "3.73",
"n26-app-build-number": "202203000",
"content-type": "application/json; charset=utf-8",
"device-token": self.config.DEVICE_TOKEN.value,
}
if headers is not None:
_headers.update(headers)

Expand Down
91 changes: 91 additions & 0 deletions tests/api_responses/transactions2.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,91 @@
{
"tags": [],
"results": [
{
"id": "12345678-1234-abcd-abcd-1234567890ab",
"accountId": "12345678-1234-abcd-abcd-1234567890ab",
"title": "Transaction Title",
"tintedSubtitle": {
"elements": [
{
"value": "1 Feb"
}
]
},
"amount": -100.0,
"amountStyle": "NONE",
"currency": "EUR",
"timestamp": 1675212875923,
"type": "TRANSACTION",
"category": "micro-v2-household-utilities",
"icon": {
"type": "CATEGORY",
"url": "https://cdn.number26.de/feed/transaction-details/categories/light/household_and_utilities.png",
"dark-url": "https://cdn.number26.de/feed/transaction-details/categories/dark/household_and_utilities.png",
"darkUrl": "https://cdn.number26.de/feed/transaction-details/categories/dark/household_and_utilities.png"
},
"balance": {
"title": "",
"amount": 0,
"currency": "EUR"
},
"deeplink": "number26://main/detail/12345678-1234-abcd-abcd-1234567890ab?accountId=12345678-1234-abcd-abcd-1234567890ab&externalId=12345678-1234-abcd-abcd-1234567890ab&source=TX_SEARCH",
"gestures": {
"swipeLeft": {
"description": "Pay back from a Space",
"deeplink": "number26://spaces/transfer?destinationId=12345678-1234-abcd-abcd-1234567890ab&amount=100.00&initialReferenceText=Iris%20%26%20Markus%20Gemeinschaftskonto%20Strom&sourceTxId=12345678-1234-abcd-abcd-1234567890ab",
"tracking": {
"action": "feed.gesture_swipe.left",
"category": "engagement",
"property": "payback"
}
}
},
"externalId": "12345678-1234-abcd-abcd-1234567890ab",
"linkId": "12345678-1234-abcd-abcd-1234567890ab",
"subtitle": {
"template": ""
}
}
],
"paginationKey": "abJlbmREYXRlIjoxNjc1MjczMTM5Ljc3MjAwMDAwMCwidG90YWxBbW91bnQiOi0xMzIuNzAsInRyYW5zYWN0aW9uQ291bnQiOjIsInRyYW5zYWN0aW9uc1BhZ2luYXRpb25LZXkiOiJleUpzWVhOMFJYaDBaWEp1WVd4SlpDSTZJbVptTm1VeVpUazVMV0V4WTJFdE1URmxaQzA1WTJZMkxUUmtOR05tTkdNd05UZzVOU0o5In0=",
"summary": {
"totalAmount": {
"value": "-€132.70",
"appearance": "STANDARD"
},
"subtitle": "2/1/23 - 2/1/23",
"title": "2 Transactions"
},
"filters": {
"filterList": [
{
"type": "DATE_RANGE",
"title": "Date range",
"maxRangeInMonths": 6,
"violationMessage": "Please select a date within a 6 month range"
},
{
"type": "DIRECTION",
"title": "Transaction type",
"value": [
{
"title": "Both incoming & outgoing",
"type": "INCOMING_AND_OUTGOING"
},
{
"title": "Incoming only",
"type": "INCOMING"
},
{
"title": "Outgoing only",
"type": "OUTGOING"
}
]
}
]
},
"searchSuggestions": {
"suggestions": []
}
}