Report API: Python examples
Here are two Python scripts that may be helpful.
The code on this page is provided as examples for how to get started.
The code works at the time of writing, but this is absolutely not anywhere near "production quality", and there may still be errors. We can not help with changes, improvements - or any coding help at all.
See the Quick start for more details, and the API guide and API spec for all the details.
Retrieve data from the Report API​
This Python script retrieves the settlement data using the Report API.
To use the script:
- Update
fetch-report-api-data.pywith your API credentials. - Run the script:
python3 fetch-report-api-data.py.
The settlement data will be saved in a file with a name like 2025-11-12.csv.
Source code:
fetch-report-api-data.py.
Generate fake data "from" the Report API​
This Python script creates fake data similar to what can be retrieved with the Report API.
It writes per-day JSON payloads
(matching the shape of GET:/report/v2/ledgers/{ledgerId}/{topic}/dates/{ledgerDate}) into compressed files:
out/funds/YYYY-MM-DD.json.gzout/fees/YYYY-MM-DD.json.gz
Example command to create sample data for 1,000,000 payments from 2025-10-01 to 2025-10-31, producing realistic mixes of entry types and attributes for both funds and fees topics:
python generate-fake-report-api-data.py \
--payments 1000000 \
--start 2025-10-01 \
--end 2025-10-31 \
--out ./out \
--ledger-id 302321 \
--recipient-handle NO:123455 \
--currency NOK \
--net-settlement \
--include-gdpr 0.12 \
--seed 42
Net settlement mode adds daily fees-retained (negative in funds, positive in fees) and a payout-scheduled when the balance is positive. The script streams to disk and avoids loading everything in memory at once.
Source code: generate-fake-report-api-data.py.
Source code​
Source code: fetch-report-api-data.py​
#!/usr/bin/env python3
#
# fetch-report-api-data.py
#
# Fetches real data from Vipps MobilePay Report API v2 "dates" endpoint
# and writes a CSV for today's funds & fees across all ledgers.
import requests
import csv
import datetime
import base64
# Set this to:
# True: If using the MIAMI API (for accounting partners)
# False: If using merchant API keys (for merchants)
USE_MIAMI_API = True
# For all types of API users:
CLIENT_ID = 'YOUR_CLIENT_ID'
CLIENT_SECRET = 'YOUR_CLIENT_SECRET'
# For merchants using their own API keys: Add these in addition to the above:
SUBSCRIPTION_KEY = 'YOUR_OCP_APIM_SUBSCRIPTION_KEY'
MERCHANT_SERIAL_NUMBER = 'YOUR_MERCHANT_SERIAL_NUMBER'
# Split the base URLs: ledgers live under settlement/v1; reports live under report/v2
LEDGERS_BASE_URL = 'https://api.vipps.no/settlement/v1'
REPORT_BASE_URL = 'https://api.vipps.no/report/v2'
TOKEN_URL_MIAMI = 'https://api.vipps.no/miami/v1/token'
TOKEN_URL_MERCHANT = 'https://api.vipps.no/accesstoken/get'
COMMON_HEADERS = {
'Vipps-System-Name': 'Report-API-Python-Example',
'Vipps-System-Version': '1.0.0',
'Vipps-System-Plugin-Name': 'Report-API-Python-Example',
'Vipps-System-Plugin-Version': '1.0.0'
}
# Get access token
def get_access_token():
if USE_MIAMI_API:
credentials = f'{CLIENT_ID}:{CLIENT_SECRET}'.encode('utf-8')
encoded_credentials = base64.b64encode(credentials).decode('utf-8')
headers = {
'Authorization': f'Basic {encoded_credentials}',
'Content-Type': 'application/x-www-form-urlencoded; charset=utf-8',
**COMMON_HEADERS
}
data = {'grant_type': 'client_credentials'}
response = requests.post(TOKEN_URL_MIAMI, headers=headers, data=data)
else:
headers = {
'Content-Type': 'application/json',
'client_id': CLIENT_ID,
'client_secret': CLIENT_SECRET,
'Ocp-Apim-Subscription-Key': SUBSCRIPTION_KEY,
'Merchant-Serial-Number': MERCHANT_SERIAL_NUMBER,
**COMMON_HEADERS
}
response = requests.post(TOKEN_URL_MERCHANT, headers=headers, json={})
response.raise_for_status()
return response.json()['access_token']
def _with_auth_headers(access_token):
# Keep existing behavior: always send subscription key and merchant serial if set.
base = {
'Authorization': f'Bearer {access_token}',
**COMMON_HEADERS
}
if SUBSCRIPTION_KEY:
base['Ocp-Apim-Subscription-Key'] = SUBSCRIPTION_KEY
if MERCHANT_SERIAL_NUMBER:
base['Merchant-Serial-Number'] = MERCHANT_SERIAL_NUMBER
return base
# Fetch all ledgers (settlement/v1)
def get_ledgers(access_token):
url = f"{LEDGERS_BASE_URL}/ledgers"
headers = _with_auth_headers(access_token)
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
# Fetch daily report entries for a specific ledger, topic ('funds' or 'fees') and date from report/v2
def get_daily_report_entries(access_token, ledger_id, topic, date):
if topic not in ('funds', 'fees'):
raise ValueError("topic must be 'funds' or 'fees'")
url = f"{REPORT_BASE_URL}/ledgers/{ledger_id}/{topic}/dates/{date}"
headers = _with_auth_headers(access_token)
response = requests.get(url, headers=headers)
response.raise_for_status()
return response.json()
# Save to CSV
def save_to_csv(data, filename):
with open(filename, mode='w', newline='', encoding='utf-8') as file:
writer = csv.writer(file)
writer.writerow([
'Reference (Vipps PSP)', # API field: reference
'Time',
'Ledger Date',
'Entry Type',
'Order ID/Reference (merchant)', # API field: pspReference
'Currency',
'Amount',
'Balance Before',
'Balance After',
'Recipient Handle',
'Message',
'Name',
'Masked Phone'
])
for item in data.get('items', []):
writer.writerow([
item.get('reference'),
item.get('time'),
item.get('ledgerDate'),
item.get('entryType'),
item.get('pspReference'),
item.get('currency'),
item.get('amount'),
item.get('balanceBefore'),
item.get('balanceAfter'),
item.get('recipientHandle'),
item.get('message'),
item.get('name'),
item.get('maskedPhoneNo')
])
if __name__ == "__main__":
try:
token = get_access_token()
today = datetime.date.today().isoformat()
csv_filename = f"{today}.csv" # CSV file named by the date only
# 1) Get ledgers from settlement/v1
ledgers = get_ledgers(token)
# 2) Pull both topics ('funds' and 'fees') from report/v2 to produce example data
all_items = []
for ledger in ledgers.get('ledgers', []):
# Spec field name is 'ledgerId' (not 'id')
ledger_id = ledger.get('ledgerId')
if not ledger_id:
continue
for topic in ('funds', 'fees'):
entries = get_daily_report_entries(token, ledger_id, topic, today)
all_items.extend(entries.get('items', []))
# 3) Save combined items to CSV
save_to_csv({'items': all_items}, csv_filename)
print(f"Daily report entries (funds & fees) saved to {csv_filename}")
except Exception as e:
print(f"Error: {e}")
Source code: generate-fake-report-api-data.py​
#!/usr/bin/env python3
#
# generate-fake-report-api-data.py
#
# Synthetic sample data for Vipps MobilePay Report API v2 "dates" endpoint.
# Writes gzipped JSON payloads per date for topics: funds and fees.
import argparse, base64, datetime as dt, gzip, json, os, random, string
from collections import defaultdict, deque
ISO = "%Y-%m-%dT%H:%M:%S.%fZ"
def dtrange(start, end):
cur = start
while cur <= end:
yield cur
cur += dt.timedelta(days=1)
def rand_time_on(date, start_h=6, end_h=23):
# Random time within [start_h, end_h)
h = random.randint(start_h, end_h - 1)
m = random.randint(0, 59)
s = random.randint(0, 59)
us = random.randint(0, 999_999)
return dt.datetime(date.year, date.month, date.day, h, m, s, us, tzinfo=dt.timezone.utc)
def b64_cursor(s: str) -> str:
return base64.b64encode(s.encode()).decode()
def gen_reference():
# order-<12 alnum>
return "order-" + "".join(random.choices(string.ascii_lowercase + string.digits, k=12))
def gen_psp_ref(prefix=""):
# numeric-ish string, optionally prefixed for clarity
digits = "".join(random.choices(string.digits, k=10))
return f"{prefix}{digits}" if prefix else digits
def masked_phone():
return "xxxx " + "".join(random.choices(string.digits, k=4))
def rand_name():
first = random.choice(["John","Jane","Alex","Chris","Emma","Noah","Liam","Olivia","Ava","Mia","Lucas","Sofie","Ella","Filip","Sara"])
last = random.choice(["Doe","Smith","Hansen","Johansen","Olsen","Andersen","Larsen","Nilsen","Karlsen","Eriksen"])
return f"{first} {last}"
def amount_ore():
# Skewed small-to-medium amounts (NOK). 20–5000 NOK, mode around 350 NOK
# Use triangular distribution (in øre)
low, high, mode = 2000, 500000, 35000 # 20.00–5000.00 NOK, mode 350.00 NOK
val = random.triangular(low, high, mode)
return int(round(val))
def capture_fee_ore(amount, rate=0.0175, fixed=150):
# Simple fee model: 1.75% + NOK 1.50 (150 øre). Round to nearest øre.
return int(round(amount * rate)) + fixed
def weekday_weight(d: dt.date):
# Slightly busier Fri/Sat, a tad quieter Sun
return {
0: 1.00, # Mon
1: 1.05, # Tue
2: 1.05, # Wed
3: 1.10, # Thu
4: 1.20, # Fri
5: 1.15, # Sat
6: 0.90, # Sun
}[d.weekday()]
def parse_args():
p = argparse.ArgumentParser(description="Generate synthetic Vipps MobilePay Report API v2 date payloads")
p.add_argument("--payments", type=int, default=1_000_000)
p.add_argument("--start", type=str, required=True, help="YYYY-MM-DD")
p.add_argument("--end", type=str, required=True, help="YYYY-MM-DD")
p.add_argument("--out", type=str, default="./out")
p.add_argument("--ledger-id", type=str, default="302321")
p.add_argument("--recipient-handle", type=str, default="NO:123455")
p.add_argument("--currency", type=str, default="NOK")
p.add_argument("--seed", type=int, default=42)
p.add_argument("--net-settlement", action="store_true", help="Use net settlement (fees retained). If not set, gross settlement (monthly invoice) is simulated.")
p.add_argument("--include-gdpr", type=float, default=0.10, help="Probability [0..1] that a transaction has GDPR fields populated (name/message/maskedPhoneNo).")
p.add_argument("--tz", type=str, default="Z", help="Timestamp suffix; default Z (UTC-style).")
return p.parse_args()
def write_day_file(out_dir, topic, date, items, page_cursor="sample-page", has_more=False, try_later=False):
os.makedirs(os.path.join(out_dir, topic), exist_ok=True)
out_path = os.path.join(out_dir, topic, f"{date.isoformat()}.json.gz")
payload = {
"cursor": b64_cursor(f"{topic}:{date.isoformat()}:{page_cursor}"),
"hasMore": bool(has_more),
"tryLater": bool(try_later),
"items": items
}
with gzip.open(out_path, "wt", encoding="utf-8") as f:
json.dump(payload, f, ensure_ascii=False, separators=(",", ":"))
return out_path
def main():
args = parse_args()
random.seed(args.seed)
start = dt.date.fromisoformat(args.start)
end = dt.date.fromisoformat(args.end)
assert start <= end, "Start must be <= end"
dates = list(dtrange(start, end))
days = len(dates)
# Allocate payments per day by weekday weights
weights = [weekday_weight(d) for d in dates]
total_w = sum(weights)
per_day = [max(0, int(round(args.payments * (w / total_w)))) for w in weights]
# Adjust to exact total
delta = args.payments - sum(per_day)
for i in range(abs(delta)):
idx = i % days
per_day[idx] += 1 if delta > 0 else -1
# State across days
funds_running_balance = 0
fees_running_balance = 0
# Deferred events scheduled for future dates (refunds, disputes, aborted payouts)
deferred_funds = defaultdict(list) # date -> list of items (dict)
deferred_fees = defaultdict(list)
# For gross settlement we accumulate monthly fees and add a monthly 'fees-invoiced' and 'credit-note' occasionally.
monthly_fees_sum = defaultdict(int) # (year, month) -> total capture-fee sum
# Occasionally plan top-ups / corrections (funds) and corrections (fees)
special_funds_queue = defaultdict(list)
special_fees_queue = defaultdict(list)
# Helper to add item with running balance computed later
def make_item(psp, time, date, entry_type, reference, currency, amount, recipient, message=None, name=None, phone=None):
item = {
"pspReference": psp,
"time": time.strftime(ISO),
"ledgerDate": date.isoformat(),
"entryType": entry_type,
"reference": reference,
"currency": currency,
"amount": amount,
# balanceBefore/After added later
"recipientHandle": recipient
}
if message is not None:
item["message"] = message
if name is not None:
item["name"] = name
if phone is not None:
item["maskedPhoneNo"] = phone
return item
# Scenario probabilities (sum <= 1; remainder is plain capture)
prob_full_refund = 0.08
prob_partial_refund= 0.07
prob_dispute = 0.002
prob_correction = 0.0005
# Occasionally schedule a top-up / credit-note (funds), regardless of payments
for d in dates:
if random.random() < 0.0006:
# top-up: positive
amt = random.randint(50_00, 5_000_00) # 50–5000 NOK
t = rand_time_on(d, 7, 20)
special_funds_queue[d].append(("top-up", amt, t))
if random.random() < 0.0003:
# credit-note (funds): negative
amt = -random.randint(50_00, 1_000_00)
t = rand_time_on(d, 7, 20)
special_funds_queue[d].append(("credit-note", amt, t))
if random.random() < prob_correction:
# correction (funds), small adjustment positive or negative
amt = random.choice([-1, 1]) * random.randint(50, 5_000)
t = rand_time_on(d, 8, 22)
special_funds_queue[d].append(("correction", amt, t))
if random.random() < prob_correction:
# correction (fees)
amt = random.choice([-1, 1]) * random.randint(50, 5_000)
t = rand_time_on(d, 8, 22)
special_fees_queue[d].append(("correction", amt, t))
# To simulate occasional payout-aborted:
planned_payout_abort = {} # date -> amount to return
# Process each day
for day_idx, date in enumerate(dates):
funds_items = []
fees_items = []
# Bring in deferred items from earlier days
if deferred_funds.get(date):
funds_items.extend(deferred_funds.pop(date))
if deferred_fees.get(date):
fees_items.extend(deferred_fees.pop(date))
# Add queued specials
for (etype, amt, t) in special_funds_queue.get(date, []):
psp = gen_psp_ref("SPC")
funds_items.append(make_item(psp, t, date, etype, "", args.currency, amt, args.recipient_handle))
for (etype, amt, t) in special_fees_queue.get(date, []):
psp = gen_psp_ref("SPC")
fees_items.append(make_item(psp, t, date, etype, "", args.currency, amt, args.recipient_handle))
# Payments captured today
captures_today = per_day[day_idx]
for _ in range(captures_today):
ref = gen_reference()
cap_amount = amount_ore()
cap_time = rand_time_on(date, 6, 23)
psp_cap = gen_psp_ref()
# GDPR fields?
msg = name = phone = None
if random.random() < args.include_gdpr:
msg = f"Payment for {ref}"
name = rand_name()
phone = masked_phone()
# funds: capture
funds_items.append(
make_item(psp_cap, cap_time, date, "capture", ref, args.currency, cap_amount, args.recipient_handle, msg, name, phone)
)
# fees: capture-fee for same psp/reference
fee_amount = capture_fee_ore(cap_amount)
fee_time = cap_time + dt.timedelta(seconds=random.randint(1, 120))
fees_items.append(
make_item(psp_cap, fee_time, date, "capture-fee", ref, args.currency, -fee_amount, args.recipient_handle)
)
# Track monthly fee totals for gross settlement
monthly_key = (date.year, date.month)
if not args.net_settlement:
monthly_fees_sum[monthly_key] += fee_amount
# Refund scenarios
r = random.random()
if r < prob_full_refund:
# full refund 0-30 days after capture (sometimes same day)
days_after = random.choices([0,1,2,3,4,5,6,7,14,21,30],[40,20,10,8,6,5,4,3,2,1,1])[0]
refund_date = date + dt.timedelta(days=days_after)
if refund_date <= end:
refund_time = (cap_time + dt.timedelta(hours=random.randint(1, 72)))
refund_time = refund_time.replace(tzinfo=dt.timezone.utc)
psp_ref = gen_psp_ref("RFD")
item = make_item(psp_ref, refund_time, refund_date, "refund", ref, args.currency, -cap_amount, args.recipient_handle)
deferred_funds[refund_date].append(item)
elif r < prob_full_refund + prob_partial_refund:
# 1-2 partial refunds totalling 10-90% of amount
n_parts = random.choice([1,2])
total_pct = random.randint(10, 90) / 100.0
remaining = int(cap_amount * total_pct)
for i in range(n_parts):
part = remaining if i == n_parts - 1 else random.randint(1, remaining - (n_parts - i - 1))
remaining -= part
days_after = random.choices([0,1,2,3,7,14],[50,20,10,8,7,5])[0]
refund_date = date + dt.timedelta(days=days_after)
if refund_date <= end:
refund_time = (cap_time + dt.timedelta(hours=random.randint(1, 48)))
refund_time = refund_time.replace(tzinfo=dt.timezone.utc)
psp_ref = gen_psp_ref("RFD")
item = make_item(psp_ref, refund_time, refund_date, "refund", ref, args.currency, -part, args.recipient_handle)
deferred_funds[refund_date].append(item)
# Dispute scenario
if random.random() < prob_dispute:
# retain disputed funds some days after capture (negative), maybe returned later (positive)
retain_days = random.randint(5, 25)
retain_date = date + dt.timedelta(days=retain_days)
if retain_date <= end:
amt = -min(cap_amount, random.randint(int(cap_amount*0.2), int(cap_amount*1.0)))
retain_time = cap_time + dt.timedelta(days=retain_days, hours=random.randint(0, 12))
deferred_funds[retain_date].append(
make_item(gen_psp_ref("DSP"), retain_time, retain_date, "retained-disputed-capture", ref, args.currency, amt, args.recipient_handle)
)
# sometimes returned
if random.random() < 0.6:
return_days = retain_days + random.randint(7, 35)
return_date = date + dt.timedelta(days=return_days)
if return_date <= end:
return_time = cap_time + dt.timedelta(days=return_days, hours=random.randint(0, 12))
deferred_funds[return_date].append(
make_item(gen_psp_ref("DSP"), return_time, return_date, "returned-disputed-capture", ref, args.currency, -amt, args.recipient_handle)
)
# Settlement-related entries for the day
# Net settlement: fees retained daily
if args.net_settlement:
# Sum fees on fees ledger today (negative entries)
fees_sum_today = -sum(i["amount"] for i in fees_items if i["entryType"] == "capture-fee")
if fees_sum_today > 0:
t = dt.datetime(date.year, date.month, date.day, 23, 50, 0, 0, tzinfo=dt.timezone.utc)
psp_fee = gen_psp_ref("FEE")
# funds: negative
funds_items.append(
make_item(psp_fee, t, date, "fees-retained", "", args.currency, -fees_sum_today, args.recipient_handle)
)
# fees: positive
fees_items.append(
make_item(psp_fee, t, date, "fees-retained", "", args.currency, fees_sum_today, args.recipient_handle)
)
else:
# Gross settlement: invoice on the last day of the month for accumulated fees
last_of_month = (date + dt.timedelta(days=31)).replace(day=1) - dt.timedelta(days=1)
if date == last_of_month:
key = (date.year, date.month)
total_fees = monthly_fees_sum.get(key, 0)
if total_fees > 0:
t = dt.datetime(date.year, date.month, date.day, 10, 0, 0, 0, tzinfo=dt.timezone.utc)
psp = gen_psp_ref("INV")
fees_items.append(
make_item(psp, t, date, "fees-invoiced", f"INV-{date.year}{date.month:02d}", args.currency, total_fees, args.recipient_handle)
)
# Occasionally a small credit-note on fees
if random.random() < 0.05:
credit = random.randint(100, min(5_000, total_fees))
t2 = t + dt.timedelta(hours=1)
fees_items.append(
make_item(gen_psp_ref("CRN"), t2, date, "credit-note", f"CN-{date.year}{date.month:02d}", args.currency, -credit, args.recipient_handle)
)
# Payout scheduling (funds)
day_delta = sum(i["amount"] for i in funds_items)
est_balance_after = funds_running_balance + day_delta
if est_balance_after > 0:
payout_amt = -est_balance_after # bring to ~0
t = dt.datetime(date.year, date.month, date.day, 22, 0, 0, 0, tzinfo=dt.timezone.utc)
psp_po = gen_psp_ref("PO")
funds_items.append(
make_item(psp_po, t, date, "payout-scheduled", f"PAYOUT-{date.isoformat()}", args.currency, payout_amt, None)
)
# Rarely: payout aborted next day (balance returns)
if random.random() < 0.0008:
planned_payout_abort[date + dt.timedelta(days=1)] = -payout_amt
# Apply payout-aborted if planned for this date
if planned_payout_abort.get(date):
amt_back = planned_payout_abort.pop(date)
t = dt.datetime(date.year, date.month, date.day, 8, 30, 0, 0, tzinfo=dt.timezone.utc)
funds_items.append(
make_item(gen_psp_ref("POA"), t, date, "payout-aborted", f"PAYOUT-ABORT-{date.isoformat()}", args.currency, amt_back, None)
)
# Sort items by time for deterministic balances
funds_items.sort(key=lambda x: x["time"])
fees_items.sort(key=lambda x: x["time"])
# Compute running balances and set balanceBefore/After
for it in funds_items:
it["balanceBefore"] = funds_running_balance
funds_running_balance += it["amount"]
it["balanceAfter"] = funds_running_balance
for it in fees_items:
it["balanceBefore"] = fees_running_balance
fees_running_balance += it["amount"]
it["balanceAfter"] = fees_running_balance
# Write per-day payloads
write_day_file(args.out, "funds", date, funds_items)
write_day_file(args.out, "fees", date, fees_items)
# Done
print(f"Wrote {len(dates)} funds files and {len(dates)} fees files to: {args.out}")
if __name__ == "__main__":
main()