API-style subject test (UPSERT/REVOKE)
Fill in person fields (email / phone / username / name / DOB / IP / country). The browser normalizes and hashes them into token_* fields
and builds a /v1/subjects:upsert-style JSON request as described in docs/API.txt. Only tokens (not raw PII) are sent to the server.
Request preview (JSON)
No request yet.
Response
No request yet.
CSV upload
Upload a single CSV file. Required columns include: external_person_id, action, industry_label, reason_label, ts and at least one token_* column.
Result
No upload yet.
ShieldID API Documentation
Quick Start
ShieldID is a fraud detection API that uses client-side tokenization to protect privacy. All PII is hashed in your application before being sent to the API.
Key Concept: Never send raw PII to the API. Always normalize and hash identifiers client-side using HMAC-SHA256.
API Endpoints
| Method | Endpoint | Description |
|---|---|---|
POST |
/v1/subjects:upsert |
Create or update a fraud subject |
POST |
/v1/subjects:batchUpsert |
Batch create/update multiple subjects |
POST |
/v1/subjects/{id}:revoke |
Revoke a fraud subject |
POST |
/v1/matches:check |
Check if tokens match existing subjects |
GET |
/v1/matches/{id} |
Get match view for a subject |
POST |
/v1/matches:search |
Search for matches with filters |
GET |
/v1/meta/industries |
Get valid industry labels |
GET |
/v1/meta/reasons |
Get valid reason labels |
GET |
/v1/meta/settings |
Get system settings |
GET |
/v1/meta/keys |
Get key metadata |
GET |
/health |
Health check endpoint |
GET |
/metrics |
Prometheus metrics |
Examples by Language
Select a language to see all examples (cURL, token creation, API requests) in that language:
Token Creation
<?php
// HMAC keys (store securely!)
define('HMAC_NETWORK_KEY', 'network_key_mvp_v1_change_in_production');
define('HMAC_SUBJECT_KEY', 'subject_key_mvp_v1_change_in_production');
function normalizeEmail($email) {
if (empty($email)) return '';
return mb_strtolower(trim($email), 'UTF-8');
}
function normalizePhone($phone) {
if (empty($phone)) return '';
$phone = preg_replace('/\D/', '', $phone);
return '+' . $phone;
}
function createToken($key, $value) {
return hash_hmac('sha256', $value, $key);
}
// Create tokens
$tokens = [];
if (!empty($pii['email'])) {
$tokens['token_email'] = createToken(HMAC_NETWORK_KEY, normalizeEmail($pii['email']));
}
if (!empty($pii['phone'])) {
$tokens['token_phone'] = createToken(HMAC_NETWORK_KEY, normalizePhone($pii['phone']));
}
// Create subject_token (sort tokens, join, hash)
ksort($tokens);
$subjectToken = createToken(HMAC_SUBJECT_KEY, implode('', $tokens));
UPSERT Request
<?php
$requestBody = [
'external_person_id' => 'C1-001',
'industry_label' => 'Gaming',
'reason_label' => 'OPPOSITE_BETTING',
'ts' => date('c'),
'tokens' => $tokens,
'notes' => 'case-123'
];
$ch = curl_init('https://encdata.noslodze.lv/v1/subjects:upsert');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY',
'Idempotency-Key: ' . bin2hex(random_bytes(16))
],
CURLOPT_POSTFIELDS => json_encode($requestBody)
]);
$response = curl_exec($ch);
$httpCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$result = json_decode($response, true);
curl_close($ch);
if ($httpCode === 200) {
echo "Success: " . $result['ingest_status'] . "\n";
echo "Match: " . $result['match']['match_status'] . "\n";
} else {
echo "Error: " . ($result['error'] ?? 'Unknown error') . "\n";
}
Check Matches
<?php
$requestBody = [
'tokens' => $tokens
];
$ch = curl_init('https://encdata.noslodze.lv/v1/matches:check');
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY'
],
CURLOPT_POSTFIELDS => json_encode($requestBody)
]);
$response = curl_exec($ch);
$result = json_decode($response, true);
if ($result['match']['match_status'] !== 'NONE') {
echo "Match found: " . $result['match']['match_status'] . "\n";
echo "Sources: " . $result['match']['sources_count'] . "\n";
}
Revoke Subject
<?php
$externalPersonId = 'C1-001';
$ch = curl_init("https://encdata.noslodze.lv/v1/subjects/{$externalPersonId}:revoke");
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'Authorization: Bearer YOUR_API_KEY',
'Idempotency-Key: ' . bin2hex(random_bytes(16))
],
CURLOPT_POSTFIELDS => json_encode([
'ts' => date('c'),
'notes' => 'False positive'
])
]);
$response = curl_exec($ch);
$result = json_decode($response, true);
Token Creation
import hmac
import hashlib
import requests
import json
from datetime import datetime
HMAC_NETWORK_KEY = b'network_key_mvp_v1_change_in_production'
HMAC_SUBJECT_KEY = b'subject_key_mvp_v1_change_in_production'
def normalize_email(email):
return email.strip().lower() if email else ''
def normalize_phone(phone):
if not phone:
return ''
digits = ''.join(filter(str.isdigit, phone))
return f'+{digits}' if digits else ''
def create_token(key, value):
return hmac.new(key, value.encode('utf-8'), hashlib.sha256).hexdigest()
# Create tokens
tokens = {}
if pii.get('email'):
tokens['token_email'] = create_token(HMAC_NETWORK_KEY, normalize_email(pii['email']))
if pii.get('phone'):
tokens['token_phone'] = create_token(HMAC_NETWORK_KEY, normalize_phone(pii['phone']))
# Create subject_token
sorted_tokens = sorted(tokens.items())
subject_token = create_token(HMAC_SUBJECT_KEY, ''.join(v for _, v in sorted_tokens))
UPSERT Request
import uuid
request_body = {
'external_person_id': 'C1-001',
'industry_label': 'Gaming',
'reason_label': 'OPPOSITE_BETTING',
'ts': datetime.utcnow().isoformat() + 'Z',
'tokens': tokens,
'notes': 'case-123'
}
headers = {
'Content-Type': 'application/json',
'Authorization': f'Bearer {API_KEY}',
'Idempotency-Key': str(uuid.uuid4())
}
response = requests.post(
'https://encdata.noslodze.lv/v1/subjects:upsert',
headers=headers,
json=request_body
)
if response.status_code == 200:
result = response.json()
print(f"Success: {result['ingest_status']}")
print(f"Match: {result['match']['match_status']}")
else:
print(f"Error: {response.json().get('error', 'Unknown error')}")
Check Matches
request_body = {
'tokens': tokens
}
response = requests.post(
'https://encdata.noslodze.lv/v1/matches:check',
headers=headers,
json=request_body
)
result = response.json()
if result['match']['match_status'] != 'NONE':
print(f"Match found: {result['match']['match_status']}")
print(f"Sources: {result['match']['sources_count']}")
Revoke Subject
external_person_id = 'C1-001'
response = requests.post(
f'https://encdata.noslodze.lv/v1/subjects/{external_person_id}:revoke',
headers=headers,
json={
'ts': datetime.utcnow().isoformat() + 'Z',
'notes': 'False positive'
}
)
Token Creation
const crypto = require('crypto');
const HMAC_NETWORK_KEY = 'network_key_mvp_v1_change_in_production';
const HMAC_SUBJECT_KEY = 'subject_key_mvp_v1_change_in_production';
function normalizeEmail(email) {
return email ? email.trim().toLowerCase() : '';
}
function normalizePhone(phone) {
if (!phone) return '';
const digits = phone.replace(/\D/g, '');
return digits ? `+${digits}` : '';
}
function createToken(key, value) {
return crypto.createHmac('sha256', key).update(value).digest('hex');
}
// Create tokens
const tokens = {};
if (pii.email) {
tokens.token_email = createToken(HMAC_NETWORK_KEY, normalizeEmail(pii.email));
}
if (pii.phone) {
tokens.token_phone = createToken(HMAC_NETWORK_KEY, normalizePhone(pii.phone));
}
// Create subject_token
const sortedKeys = Object.keys(tokens).sort();
const subjectToken = createToken(HMAC_SUBJECT_KEY, sortedKeys.map(k => tokens[k]).join(''));
UPSERT Request (Node.js)
const https = require('https');
const { v4: uuidv4 } = require('uuid');
const requestBody = {
external_person_id: 'C1-001',
industry_label: 'Gaming',
reason_label: 'OPPOSITE_BETTING',
ts: new Date().toISOString(),
tokens: tokens,
notes: 'case-123'
};
const options = {
hostname: 'encdata.noslodze.lv',
path: '/v1/subjects:upsert',
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
'Idempotency-Key': uuidv4()
}
};
const req = https.request(options, (res) => {
let data = '';
res.on('data', (chunk) => { data += chunk; });
res.on('end', () => {
const result = JSON.parse(data);
if (res.statusCode === 200) {
console.log(`Success: ${result.ingest_status}`);
console.log(`Match: ${result.match.match_status}`);
} else {
console.log(`Error: ${result.error || 'Unknown error'}`);
}
});
});
req.on('error', (error) => {
console.error('Request error:', error);
});
req.write(JSON.stringify(requestBody));
req.end();
UPSERT Request (Browser/Fetch)
const requestBody = {
external_person_id: 'C1-001',
industry_label: 'Gaming',
reason_label: 'OPPOSITE_BETTING',
ts: new Date().toISOString(),
tokens: tokens,
notes: 'case-123'
};
fetch('https://encdata.noslodze.lv/v1/subjects:upsert', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`,
'Idempotency-Key': crypto.randomUUID()
},
body: JSON.stringify(requestBody)
})
.then(response => response.json())
.then(result => {
if (result.ingest_status === 'OK') {
console.log('Success:', result);
console.log('Match:', result.match.match_status);
} else {
console.error('Error:', result.error);
}
})
.catch(error => {
console.error('Request error:', error);
});
Check Matches
fetch('https://encdata.noslodze.lv/v1/matches:check', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${API_KEY}`
},
body: JSON.stringify({ tokens: tokens })
})
.then(response => response.json())
.then(result => {
if (result.match.match_status !== 'NONE') {
console.log(`Match found: ${result.match.match_status}`);
console.log(`Sources: ${result.match.sources_count}`);
}
});
1. UPSERT a Subject
curl -X POST https://encdata.noslodze.lv/v1/subjects:upsert \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"external_person_id": "C1-001",
"industry_label": "Gaming",
"reason_label": "OPPOSITE_BETTING",
"ts": "2025-11-22T10:15:00Z",
"tokens": {
"token_email": "a6e91369...",
"token_phone": "b7f024d1..."
},
"notes": "case-123"
}'
2. Check for Matches
curl -X POST https://encdata.noslodze.lv/v1/matches:check \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"tokens": {
"token_email": "a6e91369...",
"token_phone": "b7f024d1..."
}
}'
3. Revoke a Subject
curl -X POST https://encdata.noslodze.lv/v1/subjects/C1-001:revoke \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-d '{
"ts": "2025-11-22T10:15:00Z",
"notes": "False positive"
}'
4. Get Match View
curl -X GET https://encdata.noslodze.lv/v1/matches/C1-001 \ -H "Authorization: Bearer YOUR_API_KEY"
5. Search Matches
curl -X POST https://encdata.noslodze.lv/v1/matches:search \
-H "Content-Type: application/json" \
-H "Authorization: Bearer YOUR_API_KEY" \
-d '{
"filters": {
"match_status": ["FULL", "STRONG"],
"industry": ["Gaming"],
"min_sources_count": 1
},
"page_size": 200
}'
6. Get Metadata
# Get industries curl https://encdata.noslodze.lv/v1/meta/industries # Get reasons for Gaming industry curl "https://encdata.noslodze.lv/v1/meta/reasons?industry=Gaming" # Get system settings curl https://encdata.noslodze.lv/v1/meta/settings # Get key metadata curl https://encdata.noslodze.lv/v1/meta/keys
Request/Response Examples
UPSERT Request
POST /v1/subjects:upsert
Content-Type: application/json
Authorization: Bearer sk_mvp_test_key
Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000
{
"external_person_id": "C1-001",
"industry_label": "Gaming",
"reason_label": "OPPOSITE_BETTING",
"ts": "2025-11-22T10:15:00Z",
"tokens": {
"token_email": "a6e91369...",
"token_phone": "b7f024d1..."
},
"notes": "case-123"
}
UPSERT Response
HTTP/1.1 200 OK
Content-Type: application/json
X-RateLimit-Limit: 600
X-RateLimit-Remaining: 599
X-RateLimit-Records-Limit: 100000
X-RateLimit-Records-Remaining: 99999
{
"request_id": "rq_1707700000_abcde",
"network_key_id": "nk_v1",
"subject_key_id": "sk_v1",
"token_version": "tv1",
"external_person_id": "C1-001",
"subject_token": "st_a6e91369...",
"ingest_status": "OK",
"ingest_message": null,
"match": {
"match_status": "FULL",
"matched_fields": "email",
"all_matched_tokens": ["email", "phone", "country"],
"sources_count": 4,
"top_source_industries": ["Gaming", "KYC"],
"top_reasons": ["OPPOSITE_BETTING×3", "DOCUMENT_FORGERY_CONFIRMED×1"],
"network_first_seen": "2025-09-14T12:00:00Z",
"network_last_seen": "2025-11-22T09:55:00Z"
}
}
Important Notes
- Security: HMAC keys should be stored securely (environment variables, secrets manager). Never commit keys to version control.
- Normalization: Always normalize PII before hashing. Email: lowercase. Phone: E.164 format (+country code). IP: canonical form only — IPv4 dotted decimal with no leading zeros (e.g. 203.0.113.10); IPv6 RFC 5952 (e.g. 2001:db8::1).
- Name diacritics: Name normalization trims whitespace and case-folds, but does not strip diacritics.
Jānis ŠmitsandJanis Smitsproduce different tokens. This is by design — diacritic folding would reduce precision and merge distinct identities in many locales. - Idempotency: Use
Idempotency-Keyheader (UUID) for UPSERT and REVOKE to prevent duplicates. Sending the same key with a different payload returns409 Conflict. - Rate Limits: 600 requests/minute, 100,000 records/minute. Check
X-RateLimit-*headers. - Industries: Gaming, KYC, Hospitality, Mobility, Fintech
- Error Handling: Always check HTTP status codes. 429 = rate limited (check Retry-After header).
- Network Summary:
sources_countshows count of OTHER organizations (network).top_source_industriesandtop_reasonsinclude all organizations (including your own).network_first_seen/network_last_seenreflect the subject's first and last signal timestamps across all orgs. - Single active owner rule: A subject_token can be active under only one
external_person_idper organization. UPSERTing the same subject under a differentexternal_person_idreturnsingest_status: DUPLICATE_SAME_SUBJECTwith the current owner'sexternal_person_id. Revoke the original first if you need to reassign. - Match response fields:
matched_fields— the deciding token combination that determined thematch_statuslevel (e.g. "email", "phone+dob").all_matched_tokens— array of every identity token type present in your request (e.g. ["email","phone","country"]). Useful for auditing which identifiers you supplied.
- Match level rules for phone: Phone alone → LOW. Phone + country → LOW. Phone combined with stronger identifiers (dob, name, ip, username) yields STRONG or higher.
Common Error Codes
- 400: Bad Request - Invalid JSON, missing required fields, validation errors
- 401: Unauthorized - Invalid or missing API key
- 404: Not Found - Subject not found (for GET endpoints)
- 409: Conflict - Idempotency key conflict or duplicate resource
- 422: Unprocessable Entity - Atomic CSV rejected due to validation/ingest errors (response includes
error,summary, andresults_csv_base64) - 429: Too Many Requests - Rate limit exceeded (check Retry-After header)
- 500: Internal Server Error - Server-side error (check logs)