Skip to content

Importing a file

You don’t need an integration to get data into SpeculaPro. An administrator can load customers and transactions from a file under Settings → Data import: a spreadsheet saved as CSV, or a JSON export from your systems.

Every row goes through exactly the same checks as the ingest API, and each record is loaded once: if you upload the same file again, the rows already loaded are reported as duplicates and nothing is counted twice. Rows with problems are listed by line number, and the good rows still load.

Use it for:

  • a pilot, before your integration team has connected your systems;
  • a backfill: loading past customers and transactions so rules have history to look back over;
  • a one-off correction, such as customers whose risk ratings changed in a review.

For day-to-day monitoring, connect your systems to the ingest API instead, so transactions arrive as they happen rather than when someone remembers to upload a file. If your systems already produce end-of-day files, a file drop collects them from your SFTP server or bucket on a schedule, with the same columns as this screen.

Importing needs the settings.manage permission. In the built-in roles only the Administrator has it. Other people don’t see the Data import tab. See Roles.

  1. Import customers before their transactions. A transaction for a customer SpeculaPro hasn’t seen is still accepted: SpeculaPro creates a placeholder customer that your customer record replaces later. But rules check each transaction as it arrives. Until the real customer record is there, rules that depend on the customer’s risk rating, KYC tier or segment can’t use them, and those transactions are not re-checked when the customer arrives.
  2. Load FX rates first if any transaction is in a foreign currency. A transaction in a currency other than your workspace’s base currency is rejected when there is no exchange rate for its date. See Currencies and FX.
  3. Agree what happens to alerts. Imported transactions are checked against every rule that is running, just like live ones. A backfill of three months of history can therefore raise alerts about old activity. Decide with your MLRO beforehand whether to switch rules off during a backfill, or to let the alerts come and review them.
  1. Select Settings in the left-hand menu, then the Data import tab. You can also go straight to /settings#import.
  2. Choose what the file contains: the Transactions tab or the Customers tab. The panel lists the Required columns and the Optional ones for that kind.
  3. Select Download a sample file to get a small CSV with the right column names. Open it in your spreadsheet program and use it as your template.
  4. Fill in your rows and save the file as CSV (comma-separated, UTF-8), or export JSON from your system. See The columns below.
  5. Select the box labelled Choose a CSV or JSON file and pick your file, or drag the file onto the box (it turns gold while you drag). Its name and size appear in the box.
  6. Select Import customers or Import transactions (the button follows the tab you chose). Large files take a little while; keep the page open until the results appear.
  7. Read the results. See Reading the results.
Settings, Data import tab: the Customers and Transactions tabs, the required and optional columns, Download a sample file, and the file drop area Settings, Data import tab: the Customers and Transactions tabs, the required and optional columns, Download a sample file, and the file drop area

The first row of a CSV file must be the header row with the column names. Names are not case-sensitive, extra columns are ignored, and the columns can be in any order. Leave a cell empty for a value you don’t have.

CSV file drops use exactly these columns too, so a file that imports here also loads from an SFTP server or bucket.

Column Required What to put Example
external_id Yes Your own unique reference for the transaction. It is how SpeculaPro recognises the same transaction if it is imported twice TX-2026-104455
occurred_at Yes When it happened, in ISO 8601 (see Dates and times) 2026-10-08T09:30:00Z
customer_external_id Yes Your reference for the customer C-001
direction Yes in (money received), out (money sent) or internal (between the customer’s own accounts) out
type Yes transfer, payment, deposit, withdrawal, cash_in, cash_out, airtime, bill_payment, loan_disbursement, loan_repayment, refund, reversal, chargeback, fee or other cash_in
channel Yes momo, bank_transfer, instant_payment, card, pos, atm, agent, branch, cash, online, mobile_app, ussd, qr, crypto, cheque or other momo
amount Yes, unless you give amount_minor The amount in major units, the way people write it. Commas are allowed; no currency sign 1,250.50
amount_minor Instead of amount The amount in minor units (pesewas for GHS), a whole number. Used instead of amount when both are present 125050
currency Yes Three-letter code. Lower case is accepted GHS
account_external_id No Your reference for the account, wallet or card used W-0244123456
account_type No bank, wallet, card, merchant, agent, loan, virtual or other wallet
customer_type No individual or business. Used only if SpeculaPro has to create a placeholder customer individual
status No pending, completed, failed, reversed or blocked. Empty means completed completed
counterparty_name No The other side’s name Ama Mensah
counterparty_identifier No The other side’s wallet number, account number or card token. Encrypted on arrival 0244123456
counterparty_provider No The other side’s network or bank mtn_momo
counterparty_country No Two-letter country code of the other side NG
country No Two-letter country code where the transaction took place GH
mcc No Merchant category code, four digits, for card payments 5411
reference No Payment reference or narration, up to 140 characters School fees Term 1
description No A longer description, up to 500 characters

