ShieldID — CSV Upload & API Test

CSV schema and API semantics are described in docs/CSV.txt and docs/API.txt.
Unique fraud users

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.
Download Results CSV

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 Šmits and Janis Smits produce different tokens. This is by design — diacritic folding would reduce precision and merge distinct identities in many locales.
  • Idempotency: Use Idempotency-Key header (UUID) for UPSERT and REVOKE to prevent duplicates. Sending the same key with a different payload returns 409 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_count shows count of OTHER organizations (network). top_source_industries and top_reasons include all organizations (including your own). network_first_seen / network_last_seen reflect 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_id per organization. UPSERTing the same subject under a different external_person_id returns ingest_status: DUPLICATE_SAME_SUBJECT with the current owner's external_person_id. Revoke the original first if you need to reassign.
  • Match response fields:
    • matched_fields — the deciding token combination that determined the match_status level (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, and results_csv_base64)
  • 429: Too Many Requests - Rate limit exceeded (check Retry-After header)
  • 500: Internal Server Error - Server-side error (check logs)