DigiSurvey Connect API
Connect your claim or policy administration system to your empanelled surveyors. Create claim assignments, follow their status, fetch survey reports with photos and documents, and get notified by signed webhooks. JSON over HTTPS; every request is scoped to your organisation.
https://vtlxdqgdrapeyhncbkrj.supabase.co/functions/v1/apiQuickstart
- Get your organisation activated. Request access, or sign in to the Stakeholder Portal and create your organisation. DigiSurvey verifies and activates it.
- Connect surveyors. Share your organisation's invite code with your empanelled surveyors (Portal → Surveyors).
- Create an API key. Portal → API keys → Create API key. Copy it — it is shown only once. Owners and admins can create keys.
- Call the API with
Authorization: Bearer dsk_live_…:
curl https://vtlxdqgdrapeyhncbkrj.supabase.co/functions/v1/api/me \
-H "Authorization: Bearer $DIGISURVEY_API_KEY"
const BASE = 'https://vtlxdqgdrapeyhncbkrj.supabase.co/functions/v1/api';
const res = await fetch(`${BASE}/me`, {
headers: { Authorization: `Bearer ${process.env.DIGISURVEY_API_KEY}` },
});
console.log(await res.json());
// { object: 'api_key', organisation: { id, name, type }, scopes: [...], rate_limit_per_minute: 120 }
import os, requests
BASE = "https://vtlxdqgdrapeyhncbkrj.supabase.co/functions/v1/api"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['DIGISURVEY_API_KEY']}"
print(s.get(f"{BASE}/me", timeout=30).json())
Authentication & scopes
Every request (except GET /) must send an organisation API key as a bearer token. Keys look like dsk_live_ followed by 48 hex characters. DigiSurvey stores only a SHA-256 hash of your key — if you lose it, revoke it and create a new one.
Each key carries one or more scopes. A request without the needed scope fails with 403 insufficient_scope.
| Scope | Allows |
|---|---|
claims:read | List and read claims, list connected surveyors |
claims:write | Create claims (single and bulk); assign, close, cancel, reopen |
reports:read | List and read submitted reports, get download links and HTML |
reports:write | Acknowledge reports or raise queries |
Errors
Errors use standard HTTP status codes and a consistent JSON body. Every response carries an X-Request-Id header — include it when you contact support.
{
"error": {
"code": "invalid_request",
"message": "survey_type must be spot, final or reinspection",
"details": { … } // sometimes, e.g. per-row errors
}
}| Status | code | Meaning |
|---|---|---|
| 400 | invalid_json | Body is not a JSON object |
| 401 | unauthorized, invalid_api_key | Missing, malformed, unknown or revoked key |
| 403 | insufficient_scope, organisation_inactive, forbidden | Key lacks the scope, organisation pending/suspended, or action not allowed |
| 404 | not_found, route_not_found | Object doesn't exist in your organisation, or unknown path |
| 409 | duplicate_external_ref, conflict | Claim with that external_ref exists, or claim number already open |
| 413 | payload_too_large | Body larger than 2 MB |
| 422 | invalid_request | Validation failed — see message / details |
| 429 | rate_limited | Too many requests — wait Retry-After seconds |
| 500 | internal_error | Our side — safe to retry with backoff |
Rate limits & pagination
Each API key may make 120 requests per minute. Responses include:
| Header | Meaning |
|---|---|
X-RateLimit-Limit | Requests allowed per minute (120) |
X-RateLimit-Remaining | Requests left in the current minute |
X-RateLimit-Reset | Unix time (seconds) when the window resets |
Retry-After | On 429 only — seconds to wait |
Use POST /claims/bulk instead of many single calls. List endpoints accept limit (1–200, default 50) and offset, and return has_more and next_offset:
{ "object": "list", "data": [ … ], "has_more": true, "next_offset": 50 }Idempotency
Send your own claim ID as external_ref (unique per organisation). Re-sending a claim with the same external_ref returns 409 duplicate_external_ref with the existing id in details — never a duplicate claim.
Alternatively send an Idempotency-Key header on POST /claims. It is stored as the claim's external_ref (if you didn't send one) and a retry with the same key returns the original claim with 200 and header Idempotent-Replay: true.
The claim object
A claim (assignment) is a survey request from your organisation to a connected surveyor.
| Field | Type | Description |
|---|---|---|
id | uuid | DigiSurvey claim id |
claim_number | string ≤80 | Your claim number. claim_number or vehicle_number is required. Only one open claim per number. |
vehicle_number | string ≤20 | Registration number — normalised to upper case without spaces (RJ14CD5678) |
policy_number | string ≤80 | Policy number |
insured_name, insured_phone | string | Insured / contact person |
insurer_name | string ≤160 | Insurance company (defaults to your organisation's name for insurers) |
garage_name, garage_address | string | Where the vehicle can be inspected |
loss_date | date | YYYY-MM-DD (also accepts DD-MM-YYYY / DD/MM/YYYY) |
loss_location | string ≤300 | Place of accident / loss |
estimate_amount | number | Repair estimate in ₹ (commas and ₹ are stripped) |
survey_type | enum | spot | final (default) | reinspection |
instructions | string ≤2000 | Notes for the surveyor |
external_ref | string ≤120 | Your system's ID — unique per organisation (see idempotency) |
surveyor_id / surveyor_email | uuid / string | Write only. Assign to a connected surveyor (see GET /surveyors). Omit to create unassigned. |
surveyor | object|null | Read only. { id, name, mobile } |
status | enum | new → accepted → in_progress → report_submitted → closed; or rejected (surveyor declined) / cancelled |
status_note | string | Latest note (e.g. the surveyor's reason for declining) |
survey_id | uuid|null | Set when the surveyor accepts and the survey is created |
created_via | enum | ui | csv | api |
created_at, updated_at, accepted_at, report_submitted_at, closed_at | timestamp | ISO-8601, UTC |
Create a claim
Creates one claim. If a surveyor is given, they get a push notification immediately. Unknown fields are rejected with 422 so typos don't go unnoticed. Returns 201 with the claim object.
curl -X POST $BASE/claims \
-H "Authorization: Bearer $DIGISURVEY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: CLAIMSYS-88213" \
-d '{
"claim_number": "MC-2026-0412",
"policy_number": "3001/12345678/00/000",
"vehicle_number": "RJ14 CD 5678",
"insured_name": "Ramesh Kumar",
"insured_phone": "+91 98290 00000",
"garage_name": "Shree Motors",
"garage_address": "Sikar Road, Jaipur",
"loss_date": "2026-09-20",
"loss_location": "NH-52, Chomu",
"estimate_amount": 52000,
"survey_type": "spot",
"instructions": "Insured available after 11 am",
"surveyor_email": "surveyor@example.com"
}'
const res = await fetch(`${BASE}/claims`, {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.DIGISURVEY_API_KEY}`,
'Content-Type': 'application/json',
'Idempotency-Key': claim.id, // your own claim id
},
body: JSON.stringify({
claim_number: claim.number,
vehicle_number: claim.vehicleNo,
insured_name: claim.insured,
loss_date: claim.lossDate, // 'YYYY-MM-DD'
survey_type: 'spot',
surveyor_email: claim.surveyorEmail,
}),
});
if (!res.ok) throw new Error((await res.json()).error.message);
const created = await res.json(); // created.id, created.status === 'new'
r = s.post(f"{BASE}/claims", json={
"claim_number": "MC-2026-0412",
"vehicle_number": "RJ14CD5678",
"insured_name": "Ramesh Kumar",
"loss_date": "2026-09-20",
"survey_type": "spot",
"surveyor_email": "surveyor@example.com",
"external_ref": "CLAIMSYS-88213",
}, timeout=30)
if r.status_code == 409:
claim_id = r.json()["error"]["details"]["id"] # already sent earlier
else:
r.raise_for_status()
claim_id = r.json()["id"]
{
"object": "claim",
"id": "524f9bf0-b5e5-4074-92f0-a15cad984bdf",
"claim_number": "MC-2026-0412",
"vehicle_number": "RJ14CD5678",
"insurer_name": "Your Insurance Co",
"loss_date": "2026-09-20",
"estimate_amount": 52000,
"survey_type": "spot",
"status": "new",
"survey_id": null,
"created_via": "api",
"external_ref": "CLAIMSYS-88213",
"surveyor": { "id": "20f1…", "name": "A. Sharma", "mobile": "98xxxxxx00" },
"created_at": "2026-09-26T03:02:32.923Z", …
}Create claims in bulk
Send { "claims": [ … ] } with 1–500 claim objects (same fields as POST /claims). Each row is validated on its own: valid rows are created, invalid rows are reported with their index (0-based) — so fix and resend only the failures. Returns 201 if at least one claim was created, otherwise 422. Each surveyor gets one grouped notification.
curl -X POST $BASE/claims/bulk \
-H "Authorization: Bearer $DIGISURVEY_API_KEY" -H "Content-Type: application/json" \
-d '{ "claims": [
{ "claim_number": "MC-1001", "vehicle_number": "MH12AB1234", "surveyor_email": "a@example.com", "external_ref": "X-1001" },
{ "policy_number": "only-a-policy" },
{ "claim_number": "MC-1003", "loss_date": "31/02/2026" }
] }'
{
"object": "bulk_result",
"created_count": 1,
"error_count": 2,
"created": [ { "index": 0, "id": "761cf636-…" } ],
"errors": [
{ "index": 1, "code": "invalid", "message": "claim_number or vehicle_number is required" },
{ "index": 2, "code": "invalid", "message": "date/time field value out of range: \"31-02-2026\"" }
]
}
422 and a list in details.errors — nothing is created, so a column mistake never creates half a batch. Duplicate external_ref rows come back with "code": "duplicate".List claims
Newest first. Filters (all optional, combinable):
| Query | Example | Notes |
|---|---|---|
status | status=new,accepted | Comma-separated statuses |
updated_since | 2026-09-25T00:00:00Z | Polling for changes — use the latest updated_at you saw |
claim_number, vehicle_number, policy_number, external_ref | external_ref=CLAIMSYS-88213 | Exact match |
limit, offset | limit=100&offset=100 | See pagination |
curl "$BASE/claims?status=report_submitted&updated_since=2026-09-25T00:00:00Z&limit=100" \
-H "Authorization: Bearer $DIGISURVEY_API_KEY"
def all_claims(**filters):
offset = 0
while True:
page = s.get(f"{BASE}/claims", params={**filters, "limit": 200, "offset": offset}, timeout=30).json()
yield from page["data"]
if not page["has_more"]: break
offset = page["next_offset"]
Retrieve a claim
Returns the claim object, or 404 if it doesn't exist in your organisation.
Assign, close, cancel or reopen
Body { "surveyor_id": "…" } or { "surveyor_email": "…" }, optional "note". Only for claims in new (unassigned) or rejected status; the surveyor must be connected to your organisation.
Optional body { "note": "…" }. close marks the claim finished (usually after acknowledging the report); cancel withdraws it and notifies the surveyor; reopen brings back a closed/cancelled claim. Each returns the updated claim and fires assignment.updated.
curl -X POST $BASE/claims/524f9bf0-b5e5-4074-92f0-a15cad984bdf/assign \
-H "Authorization: Bearer $DIGISURVEY_API_KEY" -H "Content-Type: application/json" \
-d '{ "surveyor_email": "surveyor@example.com", "note": "Urgent — cashless" }'List connected surveyors
Surveyors actively connected to your organisation — use their id or email when assigning.
{ "object": "list", "has_more": false, "data": [
{ "object": "surveyor", "id": "20f1…", "name": "A. Sharma", "email": "a@example.com",
"mobile": "98xxxxxx00", "license_number": "SLA-12345", "iiisla_number": "…", "connected_since": "2026-09-01T…" } ] }The report object
A report submission is created when a surveyor sends one or more reports for a survey to your organisation (they can send many surveys at once). If the survey came from one of your claims, claim_id is set and the claim moves to report_submitted.
| Field | Type | Description |
|---|---|---|
id | uuid | Submission id |
claim_id | uuid|null | Your claim, when the survey was created from an assignment |
survey_id, surveyor_id | uuid | Source survey and surveyor |
report_types | string[] | e.g. spot, final, reinspection_v2, bill_final, summary |
summary | object | Snapshot at sending time: vehicle_number, claim_number, policy_number, insured_name, insurance_company, survey_type, report_number, assessed_amount, estimate_amount, make_model, date_of_loss, place_of_survey, job_number |
files | object[] | Attached PDFs: { name, report_type, size, content_type } (+ download_url on the detail endpoint) |
message | string | Covering note from the surveyor |
status | enum | submitted → viewed → acknowledged, or query_raised (back to submitted when the surveyor replies) |
thread | object[] | Acknowledgements, queries and replies: { side: "org"|"api"|"surveyor", text, at, kind } |
created_at, viewed_at, acknowledged_at | timestamp | ISO-8601 |
List reports
Newest first. Filters: status (comma-separated), since (ISO date-time, by created_at), claim_id, plus limit/offset. A typical integration polls GET /reports?status=submitted every few minutes — or uses webhooks.
Retrieve a report with download links
Returns the report object plus the surveyor, the linked claim, and time-limited download links (valid for 1 hour — fetch the report again for fresh links) for the attached PDFs, every photo and every document of the survey. Reading a submitted report marks it viewed. Add ?include=none to skip photos/documents, or ?include=html to also embed each report's HTML body (reports[].html, images rewritten to 1-hour download links) — combine as ?include=html,none.
{
"object": "report", "id": "0896304a-…", "status": "viewed",
"claim": { "id": "524f9bf0-…", "claim_number": "MC-2026-0412", "external_ref": "CLAIMSYS-88213", "status": "report_submitted" },
"surveyor": { "name": "A. Sharma", "license_number": "SLA-12345", … },
"summary": { "vehicle_number": "RJ14CD5678", "assessed_amount": 41250, … },
"documents_expire_at": "2026-09-26T04:03:07Z",
"files": [ { "name": "Final_RJ14CD5678.pdf", "report_type": "final", "size": 812345,
"download_url": "https://digitaliiisla.co.in/.netlify/functions/r2?action=ticket&t=9ffb…" } ],
"reports": [ { "report_type": "final", "updated_at": "…", "changed_since_sent": false,
"html_url": "$BASE/reports/0896304a-…/html?type=final" } ],
"photos": [ { "stage": "primary", "label": "Front bumper", "captured_at": "…", "latitude": 26.91, "longitude": 75.78, "download_url": "…" } ],
"documents": [ { "doc_type": "rc", "uploaded_at": "…", "download_url": "…" } ]
}download_url answers with a 302 redirect to the file — no API key needed, so you can hand it to a downloader. download_url is null if a file is not available. changed_since_sent: true means the surveyor edited the report after sending it.
Get the report as HTML
Returns the saved report page (text/html) with letterhead, tables and text, ready to archive or print to PDF. Images are rewritten to download links valid for 24 hours. The attached PDF in files is the surveyor's official signed copy; prefer it when present. type must be one of the submission's report_types.
Acknowledge a report or raise a query
Optional body { "message": "Received — thank you" }. Sets status to acknowledged and notifies the surveyor.
Body { "message": "Please share RC back side" } (required). Sets query_raised; the surveyor is notified and their reply is added to thread.
curl -X POST $BASE/reports/0896304a-6ec9-4f3c-9ec4-fac4da66a950/ack \
-H "Authorization: Bearer $DIGISURVEY_API_KEY" -H "Content-Type: application/json" \
-d '{ "message": "Received, thanks" }'Webhooks
Add an HTTPS endpoint in Portal → Webhooks, choose events and copy the signing secret (whsec_…). DigiSurvey sends a POST with a JSON event when something happens:
| Event | When | data |
|---|---|---|
report.submitted | A surveyor sent a report to you | { report: { id, survey_summary, report_types, files, claim_id, created_at } } |
assignment.updated | A claim was accepted, declined, started, linked, reported, assigned, closed, cancelled or reopened | { claim: <claim object>, action } |
claim.created | Claims were created (portal, Excel or API) | { count, claims: [{ id, claim_number, vehicle_number, surveyor_id, external_ref }] } |
ping | You pressed Send test in the portal | { message } |
POST /your/webhook HTTP/1.1
Content-Type: application/json
User-Agent: DigiSurvey-Webhooks/1.0
X-DigiSurvey-Event: report.submitted
X-DigiSurvey-Delivery: 5b0d3a1e-8f7c-4c2a-9a55-0c3f6f0b7a11
X-DigiSurvey-Timestamp: 1790391777
X-DigiSurvey-Signature: v1=6f1c0a…e93b
{
"id": "5b0d3a1e-8f7c-4c2a-9a55-0c3f6f0b7a11",
"object": "event",
"type": "report.submitted",
"api_version": "2026-09-26",
"created_at": "2026-09-26T03:02:56Z",
"data": { "report": { "id": "0896304a-…", "claim_id": "524f9bf0-…", "report_types": ["final"], … } }
}- Reply with any 2xx within 8 seconds. Do heavy work asynchronously — e.g. queue the event, then call
GET /reports/{id}. - Delivery is best-effort: a failed attempt (network error or 5xx) is retried once. Use
GET /reports?status=submittedorGET /claims?updated_since=…as a safety net. - Events can arrive more than once or out of order — de-duplicate on
id. - After 25 consecutive failures the endpoint is switched off; fix it and re-enable it in the portal. The portal shows the latest deliveries with status codes.
Verifying signatures
Compute HMAC-SHA256(secret, timestamp + "." + raw_body) as hex and compare it with the value after v1= in X-DigiSurvey-Signature using a constant-time comparison. Reject events whose timestamp is more than 5 minutes old. Always use the raw request body — not re-serialised JSON.
import crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.DIGISURVEY_WEBHOOK_SECRET; // whsec_…
app.post('/webhooks/digisurvey', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-DigiSurvey-Timestamp');
const sig = (req.get('X-DigiSurvey-Signature') || '').replace(/^v1=/, '');
const expected = crypto.createHmac('sha256', SECRET).update(`${ts}.${req.body}`).digest('hex');
const fresh = Math.abs(Date.now() / 1000 - Number(ts)) < 300;
if (!fresh || sig.length !== expected.length ||
!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.sendStatus(400);
}
const event = JSON.parse(req.body);
queue.add(event); // handle asynchronously, de-duplicate on event.id
res.sendStatus(200);
});
import hmac, hashlib, time, os
from flask import Flask, request, abort
app = Flask(__name__)
SECRET = os.environ["DIGISURVEY_WEBHOOK_SECRET"].encode()
@app.post("/webhooks/digisurvey")
def digisurvey():
ts = request.headers.get("X-DigiSurvey-Timestamp", "")
sig = request.headers.get("X-DigiSurvey-Signature", "").removeprefix("v1=")
raw = request.get_data() # raw bytes
expected = hmac.new(SECRET, ts.encode() + b"." + raw, hashlib.sha256).hexdigest()
if not ts.isdigit() or abs(time.time() - int(ts)) > 300 or not hmac.compare_digest(sig, expected):
abort(400)
event = request.get_json()
enqueue(event) # de-duplicate on event["id"]
return "", 200
$secret = getenv('DIGISURVEY_WEBHOOK_SECRET');
$raw = file_get_contents('php://input');
$ts = $_SERVER['HTTP_X_DIGISURVEY_TIMESTAMP'] ?? '';
$sig = preg_replace('/^v1=/', '', $_SERVER['HTTP_X_DIGISURVEY_SIGNATURE'] ?? '');
$expected = hash_hmac('sha256', $ts . '.' . $raw, $secret);
if (!ctype_digit($ts) || abs(time() - (int)$ts) > 300 || !hash_equals($expected, $sig)) {
http_response_code(400); exit;
}
$event = json_decode($raw, true);
http_response_code(200);
End-to-end examples
Push new claims every hour, collect reports
import os, requests, pathlib
BASE = "https://vtlxdqgdrapeyhncbkrj.supabase.co/functions/v1/api"
s = requests.Session()
s.headers["Authorization"] = f"Bearer {os.environ['DIGISURVEY_API_KEY']}"
# 1) send today's claims from your system (max 500 per call)
claims = [{"claim_number": c.no, "vehicle_number": c.reg, "insured_name": c.insured,
"survey_type": "spot", "surveyor_email": c.surveyor, "external_ref": c.id} for c in new_claims()]
for i in range(0, len(claims), 500):
r = s.post(f"{BASE}/claims/bulk", json={"claims": claims[i:i+500]}, timeout=60)
for e in r.json().get("errors", []):
if e["code"] != "duplicate":
log_error(claims[i + e["index"]], e["message"])
# 2) download new reports and acknowledge them
for rep in s.get(f"{BASE}/reports", params={"status": "submitted", "limit": 50}, timeout=30).json()["data"]:
full = s.get(f"{BASE}/reports/{rep['id']}", timeout=30).json()
folder = pathlib.Path("reports") / (full["summary"].get("claim_number") or rep["id"])
folder.mkdir(parents=True, exist_ok=True)
for f in full["files"]:
if f["download_url"]:
(folder / f["name"]).write_bytes(requests.get(f["download_url"], timeout=120).content)
s.post(f"{BASE}/reports/{rep['id']}/ack", json={"message": "Auto-received by ClaimSys"}, timeout=30)
import { writeFile, mkdir } from 'node:fs/promises';
const BASE = 'https://vtlxdqgdrapeyhncbkrj.supabase.co/functions/v1/api';
const H = { Authorization: `Bearer ${process.env.DIGISURVEY_API_KEY}`, 'Content-Type': 'application/json' };
const api = async (path, init = {}) => {
const r = await fetch(BASE + path, { ...init, headers: H });
if (r.status === 429) { await new Promise(ok => setTimeout(ok, Number(r.headers.get('retry-after')) * 1000)); return api(path, init); }
return r.json();
};
const result = await api('/claims/bulk', { method: 'POST', body: JSON.stringify({ claims: todaysClaims }) });
console.log(`${result.created_count} created, ${result.error_count} errors`);
const { data: inbox } = await api('/reports?status=submitted');
for (const rep of inbox) {
const full = await api(`/reports/${rep.id}`);
const dir = `reports/${full.summary.claim_number ?? rep.id}`;
await mkdir(dir, { recursive: true });
for (const f of full.files) {
if (f.download_url) await writeFile(`${dir}/${f.name}`, Buffer.from(await (await fetch(f.download_url)).arrayBuffer()));
}
await api(`/reports/${rep.id}/ack`, { method: 'POST', body: JSON.stringify({ message: 'Received' }) });
}
Support & changelog
Questions or a sandbox organisation for testing? Email help@digitaliiisla.co.in or WhatsApp +91 83026 98307. Include the X-Request-Id of the failing call.
| Version | Changes |
|---|---|
2026-09-26 | First release: claims (single, bulk, actions), surveyors, reports (detail, HTML, download links, ack/query), webhooks (report.submitted, assignment.updated, claim.created). |
We add fields without notice (ignore unknown fields); anything that would break integrations gets a new version date and advance notice to API key owners.