Collect (Server-to-Server)
Two Verification Paths
Guardian SDK offers two ways to verify devices. Choose based on your architecture:
verify() | collect() | |
|---|---|---|
| Who calls Surt | The SDK (from the device) | Your backend (server-to-server) |
| Network calls from SDK | Yes | No |
| Your backend involved | No | Yes |
| Response | VerificationResult (allowed/denied) | CollectResult (encrypted payload) |
| Best for | Simple integration, frontend-driven decisions | Backend-driven decisions, custom logic, audit requirements |
verify() flow
App → SDK.verify() → Surt backend → risk decision → App
The SDK calls Surt directly and returns allowed: true/false. Your app acts on the decision immediately.
collect() flow
App → SDK.collect() → encrypted payload → App → Your backend → Surt /evaluate → Your backend → App
The SDK collects device data and encrypts it locally. Zero network calls to Surt. Your backend sends the payload to Surt's evaluate endpoint, receives the full risk assessment, and decides what to return to your app.
When to Use collect()
- Your backend needs the risk data before responding to the client
- You want to combine device risk with your own business logic server-side
- You need full control over what the client sees
- Compliance requires all third-party calls to originate from your infrastructure
Usage
1. Collect on the device
import { useGuardian } from '@surtai/guardian-rn';
function PaymentScreen() {
const { collect, setCustomer } = useGuardian();
const handlePayment = async () => {
setCustomer('user_123', 'John Doe', 'john@example.com');
const { payload } = await collect('withdrawal', 'User Payment');
// Send payload to YOUR backend
const response = await fetch('https://your-api.com/verify-device', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
payload,
amount: 500,
currency: 'USD',
}),
});
const result = await response.json();
// Your backend already made the risk decision
};
}
2. Call Surt from your backend
Your backend receives the encrypted payload from the app, then calls Surt's evaluate endpoint:
POST https://api.surt.com/geolocation/transactions/evaluate
Content-Type: application/json
Authorization: Bearer YOUR_SURT_API_KEY
{
"customer_id": "user_123",
"transaction_type": "withdrawal",
"transaction_name": "User Payment",
"payload": {
"type": "encrypted",
"data": "<payload from SDK>"
},
"config": {
"response": {
"address": { "type": "include" }
}
}
}
Request fields
| Field | Type | Required | Description |
|---|---|---|---|
customer_id | string | Yes | Your user identifier |
transaction_type | string | Yes | login, sign_up, deposit, or withdrawal |
transaction_name | string | No | Human-readable label |
payload.type | string | Yes | Always "encrypted" |
payload.data | string | Yes | The encrypted payload from collect() |
config.response.address | object | No | { "type": "include" } to get the full address, omitted by default |
Config
The config object controls what optional data is included in the response.
{
"config": {
"response": {
"address": { "type": "include" }
}
}
}
| Field | Values | Default | Description |
|---|---|---|---|
config.response.address.type | "include" or "omit" | "omit" | Whether to include the full reverse-geocoded address (street, city, state, country, postal code) in the response. Requires GPS data in the payload. |
When address is omitted (default), the address field in the response will be null even if location data was collected. Set to "include" if your backend needs the physical address for compliance, fraud review, or display.
Authentication
Use your Surt API key in the Authorization header as a Bearer token. This is a server-to-server call - the API key never touches the device.
3. Handle the response
The evaluate endpoint returns the same data as verify(), but with full detail. The address field depends on your config.
With config.response.address.type: "include"
{
"status_code": 200,
"message": "Transaction evaluated successfully",
"data": {
"transaction": {
"transaction_id": "1775514112-f2e3034b891e...",
"created_at": "2026-04-06T22:21:52Z",
"customer_id": "user_123",
"transaction_type": "withdrawal",
"transaction_name": "User Payment",
"status": {
"type": "completed",
"risk_level": "low",
"result": {
"status": "accepted",
"review": false
},
"address": {
"street": "123 Main St",
"city": "San Francisco",
"state": "California",
"country": "United States",
"postal_code": "94103",
"formatted_address": "123 Main St, San Francisco, CA 94103, USA"
},
"signals": [ ... ],
"triggered_scenarios": [ ... ]
},
"device": {
"device_id": "fingerprint_abc",
"manufacturer": "Apple",
"model": "iPhone 15",
"os_version": "18.0"
},
"metadata": {
"device_locations": [ ... ],
"ip_locations": [ ... ],
"device_ids": [ ... ]
},
"network_threat": {
"status": "not_analyzed"
},
"country": "United States",
"ip_address": "203.0.113.50"
}
}
}
With config.response.address.type: "omit" (default)
Same response, but address is null:
{
"status_code": 200,
"message": "Transaction evaluated successfully",
"data": {
"transaction": {
"transaction_id": "1775514112-f2e3034b891e...",
"created_at": "2026-04-06T22:21:52Z",
"customer_id": "user_123",
"transaction_type": "withdrawal",
"transaction_name": "User Payment",
"status": {
"type": "completed",
"risk_level": "low",
"result": {
"status": "accepted",
"review": false
},
"address": null,
"signals": [ ... ],
"triggered_scenarios": [ ... ]
},
"device": {
"device_id": "fingerprint_abc",
"manufacturer": "Apple",
"model": "iPhone 15",
"os_version": "18.0"
},
"metadata": {
"device_locations": [ ... ],
"ip_locations": [ ... ],
"device_ids": [ ... ]
},
"network_threat": {
"status": "not_analyzed"
},
"country": "United States",
"ip_address": "203.0.113.50"
}
}
}
Your backend can use status.risk_level, status.result.status, signals, triggered_scenarios, and address to make its own decision before responding to the client.
Location Override
Same as verify(), you can override location collection per call:
// Collect with location
const { payload } = await collect('login', 'Login', { collectLocation: true });
// Collect without location
const { payload } = await collect('login', 'Login', { collectLocation: false });
CollectResult
interface CollectResult {
/** Base64-encoded encrypted payload. Pass as payload.data in the evaluate request. */
payload: string;
}
The payload is encrypted and can only be decrypted by Surt's backend. It contains device fingerprint, attestation data, security signals, and optionally location.
Backend Example
- Node.js
- Java
- Python
app.post('/verify-device', async (req, res) => {
const { payload, userId } = req.body;
const surtResponse = await fetch(
'https://api.surt.com/geolocation/transactions/evaluate',
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${process.env.SURT_API_KEY}`,
},
body: JSON.stringify({
customer_id: userId,
transaction_type: 'login',
payload: { type: 'encrypted', data: payload },
config: { response: { address: { type: 'include' } } },
}),
}
);
const { data } = await surtResponse.json();
const risk = data.transaction.status.risk_level;
const accepted = data.transaction.status.result.status === 'accepted';
res.json({ allowed: accepted, risk });
});
@PostMapping("/verify-device")
public ResponseEntity<Map<String, Object>> verifyDevice(@RequestBody Map<String, Object> body) {
String payload = (String) body.get("payload");
String userId = (String) body.get("userId");
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.set("Authorization", "Bearer " + System.getenv("SURT_API_KEY"));
Map<String, Object> request = Map.of(
"customer_id", userId,
"transaction_type", "login",
"payload", Map.of("type", "encrypted", "data", payload),
"config", Map.of("response", Map.of("address", Map.of("type", "include")))
);
ResponseEntity<Map> response = restTemplate.exchange(
"https://api.surt.com/geolocation/transactions/evaluate",
HttpMethod.POST,
new HttpEntity<>(request, headers),
Map.class
);
Map data = (Map) response.getBody().get("data");
Map transaction = (Map) data.get("transaction");
Map status = (Map) transaction.get("status");
String risk = (String) status.get("risk_level");
Map result = (Map) status.get("result");
boolean accepted = "accepted".equals(result.get("status"));
return ResponseEntity.ok(Map.of("allowed", accepted, "risk", risk));
}
@app.post("/verify-device")
async def verify_device(request: Request):
body = await request.json()
surt_response = requests.post(
"https://api.surt.com/geolocation/transactions/evaluate",
headers={
"Content-Type": "application/json",
"Authorization": f"Bearer {os.environ['SURT_API_KEY']}",
},
json={
"customer_id": body["userId"],
"transaction_type": "login",
"payload": {"type": "encrypted", "data": body["payload"]},
"config": {"response": {"address": {"type": "include"}}},
},
)
data = surt_response.json()["data"]
risk = data["transaction"]["status"]["risk_level"]
accepted = data["transaction"]["status"]["result"]["status"] == "accepted"
return {"allowed": accepted, "risk": risk}