What each field means for monitoring is explained in Transactions. A CSV file can’t carry the API’s less common fields (device fingerprint, IP address, location, custom attributes); use JSON or the API for those.

Column Required What to put Example
external_id Yes Your own unique reference for the customer C-001
type Yes individual or business individual
full_name No Name, up to 200 characters Kofi Boateng
segment No Your own grouping, up to 64 characters Retail
risk_rating No unrated, low, medium, high or unacceptable. Empty means unrated medium
kyc_level No Your KYC tier, a whole number from 0 to 10 2
status No active, dormant, restricted or exited. Empty means active active
phone No Phone number. Local numbers are read with your workspace’s country code. Encrypted on arrival 0244123456
email No Email address. Encrypted on arrival [email protected]
national_id_type No ghana_card, passport, voter_id, drivers_licence, tin, company_reg or other. Empty means other ghana_card
national_id No The ID number. Encrypted on arrival GHA-123456789-0
nationality No Two-letter country code GH
country No Two-letter country code of residence GH
date_of_birth No YYYY-MM-DD 1988-04-17
registration_date No For businesses, the date of incorporation, YYYY-MM-DD 2015-02-01
occupation No Up to 100 characters Trader
onboarded_at No When the customer joined you, in ISO 8601 2024-01-15

What each field means is explained in Customers. Phone numbers, email addresses and ID numbers are encrypted as they arrive; only people with permission to view protected personal data can see them afterwards. See Personal data.

Write times in ISO 8601, for example:

You write SpeculaPro reads
2026-10-08T09:30:00Z 09:30 UTC on 8 October 2026 (the Z means UTC)
2026-10-08T09:30:00+01:00 08:30 UTC
2026-10-08 09:30:00 09:30 in your workspace’s time zone
2026-10-08 Midnight at the start of 8 October, in your workspace’s time zone

A transaction can’t be dated in the future (more than 5 minutes ahead of SpeculaPro’s clock) or before the year 2000.

