Open Finance
Getting started with open banking APIs: a hands-on developer guide
Build your first Fiskil sandbox integration: authenticate, create a test user, complete consent and retrieve paginated accounts and transactions with Python.
Start an open banking integration by completing one sandbox consent journey and retrieving the authorised account and transaction data from your backend. This Australian developer guide walks through that first integration with Fiskil's Data API, using a small Python script and the Console's hosted consent flow.
The goal is narrow: prove that your application can authenticate, identify a test user, complete consent and finish retrieving the available pages. Sandbox data is test data. A successful run does not establish live-bank coverage, data freshness or production approval.
If you are choosing what to evaluate and how to make a buying decision, start with our banking API proof-of-concept guide. This walkthrough supplies the developer steps underneath that plan.
Understand the two separate permissions
Your server authenticates to Fiskil using a client ID and client secret. The customer separately authorises data sharing through the consent journey. A valid API token alone does not give your app access to a customer's banking data.
Fiskil's authentication guide documents POST /v1/token. Its response uses the field token, with expires_in: 900: a 15-minute bearer token. Fiskil does not issue refresh tokens for this client authentication flow. Obtain another token securely when it expires.
These are Fiskil Data API calls. Do not mix them with a direct bank's CDR endpoints, OAuth implementation or response models. In this walkthrough, the hosted consent journey handles the provider-facing interaction.
1. Prepare the sandbox and local settings
Create a Fiskil Console account. The current testing guide describes self-service signup and free, unmetered sandbox use without a credit card or sales call. That allowance concerns testing, not production pricing.
In Settings > API Keys, create the credentials for your test integration. Store the secret when it is shown. Configure the consent UI's purpose, data history and consent period for the feature you intend to test, following the quick start.
Use Python 3.10 or later. The example uses only the standard library, including urllib.request, so no packages are required. In a private local folder, save fiskil.local.json:
{
"client_id": "REPLACE_WITH_YOUR_CLIENT_ID",
"client_secret": "REPLACE_WITH_YOUR_CLIENT_SECRET",
"test_email": "REPLACE_WITH_AN_EMAIL_YOU_CONTROL",
"test_phone": "REPLACE_WITH_YOUR_TEST_PHONE_IN_E164_FORMAT",
"end_user_id": ""
}
Replace the placeholders locally. Use contact details you control for testing. Add fiskil.local.json to .gitignore before saving credentials; keep the folder private and never serve the file to a browser. The script prints a test-user ID or record counts, without printing tokens or banking payloads.
2. Save the small API client
Save this as sandbox.py beside the settings file. It pins X-Fiskil-Version: v3. Fiskil's versioning guide explains that /v1/ remains the URL namespace: the header or Console setting selects the API major version.
import json
import sys
from pathlib import Path
from urllib.error import HTTPError
from urllib.parse import urljoin, urlparse, parse_qsl, urlencode, urlunparse
from urllib.request import Request, urlopen
API = "https://api.fiskil.com"
def request(url, token=None, payload=None):
headers = {"Accept": "application/json", "X-Fiskil-Version": "v3"}
if token:
headers["Authorization"] = f"Bearer {token}"
data = None
if payload is not None:
headers["Content-Type"] = "application/json"
data = json.dumps(payload).encode()
req = Request(url, data=data, headers=headers,
method="POST" if payload is not None else "GET")
try:
with urlopen(req, timeout=20) as response:
return json.load(response)
except HTTPError as error:
raise RuntimeError(f"Fiskil returned HTTP {error.code}; stop and investigate.") from None
def count_pages(path, key, end_user_id, token):
next_url = API + path
seen = set()
count = 0
for _ in range(20):
parsed = urlparse(urljoin(API, next_url))
if (parsed.scheme != "https" or parsed.netloc != "api.fiskil.com"
or not parsed.path.startswith("/v1/")):
raise RuntimeError("Unexpected pagination destination.")
query = [(k, v) for k, v in parse_qsl(parsed.query)
if k != "end_user_id"]
query.append(("end_user_id", end_user_id))
url = urlunparse(parsed._replace(query=urlencode(query), fragment=""))
if url in seen:
raise RuntimeError("Repeated pagination link.")
seen.add(url)
body = request(url, token=token)
if not isinstance(body.get(key), list):
raise RuntimeError(f"Missing {key} collection.")
count += len(body[key])
next_url = body.get("links", {}).get("next")
if not next_url:
return {"loaded_records": count, "retrieved_pages_complete": True}
if not isinstance(next_url, str):
raise RuntimeError("Invalid next-page link.")
return {"loaded_records": count, "retrieved_pages_complete": False}
def main():
command = sys.argv[1] if len(sys.argv) == 2 else ""
if command not in ("create-user", "read"):
raise RuntimeError("Use: python3 sandbox.py create-user|read")
config = json.loads(Path("fiskil.local.json").read_text())
for key in ("client_id", "client_secret"):
if not isinstance(config.get(key), str) or not config[key]:
raise RuntimeError(f"Set {key} in fiskil.local.json.")
if command == "create-user" and config.get("end_user_id"):
raise RuntimeError("An end-user ID is already saved; use read or inspect Console.")
if command == "read" and not config.get("end_user_id"):
raise RuntimeError("Save the test end_user_id first.")
auth = request(API + "/v1/token", payload={
"client_id": config["client_id"], "client_secret": config["client_secret"]
})
token = auth.get("token")
if not isinstance(token, str) or not token:
raise RuntimeError("No bearer token returned.")
if command == "create-user":
result = request(API + "/v1/end-users", token=token, payload={
"name": "Sandbox Developer Test", "email": config["test_email"],
"phone": config["test_phone"]
})
end_user_id = result.get("end_user_id")
if not isinstance(end_user_id, str) or not end_user_id:
raise RuntimeError("No end-user ID returned; inspect Console before retrying.")
print(json.dumps({"end_user_id": end_user_id}, indent=2))
else:
result = {}
for key in ("accounts", "transactions"):
result[key] = count_pages(f"/v1/banking/{key}?page[size]=100",
key, config["end_user_id"], token)
print(json.dumps(result, indent=2))
if __name__ == "__main__":
try:
main()
except Exception as error:
print(f"Stopped: {error}", file=sys.stderr)
sys.exit(1)
The example has been checked locally with controlled API responses. An authenticated Fiskil sandbox journey has not been run as part of preparing this guide. Treat the live steps below as your integration acceptance test.
3. Create one test end user
Run this once from that folder:
python3 sandbox.py create-user
The script exchanges your credentials, then calls POST /v1/end-users, as described in the quick start. Copy the returned end_user_id into the empty field in fiskil.local.json.
If the request times out or you lose the result, inspect End-Users in Console before retrying. Do not assume an uncertain response means nothing was created. Keep one test consent per end user for this first run, as the testing guide recommends.
In a real application, your backend maps its authenticated customer to the correct end-user ID. Never let an arbitrary browser-supplied ID decide whose banking data to retrieve.
4. Complete the hosted sandbox consent journey
Follow the sandbox testing steps:
- Select the test user in End-Users and choose Create Auth Session.
- Open the generated link and select Allow.
- Choose the sandbox bank provider.
- Use an email inbox you can access to receive and enter the one-time password.
- Select an available test account, then choose Confirm and share.
For this first run, using Console avoids building the consent interface before you have proved the data path. When integrating it into your app, create an auth session on your backend and use the Fiskil Link SDK. Follow the current request and session response fields rather than inventing a bank-token exchange in the browser.
Consent completion and dataset availability are separate events. Configure a development webhook receiver in Settings > Webhooks. Follow the webhook guide to verify X-Fiskil-Signature against the original request bytes before processing the event. Store the signing secret separately from the API client secret.
Wait for the relevant banking.accounts.sync.completed and banking.transactions.sync.completed notifications for your test user before treating those datasets as ready. A consent.received event records consent; it does not establish that full transaction syncing has completed. The recent-transactions event is also distinct from the full detailed-transaction event.
5. Retrieve accounts and transactions
Run:
python3 sandbox.py read
The script reads GET /v1/banking/accounts and GET /v1/banking/transactions for the fixed end user in your local settings. It follows links.next using the documented cursor pagination.
Each collection reports loaded_records and retrieved_pages_complete. Counts depend on your test consent and the data available. There is no universal account or transaction count to match.
The loop stops after 20 pages per collection. If more pages remain, it reports retrieved_pages_complete: false; do not present that result as a finished import. A true result means the returned pagination chain ended. It does not prove that every expected account or date is present, or that the data was refreshed when you ran the script.
The client rejects pagination destinations outside Fiskil's HTTPS API namespace before attaching the bearer token, and retains the configured end-user scope. A deployed importer also needs durable progress, appropriate pacing, retries and update handling. This short example deliberately stops on HTTP failures so you can investigate the first integration without hidden retry behaviour.
6. Check the failure paths before expanding
Use this acceptance checklist for the first sandbox integration. The expected application behaviour is a recommendation, not a promise that every fixture is supplied automatically by the sandbox.
| Test | Evidence to keep |
|---|---|
| Consent completes | The correct test user, selected account and consent outcome |
| Data becomes available | Verified dataset event and successful retrieval for that user |
| More than one page is returned | Every next link is followed, or the import is explicitly marked partial |
| Consent is cancelled or sharing ends | A clear next step; the app stops assuming ongoing access |
| The token expires | The backend obtains a new token; credentials stay private |
| A notification is delivered again | The receiver deduplicates processing using message_id |
Use controlled responses for cases you cannot reliably trigger in the test environment. An empty collection on its own does not distinguish an empty account from incomplete sync, a different user or an insufficient consent scope. Check the relevant consent, dataset event and request before reporting “no transactions”.
Keep production readiness separate
This walkthrough retrieves existing test data. It does not initiate payments or request another bank-data refresh. It also does not implement application login, a webhook server, persistent storage or a production retry policy.
Before using real customer data, work through Fiskil's go-live checklist. Confirm the access arrangement, institutions, account types, datasets and operating requirements for your product. Submit the live-access questionnaire in Console and complete the required review before configuring production.
For Australian teams building a consented bank-data feature, Fiskil's Banking API provides the documented integration used here. Start with one complete sandbox journey, retain the test evidence and use the proof-of-concept guide to decide what still needs proving before launch.


