NOTICE: All information contained herein is, and remains
the property of TechnoCore Automate.
Updated : 2026-03-16
ObjServiceSAFPS integrates with the Southern African Fraud
Prevention Service (SAFPS) API to check ID numbers, contact numbers,
email addresses, and bank account numbers against fraud incident
databases. Supports both ReferenceSearch (summary) and
DetailedObjectSearch (full incident data with subject details).
Results are cached in bloom_safps to minimise API calls:
When a person needs to be verified — for example during a credit
application, onboarding, or collections process — the service runs
through three checks in order, stopping at the first match:
Check 1 — Have we checked this person before?
The system keeps a local record of every person already verified.
If this ID number was checked in the last 7 days and came back
clean, the saved result is reused. If they were previously flagged
as fraud, that flag stays permanently. This avoids paying for the
same check twice.
Check 2 — Is this person on the client's own fraud watchlist?
Clients maintain internal lists of suspect cell phone numbers and
ID numbers gathered from their own operations — for example,
numbers linked to repeat defaulters, known fraudulent applications,
or flagged during collections. If the person's ID or cell number
appears on these lists, they are immediately flagged as fraud
without calling the external service. This is instant, costs
nothing, and catches patterns the client has already identified.
The prefilter tables are configured per deployment in config.yaml:
base:
safps:
prefilter_cell_table: "leads_data_suspect_cellshort"
prefilter_idnum_table: "leads_data_suspect_idnum"
Check 3 — Check with SAFPS (national fraud database)
Only if the person wasn't found in checks 1 or 2, the service
calls SAFPS — the South African Fraud Prevention Service. This is
a national database shared across banks, retailers, and insurers.
SAFPS reports whether the person has any fraud incidents reported
by other companies, is a victim of identity theft, or has filed a
protective registration. This call costs money, so it only happens
when the first two checks don't give an answer.
The result from whichever check catches it looks the same to the
workflow system — a status of "fraud" or "clear" with an incident
count. The workflow doesn't need to know which check provided the
answer.
SAFPS (Southern African Fraud Prevention Service NPC) is a
non-profit fraud prevention organisation. Members can query
the SAFPS database to check if an individual or entity has been
involved in fraud, is a victim of identity theft, or has a
protective registration.
API documentation version: 2.4 (2025-05-19)
| API | URL |
|---|---|
| Authentication | https://auth.safps.org.za/ |
| Token endpoint | https://auth.safps.org.za/connect/token |
| External API | https://external.safps.org.za/ |
| ReferenceSearch (V3) | https://external.safps.org.za/Api/V3/Search/ReferenceSearch |
| DetailedObjectSearch (V3) | https://external.safps.org.za/Api/V3/Search/DetailedObjectSearch |
The authentication server (auth.safps.org.za) is on a separate
domain from the API server (external.safps.org.za).
Standard OAuth 2.0 with client_credentials grant. Bearer tokens
are one-time use — a new token must be obtained for each API call.
POST https://auth.safps.org.za/connect/token
Body (form-encoded):
grant_type=client_credentials
scope=ExternalApi MainApi
Auth: Basic (client_id:client_secret)
Response:
{
"access_token": "eyJ...",
"expires_in": 3600,
"token_type": "Bearer"
}
base:
safps:
token_url: "https://auth.safps.org.za/connect/token"
client_id: "your_client_id"
client_secret: "your_client_secret"
If credentials are not in config.yaml, falls back to the
def_remoteconnections table:
| Column | Value |
|---|---|
remote |
safps |
remoteurl |
Token endpoint URL |
username |
OAuth2 client ID |
remotepassword |
OAuth2 client secret |
settings:
api_base_url: https://external.safps.org.za
reference_search_path: /Api/V3/Search/ReferenceSearch
detailed_search_path: /Api/V3/Search/DetailedObjectSearch
bloom_table: bloom_safps
cache_ttl_days_clean: 7
check_fraud(id_number, cell_number)Summary search returning incident references and log dates.
Runs through three gates in order: cache → prefilter → API.
Results are bloomed into bloom_safps.
Request:
POST /Api/V3/Search/ReferenceSearch
Authorization: Bearer {token}
Content-Type: application/json
{
"idNumber": "1111111111111",
"contactNumber": "",
"emailAddress": "",
"bankAccountNumber": "",
"requestedBy": ""
}
Response:
[
{
"incidentReference": "VICTIM00262474",
"incidentLogDate": "2020-04-07"
},
{
"incidentReference": "SH00262473",
"incidentLogDate": "2020-04-07"
}
]
Parameters are searched independently as OR conditions —
adding more parameters broadens results, not narrows them.
Use a single parameter (e.g. idNumber) for precise searches.
Return dict:
| Field | Description |
|---|---|
incidentCount |
Number of incidents found |
idnumber |
The ID number searched |
incidents |
List of incident references |
status |
"fraud" if incidentCount > 0, else "clear" |
detailed_search(id_number)Full search returning structured subject and incident data
including addresses, police cases, devices, crypto details,
and online activity.
Request: Same format as ReferenceSearch.
Response — array of subject objects:
[
{
"subjectSurname": "string",
"subjectName": "string",
"subjectDateOfBirth": "YYYY-MM-DD",
"subjectGender": "string",
"subjectTitle": "string",
"incidents": [
{
"incidentReference": "SH00262473",
"incidentCategory": {
"categoryNumber": "24",
"categoryName": "Impersonation",
"subCategoryNumber": "3",
"subCategoryName": "Impersonation of another",
"additionalDetails": ""
},
"incidentDate": "2020-04-01",
"incidentLogDate": "2020-04-01",
"memberReference": "ABC123",
"member": "SAFPS",
"reportedBy": "Admin, SAFPS",
"savings": "0.0000",
"loss": "0.0000",
"details": "Description text",
"forensicInformation": "False",
"productAppliedFor": "Credit card",
"degreeOfFraud": "",
"idDocuments": [
{
"type": "Barcode green book",
"number": "1111111111111",
"issueDate": "2022-10-02",
"country": "South Africa"
}
],
"contactNumbers": [
{"type": "Personal", "number": "2222222222"}
],
"addresses": [
{
"type": "Personal",
"province": "Gauteng",
"city": "Pretoria",
"suburb": "Pretoria Central",
"postalCode": "0001",
"streetName": "1",
"streetNumber": "1"
}
],
"emailAddresses": [
{"type": "Personal", "email": "test@test.com"}
],
"bankAccounts": [
{
"bank": "ABSA",
"accountType": "Current",
"accountNo": "1234567890"
}
],
"employers": [
{
"employerName": "Business name",
"employerTelNo": "0121111111",
"occupation": "Tester",
"companyRegisteredName": "Company name",
"companyRegisteredNumber": "Reg number"
}
],
"policeCases": [
{
"policeCaseNumber": "12312313213",
"policeStation": "Pretoria Central",
"policeReportDate": "2020-09-01",
"officer": "Tester van Tester",
"caseType": "Aid, abett or assist",
"caseStatus": "Convicted",
"reasonForFiling": "Account take over",
"reasonForExtension": "Continued risk",
"policeContactNumber": "2222222222",
"policeEmail": "test@case.com",
"policeFax": "",
"details": "Test case"
}
],
"devices": [
{
"deviceType": "Cellular Phone",
"imeiNumber": "1",
"imisNumber": "1",
"ipAddress": "1",
"model": "1",
"make": "Samsung",
"network": "Orange",
"country": "South Africa",
"serialNumber": "1",
"deviceBlacklisted": "True",
"numberBlocked": "True"
}
],
"onlineDetails": [
{
"platformUsername": "username",
"platformType": "type",
"platformAccountStatus": "Active",
"websiteURL": "encoded_url",
"websiteURLStatus": "Active"
}
],
"cryptoDetails": [
{
"currencyType": "Bitcoin",
"walletAddress": "address",
"cryptoExchange": "exchange",
"amount": "3333333.0000",
"walletStatus": "Suspended"
}
]
}
]
}
]
| Code | Status | Description |
|---|---|---|
| 200 | OK | Successful call |
| 204 | NoContent | No information found |
| 400 | BadRequest | Unsuccessful call |
| 401 | Unauthorized | Not authorized |
| Prefix | Type |
|---|---|
SH |
Fraud incident (Shared) |
VICTIM |
Victim of identity theft |
PR |
Protective Registration |
The service uses three layers to minimise API calls:
| Layer | Source | TTL | Cost |
|---|---|---|---|
| Cache (fraud) | bloom_safps |
Permanent | Free |
| Cache (clean) | bloom_safps |
7 days | Free |
| Prefilter (ID) | Client suspect ID table | Instant | Free |
| Prefilter (cell) | Client suspect cell table | Instant | Free |
| SAFPS API | External service | Per call | Paid |
Order of checks: cache → prefilter → API
Prefilter results return the same format as SAFPS API results
with "source": "prefilter_idnum" or "source": "prefilter_cell"
to identify the origin.
Results are stored via the bloom_table_data_structure pattern
into bloom_safps. The bloom pattern auto-creates columns from
the API response fields.
Top-level result shape:
{
"incidentCount": 5,
"idnumber": "1111111111111",
"incidents": [...],
"status": "fraud"
}
| Command | Context Keys | Result Keys |
|---|---|---|
check |
id_number or param1, cell_number (optional) |
_safps_status, _safps_incidents, _safps_result (JSON) |
detailed |
id_number or param1 |
_safps_status, _safps_incidents, _safps_result (JSON) |
# Reference search (summary)
python ObjServiceSAFPS.py check "1111111111111"
# Detailed search (full incident data)
python ObjServiceSAFPS.py detailed "1111111111111"
# Test OAuth2 login
python ObjServiceSAFPS.py test-oauth
| Parameter | Description | Format |
|---|---|---|
idNumber |
SA ID number | 13 digits |
contactNumber |
Phone number | 10 digits, no +27 |
emailAddress |
Email address | string |
bankAccountNumber |
Bank account | string |
requestedBy |
Requesting party | string |
Parameters are OR conditions — use one at a time for
precise results.
Auth: https://auth.safps.org.za/connect/token — OK
API: https://external.safps.org.za/Api/V3/Search/
Reference Search (1111111111111):
Status: fraud
Incidents: 5
Detailed Search (1111111111111):
Subjects: 6
Samual Jones — 1 incident (Victim of impersonation)
george botha — 2 incidents (Victim + PR)
from ObjServiceSAFPS import ObjServiceApi
svc = ObjServiceApi()
# Quick check (reference search + cache)
result = svc.check_fraud("1111111111111")
print(f"Status: {result['status']}")
print(f"Incidents: {result['incidentCount']}")
# Full details
data = svc.detailed_search("1111111111111")
for subject in data:
for incident in subject.get("incidents", []):
cat = incident["incidentCategory"]
print(f" {cat['categoryName']}")
Updated : 2026-03-16
cythonize -3 -a -i ObjServiceSAFPS.py
Compiling /home/axion/projects/axion/factory.service/package.core/ObjServiceSAFPS.py because it changed.
[1/1] Cythonizing /home/axion/projects/axion/factory.service/package.core/ObjServiceSAFPS.py
Updated : 2026-03-16