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) |
| JWT requirement | Required, fresh per call | Optional (only for device IP) |
| Network calls from SDK | Yes | Only GET /geolocation/client-ip when a JWT is supplied |
| 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 → fetch fresh JWT from your backend → SDK.verify(jwt) → Surt backend → risk decision → App
The SDK calls Surt directly and returns allowed: true/false. Your app acts on the decision immediately. A fresh JWT is required for every call.
collect() flow
App → SDK.collect() → encrypted payload → App → Your backend → Surt /evaluate → Your backend → App
The SDK collects device data and encrypts it locally. With no arguments, collect() makes 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 } = useGuardian();
const handlePayment = async () => {
// JWT is optional for collect(). Call collect() with no arguments,
// or pass collect(jwt) only if you want the device's public IP
// resolved into the payload.
const { payload } = await collect();
// 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
};
}
For collect(), pass a JWT to collect(jwt) only if you want the device's public IP resolved into the payload; otherwise call collect() with no arguments. Unlike verify(), a collect() JWT may be reused or omitted - collect() generates its own attestation nonce.
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 (and resolve device IP via the JWT)
const { payload } = await collect(jwt, { collectLocation: true });
// Collect without location and without a JWT (no Surt network call)
const { payload } = await collect(undefined, { 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}