SpeculaPro looks at the content to tell which format a file is in: if it starts with [ or {, it is JSON; otherwise it is CSV.

CSV. Save from your spreadsheet as CSV UTF-8 (comma delimited). A cell containing a comma, such as "1,250.50", must be in quotes; spreadsheet programs do this for you.

JSON. Each record has the same fields as in the ingest API, including the API-only fields CSV can’t carry. Three layouts are accepted:

[
{"external_id": "C-001", "type": "individual", "full_name": "Kofi Boateng"},
{"external_id": "C-002", "type": "business", "full_name": "Volta Agro Exports Ltd"}
]
{"items": [{"external_id": "C-001", "type": "individual"}]}

or one record per line (newline-delimited JSON, often saved as .ndjson):

{"external_id": "C-001", "type": "individual"}
{"external_id": "C-002", "type": "business"}

In JSON, amounts are given as amount_minor (minor units) and times as RFC 3339, exactly as in the API. A JSON time without Z or an offset is rejected, unlike in CSV. What the “line” numbers in the results mean depends on the layout:

  • One record per line (.ndjson): the real line number in the file, as your text editor shows it. Blank lines are skipped but still counted, so the numbers always match the file.
  • A list ([ … ] or {"items": [ … ]}): the record’s position in the list, starting at 1.

If the file can’t be read as JSON at all, the whole file is rejected with one invalid JSON: … problem on line 1.

Limit Value
Rows per file 50,000. A larger file is refused as a whole, with a message such as 60000 rows; split the file into parts of up to 50000
File size 8 MB by default (SP_INGEST_MAX_BODY_BYTES). A larger file is refused as a whole as too large (HTTP 413)

The size limit is usually the one you reach first: a transaction row with a counterparty and a reference takes about 150 to 250 bytes, so 8 MB holds roughly 30,000 to 50,000 rows. If a file is refused, split it into parts and import them one after the other, oldest first.

When the import finishes, five figures appear under the button:

Figure What it means What to do
Rows Data rows found in the file (the header doesn’t count)
Loaded New records that were stored Nothing
Already loaded Records SpeculaPro already had with exactly the same content, or customer updates older than the version it holds. Nothing was changed Nothing. This is what a re-upload looks like
Rejected Rows that failed a check. Each is listed below with its line number and the reason Fix those rows and import them again
Retry Rows that weren’t loaded because of a temporary problem on SpeculaPro’s side Import the same file again: rows already loaded are skipped

Below the figures, each problem is listed with its line number and the reason, for example line 14 amount “1.234” has more decimals than GHS allows. In a CSV file, line 1 is the header, so line 14 is the thirteenth data row: the same row number your spreadsheet shows. The list shows at most 200 problems: all problems, whether the row couldn’t be read, broke a rule or couldn’t be loaded, are sorted by line first, so you always see the earliest 200. If there are more, fix those and import again to see the rest.

Message What it means How to fix it
missing column(s): occurred_at, currency The header row lacks required columns, so nothing was imported Add the columns, spelled exactly as in The columns
could not read the header row: … The file is empty or isn’t readable as CSV Save it again as CSV UTF-8 (comma delimited)
the file has no data rows Only a header row Add your rows
amount "" is not a positive number No amount (and no amount_minor) Fill in the amount
amount "-50" is not a positive number A negative amount Amounts are always positive; the direction says which way money moved. Record refunds and reversals with their type
amount "1.234" has more decimals than GHS allows More decimals than the currency has (GHS has two) Round to the currency’s decimals
amount_minor must be a whole number A decimal point or text in amount_minor Use whole pesewas: GHS 12.50 is 1250
time "08/10/2026": use ISO 8601, e.g. 2026-10-09T14:30:00Z A date in a local format Use 2026-10-08, with the time if you have it. In a spreadsheet, format the column as text yyyy-mm-dd hh:mm:ss before saving
occurred_at is in the future A date after now, often a day and month swapped Check the date
occurred_at is before 2000 A placeholder or corrupted date Use the real date
expected value to be one of "in, out, internal" (direction: debit) A value outside the allowed list. The part in brackets names the column and the value you sent Use one of the listed values; see The columns
expected string to match pattern ^[A-Z]{2}$ (country: Ghana) A country written in full Use the two-letter code, GH
expected string to match pattern ^[0-9]{4}$ (mcc: …) A merchant category code that isn’t four digits Use four digits, keeping leading zeros (0742)
expected string to be RFC 3339 date (date_of_birth: 17/04/1988) A date of birth not in YYYY-MM-DD Write 1988-04-17
kyc_level must be a whole number Text such as Tier 2 Write 2
phone is not a valid number A phone number that can’t be read Use the full number, with the country code for foreign numbers (+234…)
currency must be an ISO 4217 code A currency code that doesn’t exist, such as CED Use GHS
no FX rate for USD→GHS on or before 2026-10-08; load rates via /api/v1/ingest/fx-rates A foreign-currency transaction with no exchange rate Load FX rates for those dates first, then import again. See Currencies and FX
external_id was already used for a different transaction The reference was used before for a transaction with different details See the caution above
external_id repeated in this batch with different content The same reference appears twice in the file with different details Give each transaction its own reference
external_id has leading or trailing spaces A space before or after the reference (JSON only; CSV cells are trimmed) Remove the spaces
lines 501–1000 not loaded: temporary problem, upload again SpeculaPro couldn’t store that block of rows just then; they are counted under Retry Import the same file again

For the full list of checks behind these messages, see Troubleshooting sending data.

Imported records are stored exactly like records sent through the API: each transaction with its original content as evidence, and customers’ protected details encrypted. Imported transactions go through the same pipeline as live ones, so they appear on the Transactions screen within seconds and are checked against your running rules.

The Data import screen calls POST /api/v1/imports/transactions or POST /api/v1/imports/customers, and you can call it yourself, for example from a script that loads a nightly extract while your integration is being built. It needs a signed-in person with settings.manage, not an API key. See Imports API for the full reference.

Terminal window
curl -s -b ~/.sp-cookies -H 'Content-Type: text/csv' \
--data-binary @customers.csv \
"$SP/api/v1/imports/customers"

Use --data-binary, not -d: -d strips the line breaks and the file arrives as one line.