Sety
    Sety
    • Developer API Reference
    • Health
      • Health check
        GET
    • Incidents
      • Create incident
        POST
      • List incidents
        GET
      • Get incident
        GET
      • Cancel incident
        POST
      • Add note
        POST
    • Access Logs
      • List access logs
        GET
      • Access log stats
        GET
      • Create access log
        POST
      • Get current visitors
        GET
      • Checkout visitor
        PATCH
    • Threat Intelligence
      • List danger zones
        GET
      • Danger analytics for a location
        GET
      • Heatmap data
        GET
      • Safety score
        GET
      • Route safety analysis
        GET
    • Schemas
      • Error
      • PaginatedResponse
      • GeoPoint
      • Subject
      • Location
      • IncidentLocation
      • SLA
      • IncidentTimelineEvent
      • ResponderAssignment
      • TimelineEvent
      • Incident
      • CreateIncidentRequest
      • IncidentAnalytics
      • AccessCode
      • CreateAccessCodeRequest
      • ValidationResult
      • AccessLog
      • CurrentVisitor
      • DangerAnalytics
      • HeatmapResponse
      • SafetyScore
      • TimeAnalysis
      • LocationComparison
      • RouteSafety
      • RiskPrediction
      • IncidentPatterns
      • SafeZones
      • ApiKey
      • CreateApiKeyRequest
      • WebhookEndpoint
      • WebhookDelivery

    Developer API Reference

    Sety#

    Version: v1
    Base URL: https://intel-api.sety.io/v1/external

    Table of Contents#

    1.
    Overview
    2.
    Getting Started
    3.
    Authentication
    4.
    Rate Limiting
    5.
    Errors
    6.
    Incidents
    7.
    Access Codes
    8.
    Access Logs
    9.
    Threat Intelligence
    10.
    Webhooks
    11.
    Scopes Reference

    Overview#

    The Sety API lets your platform create emergency incidents, manage visitor access, and query real-time threat intelligence across Nigeria and Sub-Saharan Africa.

    How the incident model works#

    Your platform is the partner. You report incidents on behalf of your users. Sety's responder network handles everything from there — you never manage responders directly.
    Your platform          Sety Intel
    ─────────────          ──────────
    POST /incidents   →    Receives incident
                      →    Dispatches nearest responder
                      ←    Webhooks: status updates
    GET  /incidents/:id ←  Poll anytime for current status
    POST /incidents/:id/cancel  (if false alarm)
    Your user's information is passed as a subject — they don't need a Sety account. Pass whatever PII your platform already holds: name, phone, your internal reference ID, and any freeform metadata.

    Incident categories#

    CategoryUse for
    sosPanic button, personal danger, user in distress
    vehicleHijack, road robbery, accident
    location_threatRobbery or violence at a specific place
    medicalInjury, health emergency

    Environments#

    Key prefixEnvironmentBehavior
    sety_live_ProductionReal dispatch, live responders
    sety_test_SandboxNo real dispatch, safe for testing
    Always develop against the sandbox. Switch to live keys only when you're ready to go live.

    Getting Started#

    1. Get your API keys#

    Log in to the Sety Intel Dashboard and go to Settings → API Keys. Create a key with the scopes your integration needs.

    2. Make your first request#

    {
      "status": "ok",
      "api_version": "v1",
      "timestamp": "2026-03-10T14:22:00.000Z"
    }

    3. Create a test incident#

    4. Set up webhooks#

    In the dashboard under Settings → Webhooks, add your endpoint URL and subscribe to incident.* events. We'll POST to you every time an incident status changes.

    Authentication#

    Every request must include your API key as a Bearer token:
    Authorization: Bearer sety_live_a3f9c2d18e4b7a6509c1d2e3f4a5b6c7

    Key security#

    Never expose API keys in client-side code or mobile apps
    Use your test key (sety_test_) during development and CI
    Rotate a key immediately if it's compromised — create a new one from the dashboard
    Restrict keys to only the scopes your integration actually uses

    Rate Limiting#

    Limits are enforced per API key on a sliding 1-minute window.
    PlanRequests / minute
    Starter60
    Growth300
    Enterprise1,000
    Every response includes:
    X-RateLimit-Limit: 300
    X-RateLimit-Remaining: 287
    X-RateLimit-Reset: 2026-03-10T14:31:00.000Z
    When you exceed the limit, you'll receive 429 Too Many Requests with a Retry-After header. Back off and retry after that duration.

    Errors#

    All errors follow the same shape:
    {
      "code": 404,
      "message": "Incident not found"
    }
    CodeMeaning
    400Invalid request — message describes which field failed
    401Missing or invalid API key
    402Plan does not include API access, or subscription suspended
    403Key lacks required scope
    404Resource not found or belongs to a different organization
    409State conflict — e.g. cancelling an already-resolved incident
    429Rate limit exceeded
    500Unexpected server error — contact support

    Incidents#

    The incident lifecycle#

    Once you create an incident, Sety owns the status. Your platform observes via polling or webhooks — you never set status directly.
    created → dispatching → assigned → responding → on_scene → resolved
                  │
                  └──── escalated ──── unresponsive
    Any active status can transition to cancelled if you call the cancel endpoint.
    StatusWhat it means
    createdReceived — dispatch starting
    dispatchingBroadcasting to Sety responder network
    assignedA responder has accepted
    respondingResponder confirmed en route
    on_sceneResponder has arrived
    resolvedIncident closed
    cancelledCancelled by your platform or Sety
    escalatedNo responder accepted within the dispatch window
    unresponsiveEscalated — still no response after the escalation window

    Create incident#

    POST /api/v1/incidents
    Report an emergency on behalf of one of your users. Sety immediately dispatches the nearest available responder.
    Required scope: incidents:write
    Request body
    FieldTypeRequiredDescription
    categorystring✅sos, vehicle, location_threat, or medical
    severitystring✅low, medium, high, or critical
    subjectobject✅Your customer — see Subject object below
    locationobject✅Where the incident is happening
    titlestringShort title. Auto-generated if omitted
    descriptionstringAdditional context. Max 2,000 chars
    idempotency_keystringUnique key for safe retries. Max 128 chars
    is_drillbooleanDefault false. Drills don't trigger real dispatch
    Subject object
    FieldTypeRequiredDescription
    namestring✅Your customer's full name
    phonestringPhone number
    emailstringEmail address
    referencestringYour internal ID — trip ID, user ID, waybill, etc. Echoed back in all responses and webhook payloads
    metadataobjectUp to 20 key-value string pairs of freeform context
    Location object
    FieldTypeRequiredDescription
    coordinatesnumber[2]✅[longitude, latitude] — GeoJSON order
    addressstringHuman-readable address
    landmarkstringNearby landmark to help the responder
    ⚠️ Coordinates are [longitude, latitude] — this is GeoJSON convention, which is the reverse of the common lat/lng order.
    Idempotency
    Pass idempotency_key to safely retry a request without creating duplicate incidents. If a key matches an existing incident for your organization, the existing incident is returned with HTTP 200 and "idempotent": true. Use a value that's naturally unique per incident attempt — a trip ID, session ID, or UUID you generate.
    Example — SOS panic button
    {
      "category": "sos",
      "severity": "critical",
      "idempotency_key": "session_abc123",
      "subject": {
        "name": "Amaka Obi",
        "phone": "+2348012345678",
        "reference": "USER-9921"
      },
      "location": {
        "coordinates": [3.3792, 6.5244],
        "address": "14 Bourdillon Road, Ikoyi, Lagos"
      }
    }
    Example — Vehicle hijack (ride-hailing)
    {
      "category": "vehicle",
      "severity": "critical",
      "idempotency_key": "TRIP-44821",
      "subject": {
        "name": "Emeka Adeyemi",
        "phone": "+2348099887766",
        "reference": "TRIP-44821",
        "metadata": {
          "vehicle_plate": "LND-204-KJA",
          "driver_name": "Olu Babs",
          "pickup_area": "Lekki Phase 1"
        }
      },
      "location": {
        "coordinates": [3.3512, 6.4698],
        "landmark": "Eko Bridge, Lagos"
      },
      "description": "Driver reports passengers attempted armed robbery"
    }
    Example — Location threat (fintech / banking)
    {
      "category": "location_threat",
      "severity": "high",
      "subject": {
        "name": "Branch Security",
        "reference": "BRANCH-VI-01"
      },
      "location": {
        "coordinates": [3.4219, 6.4281],
        "address": "1 Ozumba Mbadiwe Ave, Victoria Island",
        "landmark": "GTBank branch ATM vestibule"
      },
      "description": "Armed men demanding cash from customers at ATM"
    }
    Response 201
    {
      "incident": {
        "incident_id": "inc_a1b2c3d4e5f6g7h8",
        "category": "vehicle",
        "severity": "critical",
        "status": "dispatching",
        "subject": {
          "name": "Emeka Adeyemi",
          "phone": "+2348099887766",
          "reference": "TRIP-44821",
          "metadata": {
            "vehicle_plate": "LND-204-KJA"
          }
        },
        "location": {
          "coordinates": [3.3512, 6.4698],
          "landmark": "Eko Bridge, Lagos"
        },
        "title": "[CRITICAL] Vehicle Emergency",
        "description": "Driver reports passengers attempted armed robbery",
        "is_drill": false,
        "timeline": [
          {
            "status": "created",
            "event": "created",
            "message": "Incident received",
            "timestamp": "2026-03-10T14:22:00.000Z"
          },
          {
            "status": "dispatching",
            "event": "dispatched",
            "message": "Dispatching to Sety responder network",
            "timestamp": "2026-03-10T14:22:00.120Z"
          }
        ],
        "resolution_outcome": null,
        "created_at": "2026-03-10T14:22:00.000Z",
        "updated_at": "2026-03-10T14:22:00.120Z",
        "resolved_at": null,
        "cancelled_at": null
      }
    }
    Response 200 — idempotent (key already used)
    {
      "incident": { ... },
      "idempotent": true
    }

    List incidents#

    GET /api/v1/incidents
    Returns all incidents created by your organization, newest first.
    Required scope: incidents:read
    Query parameters
    ParameterTypeDescription
    statusstringFilter by status
    categorystringFilter by category
    severitystringFilter by severity
    from_datestringISO 8601
    to_datestringISO 8601
    pageintegerDefault 1
    limitintegerDefault 20, max 100
    Response 200
    {
      "results": [ /* array of incident objects */ ],
      "page": 1,
      "limit": 20,
      "totalPages": 3,
      "totalResults": 47
    }

    Get incident#

    GET /api/v1/incidents/:incidentId
    Fetch the current state of a single incident. Use this to poll for status updates if you're not using webhooks.
    Required scope: incidents:read
    Response 200 — incident object (same shape as create response)

    Cancel incident#

    POST /api/v1/incidents/:incidentId/cancel
    Cancel an active incident — for example when your user confirms they are safe, or the alert was a false alarm. Only incidents with an active status can be cancelled.
    Required scope: incidents:write
    Request body
    FieldTypeDescription
    reasonstringOptional. Recorded in the incident timeline
    Example
    {
      "reason": "User confirmed they are safe — false alarm"
    }
    Response 200 — updated incident with status: "cancelled"
    Response 409 — incident already closed
    {
      "code": 409,
      "message": "Incident cannot be cancelled — current status: resolved"
    }

    Add note#

    POST /api/v1/incidents/:incidentId/notes
    Append additional context to an active incident. Use this when new information becomes available after creation — an updated location, new details from your user, or a changed situation.
    Required scope: incidents:write
    Request body
    FieldTypeRequiredDescription
    messagestring✅Note content. Max 2,000 chars
    Example
    {
      "message": "User called back — now at Lekki Phase 1 roundabout, vehicle is a white Toyota Camry"
    }
    Response 200 — updated incident with note appended to timeline

    Access Codes#

    Generate time-limited codes for visitors and validate them at entry points. Integrate with your reception system, gate controller, or check-in flow.

    Generate access code#

    POST /api/v1/access-codes
    Required scope: access_codes:write
    Request body
    FieldTypeRequiredDescription
    visitor_namestring✅
    visitor_phonestring✅
    visitor_emailstring
    purposestringReason for visit
    entry_pointstringGate or entrance identifier
    host_user_idstringStaff member hosting the visitor
    valid_fromstringISO 8601. Defaults to now
    valid_untilstringISO 8601 expiry
    max_usesintegerDefault 1
    Example
    {
      "visitor_name": "Adaeze Okonkwo",
      "visitor_phone": "+2348012345678",
      "visitor_email": "[email protected]",
      "purpose": "Board meeting",
      "entry_point": "Main Gate",
      "valid_from": "2026-03-11T09:00:00.000Z",
      "valid_until": "2026-03-11T18:00:00.000Z",
      "max_uses": 1
    }
    Response 201
    {
      "access_code": {
        "code_id": "code_abc123...",
        "code": "SETY-7X4K",
        "visitor_name": "Adaeze Okonkwo",
        "visitor_phone": "+2348012345678",
        "status": "active",
        "valid_from": "2026-03-11T09:00:00.000Z",
        "valid_until": "2026-03-11T18:00:00.000Z",
        "max_uses": 1,
        "use_count": 0,
        "created_at": "2026-03-10T14:00:00.000Z"
      }
    }

    Bulk generate#

    POST /api/v1/access-codes/bulk
    Generate codes for up to 50 visitors in one request. All codes share the same purpose, valid_from, and valid_until.
    Required scope: access_codes:write
    Request body
    FieldTypeRequiredDescription
    visitorsobject[]✅Array of visitor objects (max 50). Each needs visitor_name and visitor_phone
    purposestringShared purpose for all
    valid_fromstring
    valid_untilstring

    Validate access code#

    POST /api/v1/access-codes/validate
    Validate and consume a visitor code at a gate or entry point. Check the valid field — an invalid code returns 200 with valid: false, not a 4xx error.
    Required scope: access_codes:validate
    Request body
    FieldTypeRequiredDescription
    codestring✅The code to validate (e.g. SETY-7X4K)
    validated_bystringName or ID of the officer validating
    Response 200 — valid
    {
      "valid": true,
      "code_id": "code_abc123...",
      "visitor": {
        "name": "Adaeze Okonkwo",
        "phone": "+2348012345678"
      },
      "purpose": "Board meeting",
      "uses_remaining": 0,
      "valid_until": "2026-03-11T18:00:00.000Z"
    }
    Response 200 — invalid
    {
      "valid": false,
      "reason": "Code has expired"
    }
    Possible reason values: Code not found · Code has expired · Code has been revoked · Maximum uses reached · Code not yet active

    List access codes#

    GET /api/v1/access-codes
    Required scope: access_codes:read
    Query parameters: status, visitor_phone, search, from_date, to_date, page, limit

    Get access code#

    GET /api/v1/access-codes/:codeId
    Required scope: access_codes:read

    Revoke access code#

    DELETE /api/v1/access-codes/:codeId
    Immediately revokes a code. Any subsequent validation will fail with Code has been revoked.
    Required scope: access_codes:write
    Request body
    FieldTypeDescription
    reasonstringOptional

    Access code stats#

    GET /api/v1/access-codes/stats
    Required scope: access_codes:read
    Query parameters: from_date, to_date
    Response 200
    {
      "total_generated": 320,
      "total_used": 281,
      "total_expired": 24,
      "total_revoked": 3,
      "total_active": 12
    }

    Access Logs#

    A complete record of who entered and left your premises.

    Create access log#

    POST /api/v1/access-logs
    Manually record a check-in without a code — for walk-ins, deliveries, or when the code system is bypassed.
    Required scope: access_codes:write
    Request body
    FieldTypeRequiredDescription
    visitor_namestring✅
    visitor_phonestring✅
    entry_pointstring
    host_user_idstring
    notesstring

    List access logs#

    GET /api/v1/access-logs
    Required scope: logs:read
    Query parameters: status (checked_in or checked_out), visitor_phone, entry_point, from_date, to_date, search, page, limit

    Get current visitors#

    GET /api/v1/access-logs/current-visitors
    All visitors currently on-premises — checked in but not yet checked out.
    Required scope: logs:read
    Response 200
    {
      "visitors": [
        {
          "log_id": "log_abc...",
          "visitor_name": "Adaeze Okonkwo",
          "visitor_phone": "+2348012345678",
          "entry_point": "Main Gate",
          "checked_in_at": "2026-03-10T09:15:00.000Z",
          "duration_minutes": 187
        }
      ],
      "count": 1
    }

    Checkout visitor#

    PATCH /api/v1/access-logs/:logId/checkout
    Record a visitor's departure.
    Required scope: access_codes:write
    Request body
    FieldTypeDescription
    checkout_notesstringOptional

    Access log stats#

    GET /api/v1/access-logs/stats
    Required scope: logs:read
    Query parameters: from_date, to_date

    Threat Intelligence#

    Query Sety's threat database — sourced from GDELT, community reports, and verified security incidents across Sub-Saharan Africa.

    Safety score#

    GET /api/v1/safety/score
    Real-time safety score (0–100) for a location based on recent incident history. Use this before dispatching a driver, approving a delivery zone, or surfacing a risk warning to your users.
    Required scope: analytics:read
    Query parameters
    ParameterTypeRequiredDescription
    latitudenumber✅
    longitudenumber✅
    radius_kmnumberAnalysis radius. Default 2
    Response 200
    {
      "location": { "latitude": 6.4281, "longitude": 3.4219 },
      "score": 72,
      "level": "moderate",
      "color": "yellow",
      "breakdown": {
        "recent_incidents_7d": 1,
        "recent_incidents_30d": 4,
        "trend": "stable"
      },
      "timestamp": "2026-03-10T14:22:00.000Z"
    }
    Score levels
    ScoreLevelColorMeaning
    70–100safegreenLow incident history
    50–69moderateyellowSome activity — monitor
    30–49high_riskorangeActive threat area
    0–29criticalredAvoid if possible

    Route safety analysis#

    GET /api/v1/safety/route
    Evaluates safety conditions along a route between two points by sampling checkpoints. Returns a composite score and flags dangerous segments. Use this to warn users before a trip or suggest alternatives.
    Required scope: analytics:read
    Query parameters
    ParameterTypeRequiredDescription
    from_latnumber✅Origin latitude
    from_lngnumber✅Origin longitude
    to_latnumber✅Destination latitude
    to_lngnumber✅Destination longitude
    checkpointsintegerSampling points along route. Default 10, max 20
    Response 200
    {
      "route_safety_score": 68,
      "lowest_safety_score": 41,
      "checkpoints": [
        { "checkpoint": 1, "location": { "lat": 6.52, "lng": 3.37 }, "safety_score": 81, "level": "safe" },
        { "checkpoint": 6, "location": { "lat": 6.46, "lng": 3.39 }, "safety_score": 41, "level": "high_risk" }
      ],
      "danger_zones": [
        { "checkpoint": 6, "score": 41, "warning": "High risk area — exercise caution" }
      ],
      "recommendation": "⚠️ Exercise caution — dangerous segment detected along route",
      "alternative_needed": false
    }

    Danger zone analytics#

    GET /api/v1/danger-zones/analytics
    Aggregated threat assessment for a location — incident counts, top threat types, and trend direction.
    Required scope: analytics:read
    Query parameters: latitude ✅, longitude ✅, radius_km (default 5)
    Response 200
    {
      "location": { "latitude": 6.4281, "longitude": 3.4219 },
      "radius_km": 5,
      "analytics": {
        "danger_level": "medium",
        "total_incidents": 47,
        "last_7days": 3,
        "last_30days": 11,
        "last_90days": 32,
        "top_incident_types": ["theft", "suspicious_activity", "vandalism"],
        "trend": "stable"
      }
    }

    Heatmap data#

    GET /api/v1/danger-zones/heatmap
    Weighted point data for rendering a threat heatmap on a map. Use this to power risk overlay layers in your product.
    Required scope: analytics:read
    Query parameters
    ParameterTypeRequiredDescription
    boundsstring✅"lat1,lng1,lat2,lng2" bounding box
    daysintegerLookback period. Default 90, max 365
    Example
    GET /api/v1/danger-zones/heatmap?bounds=6.35,3.30,6.70,3.55&days=30
    Response 200
    {
      "count": 128,
      "heatmap": [
        { "lat": 6.5244, "lng": 3.3792, "weight": 7, "type": "theft" }
      ]
    }

    List danger zones#

    GET /api/v1/danger-zones
    Required scope: analytics:read
    Query parameters: incident_type, severity_min (1–10), latitude, longitude, radius_km, from_date, to_date, page, limit

    Webhooks#

    Subscribe to real-time event notifications from the Sety dashboard under Settings → Webhooks.

    Event types#

    EventTrigger
    incident.createdIncident successfully received
    incident.updatedStatus changed (dispatching → assigned → responding → on_scene)
    incident.escalatedNo responder accepted in time
    incident.unresponsiveStill no response after escalation window
    incident.resolvedIncident closed by Sety
    incident.cancelledIncident cancelled
    access_code.createdCode generated
    access_code.usedCode successfully validated
    access_code.expiredCode passed expiry date
    access_code.revokedCode manually revoked
    visitor.arrivedVisitor checked in
    visitor.departedVisitor checked out

    Payload envelope#

    All events share this outer structure:
    {
      "id": "evt_a1b2c3d4...",
      "type": "incident.updated",
      "created_at": "2026-03-10T14:23:10.000Z",
      "data": {
        // Incident or access code object — same shape as the REST response
      }
    }

    Verifying signatures#

    Every webhook POST includes a Sety-Signature header. Verify it on your server to confirm the payload came from Sety.
    Header format:
    Sety-Signature: t=1741613220,v1=a3f9c2d18e4b7a6509c1...
    Node.js verification:

    Delivery behaviour#

    Sety waits up to 10 seconds for your endpoint to respond
    Any 2xx is treated as success — respond immediately and process async
    Failed deliveries are retried 3 times: immediately → +30s → +5 minutes
    Use event.id to deduplicate in case of retries

    Scopes Reference#

    ScopeWhat it grants
    incidents:readList and get incidents
    incidents:writeCreate incidents, cancel, add notes
    access_codes:readRead codes and stats
    access_codes:writeGenerate, bulk generate, revoke codes. Create and update access logs
    access_codes:validateValidate (consume) a code at a gate
    logs:readRead access logs and current visitors
    analytics:readSafety scores, route analysis, danger zone data, heatmaps

    Recommended scope sets by integration type#

    Emergency response integration (e.g. ride-hailing, fintech, logistics)
    incidents:read
    incidents:write
    analytics:read
    Visitor management (e.g. co-working space, corporate facility)
    access_codes:read
    access_codes:write
    access_codes:validate
    logs:read
    Full integration
    incidents:read
    incidents:write
    access_codes:read
    access_codes:write
    access_codes:validate
    logs:read
    analytics:read

    Questions? Contact us at [email protected] or visit sety.io/developer.
    Modified at 2026-03-10 02:55:40
    Next
    Health check
    Built with