Integration guide
AFCAS Mobile API Reference
Hosted integration guide for AFCAS Mobile field journeys, endpoints, equipment fields, and recovery rules.
AFCAS Mobile API Reference
Scope and non-scope
AFCAS Mobile is a native field client for AFCAS Safety. It supports six journeys:
- Auth and session resolution.
- Provider installation and equipment linking.
- Provider maintenance work.
- Provider-stock tagging.
- Client-site tagging by an assigned field team.
- AFCAS Safety compliance scan, equipment history, and field reports.
The app does not create clients, locations, QR batches, work packages, tagging teams, assignments, or provider membership. The AFCAS dashboard creates and approves those records. The app does not collect passwords, MFA codes, account recovery data, billing data, or a client asset number.
Environments and base URLs
Use values supplied by the environment owner. Do not copy a production host, OAuth client ID, or redirect URI from this document.
| Value | Placeholder |
|---|---|
| AFCAS Auth issuer and base URL | https://<auth-host> |
| AFCAS Safety API base URL | https://<safety-api-host>/api/v1 |
| OAuth client ID | <ios-or-android-mobile-client-id> |
| Redirect URI | <registered-native-callback-uri> |
| Safety API audience | <safety-api-audience> |
| OAuth scope | openid profile email guardian:mobile |
The scope name is a legacy technical identifier. Do not rename it without an OAuth migration. All Safety paths below are relative to the Safety API base.
Authentication and session order
Use Authorization Code with PKCE in the system browser. The client is public: never send a client secret.
- Create a fresh
state,nonce,code_verifier, and S256code_challenge. - Open
GET <auth-base>/oauth/authorize/withresponse_type=code,client_id, exactredirect_uri,scope,audience,state,nonce,code_challenge, andcode_challenge_method=S256. - Let AFCAS Auth complete login and MFA.
- On callback, reject a missing code or a state mismatch.
- Form-post to
POST <auth-base>/oauth/token/withgrant_type=authorization_code,client_id,code,redirect_uri, and the originalcode_verifier. - Store tokens and expiry only in OS-backed secure storage.
- Call
GET /auth/afcas/session/beforeGET /auth/users/me/. - Route only from the resolved Safety linkage, role, membership, access channel, permissions, and capabilities.
Token response shape. refresh_token is present only when the registered client
is allowed to receive refresh tokens:
{"access_token":"<redacted>","id_token":"<redacted>","refresh_token":"<redacted>","token_type":"Bearer","expires_in":600,"scope":"openid profile email guardian:mobile"}
Refresh once with POST <auth-base>/oauth/token/ and form fields
grant_type=refresh_token, client_id, and refresh_token. AFCAS Auth rotates
refresh tokens. Replace stored tokens immediately. On invalid_grant, clear
tokens and start sign-in. For sign-out, form-post client_id and refresh token
to POST <auth-base>/oauth/revoke/, then clear local tokens even if the network
call fails. Send Authorization: Bearer <access_token> to every protected Safety
route.
Session, roles, and gates
GET /auth/afcas/session/ requires a bearer token. Its key fields are
auth_subject, linked, requires_product_setup, setup_product,
setup_type, requires_safety_onboarding, claims, and message. If
linked is false or setup is required, stop field routing and open the
appropriate hosted AFCAS flow.
Call GET /auth/users/me/ only after session resolution reports a linked
account. Use its role,
service_provider_organization_id, membership role/status, access channel,
permissions, and auth_capabilities as display gates. Do not infer authority
from a token alone.
| Journey | Required server gate |
|---|---|
| Installation, maintenance, provider stock | Active provider membership, assigned work, and endpoint-specific provider checks. |
| Client-site tagging | IsActiveTaggingUser, active tagging-team membership, a matching group/region, and per-job actions/readiness. |
| AFCAS Safety verification and reports | AFCAS Safety or Compliance field access. |
Always render the server-supplied capabilities, readiness, and actions.
Do not enable an action from a cached role. A web actor must first accept and
assign provider work, prepare stock and QR custody, or create/assign the
tagging job. An AFCAS Safety manager must maintain active team membership and decide
top-up or exception requests.
Useful role values are service_provider_member, service_provider_admin,
safety_officer, and safety_admin. Client roles have no AFCAS Mobile
field-work home. Effective assignment, membership status, and endpoint checks
still control access for each role.
Endpoint catalog
All protected routes use bearer authentication. List endpoints use normal DRF
pagination where returned. Treat 403 as a gate failure, 404 as inaccessible
or absent, and validation 400 as field errors.
Authentication and session
| Method and path | Purpose and request | Response and retry |
|---|---|---|
GET <auth-base>/oauth/authorize/ |
Browser PKCE request above. | Redirect to hosted login or callback. Do not reuse a code. |
POST <auth-base>/oauth/token/ |
Form-encoded code exchange or refresh. | Tokens, 200; code/refresh errors are not retryable with changed values. |
POST <auth-base>/oauth/revoke/ |
Form-encoded client_id, refresh token. |
200; clear device state regardless. |
GET /auth/afcas/session/ |
Resolve Safety linkage first. | Session contract, 200; retry after refresh once on 401. |
GET /auth/users/me/ |
Get resolved account and effective access. | User contract, 200; do not call before linked session. |
Provider installation
| Method and path | Request / purpose | Response, side effect, retry |
|---|---|---|
GET /mobile/work-packages/ |
List assigned installation packages. | Package rows, 200; safe to retry. |
GET /mobile/work-packages/{id}/ |
Read items, client/location scope, evidence, and installed records. | Package, 200; refresh before mutation. |
POST /mobile/work-packages/{id}/start/ |
Start assigned package. | Updated package, 200; do not assume a local start state. |
POST /mobile/work-packages/{id}/block-item/ |
installation_work_package_item, blocked_reason, optional notes and evidence. |
Blocked item, 200; no idempotency key or replay guarantee exists. |
POST /mobile/work-packages/{id}/evidence-upload-url/ |
content_type, evidence_type, optional filename. |
Upload intent, 200; no equipment mutation. |
POST /mobile/installed-equipment/ |
Create/link equipment. See example below. | Installed-equipment record, 201; optional idempotency_key supports same-payload replay and is required when the external QR service is enabled. |
POST /mobile/installed-equipment/{id}/tag/ |
Link or update tag state for the record; optional idempotency_key. |
Updated record, 200; the optional key supports same-payload replay. |
GET /mobile/installed-equipment/{id}/history/ |
Read equipment lifecycle history. | History, 200; safe to retry. |
POST /mobile/work-packages/{id}/complete/ |
Complete only when server rules allow it. | Updated package, 200; no idempotency key or replay guarantee exists. |
Provider maintenance
| Method and path | Request / purpose | Response, side effect, retry |
|---|---|---|
GET /mobile/maintenance-work-packages/ |
List field-assigned maintenance packages. | Rows, 200; safe to retry. |
GET /mobile/maintenance-work-packages/{id}/ |
Read package and equipment items. | Package, 200; refresh before mutation. |
POST /mobile/maintenance-work-packages/{id}/start/ |
Start work. | Updated package, 200. |
POST /mobile/maintenance-work-packages/{id}/evidence-upload-url/ |
content_type, evidence_type, optional filename. |
Upload intent, 200. |
POST /mobile/maintenance-work-packages/{id}/record-service/ |
installed_equipment, qr_payload, optional condition_before (default unknown) and condition_after (default good), optional service_report, notes, evidence, idempotency_key. |
Updated item/service record, 200; the optional key enables same-payload replay. |
POST /mobile/maintenance-work-packages/{id}/block-item/ |
installed_equipment, blocked_reason, optional notes and idempotency_key. |
Updated item, 200; the optional key supports same-payload replay. |
POST /mobile/maintenance-work-packages/{id}/complete/ |
Optional idempotency_key; complete package. |
Updated package, 200; the optional key enables same-payload replay. |
Provider-stock tagging
| Method and path | Request / purpose | Response, side effect, retry |
|---|---|---|
GET /mobile/stock-tagging/jobs/ |
List provider-stock tagging jobs. | Job rows, 200; safe to retry. |
GET /mobile/stock-tagging/jobs/{id}/ |
Read a job. | Job, 200. |
GET/POST /mobile/stock-tagging/jobs/{id}/sessions/ |
GET lists sessions. POST requires item and stock_location, with optional session template. |
Session data, 200/201; preserve the returned session. |
POST /mobile/stock-tagging/jobs/{id}/units/ |
session, qr_code; optional item, serial/batch/dates, unit_template, evidence, field context, notes, and idempotency key. |
Submitted unit, 201; field context is optional. |
POST /mobile/stock-tagging/jobs/{id}/bulk-units/ |
session, up to 500 units; optional item and top-level template. Each row has qr_code and optional serial/batch/dates, evidence, field context, notes, unit_template, and idempotency key. |
Submitted units, 201; per-row keys replay only their same row/payload. Do not blindly replay a partial batch. |
POST /mobile/stock-tagging/jobs/{id}/sessions/{session_id}/complete/ |
status is completed or blocked; blocked_reason is required when blocked. |
Completed or blocked session, 200; no idempotency guarantee. |
Client-site tagging: canonical V2 lifecycle
This is the authoritative 13-endpoint lifecycle. It is used by eligible
provider field users and AFCAS Safety field users. Each mutable request has a
URL-safe idempotency_key of at most 120 characters. Its same-key replay is
safe only with the same payload.
| Method and path | Request/query | Response, side effect, retry |
|---|---|---|
GET /mobile/tagging/context/ |
None. | Teams, capabilities, policies, server time; 200. |
GET /mobile/tagging/jobs/ |
Pagination and supported filters such as execution_mode. |
Authorized jobs only; 200. |
GET /mobile/tagging/jobs/{job_id}/ |
None. | Job, items, units, property contract, evidence requirements, readiness, actions; 200. |
POST /mobile/tagging/jobs/{job_id}/claim/ |
idempotency_key, expected_job_version. |
Claim result, 200; refresh job on 409. |
POST /mobile/tagging/jobs/{job_id}/start/ |
idempotency_key, expected_job_version. |
Start result, 200; refresh on 409. |
GET /mobile/tagging/jobs/{job_id}/candidates/ |
Required work_item_id, optional search. |
Compatible existing-equipment candidates; 200. |
POST /mobile/tagging/jobs/{job_id}/units/prepare/ |
idempotency_key, expected_job_version, work_item_id, qr_payload, field_context; optional equipment_type and partial equipment. |
Reserved field unit, 201; preserve unit ID/version. |
POST /mobile/tagging/jobs/{job_id}/units/{unit_id}/complete/ |
idempotency_key, expected job/unit versions, resolution, reserved qr_payload, details_status, field_context, evidence; see payload rules below. |
Completed unit, 200; refresh on 409. |
POST /mobile/tagging/jobs/{job_id}/units/{unit_id}/exceptions/ |
Key, expected versions, exception code, note, field_context, optional evidence. |
Exception result, 200; key is replayable. |
POST /mobile/tagging/jobs/{job_id}/top-up-requests/ |
Key, expected job version, request_item_id, requested_min, requested_max, reason, context. |
Top-up request, 201; wait for web decision. |
GET /mobile/tagging/jobs/{job_id}/reconciliation/ |
None. | Remaining work and completion summary; 200. |
POST /mobile/tagging/jobs/{job_id}/complete/ |
Key, expected job version, context, optional variance_reason. |
Final reconciliation/job result; 200. |
GET /mobile/tagging/operations/{idempotency_key}/ |
Percent-encoded key. | Stored operation status, result, or error; 200. Use after an unknown mutation result. |
AFCAS Safety QR verification and reporting
| Method and path | Request / purpose | Response, side effect, retry |
|---|---|---|
POST /mobile/fire-service/qr-codes/verify/ |
qr_payload. Safety/Compliance only. |
valid, status, reason, batch, code state, linked equipment; 200. It records a scan where applicable. |
GET /mobile/fire-service/equipment-reports/ |
List caller reports, or all reports for Safety admin. | Report rows, 200; safe to retry. |
POST /mobile/fire-service/equipment-reports/evidence-upload-url/ |
content_type, evidence_type, optional filename. |
Upload intent, 200. |
POST /mobile/fire-service/equipment-reports/ |
Required report_type, summary, and either qr_payload or installed_equipment; optional severity, details, metadata, evidence, and idempotency_key. |
Created report, 201; the optional key enables same-payload replay. |
GET /mobile/fire-service/equipment-reports/{id}/ |
Read one authorized report. | Report, 200. |
POST /mobile/qr-codes/validate/ is a provider/Safety QR validation surface.
It takes qr_payload plus the endpoint's permitted work/equipment context.
Use it before a provider action when its response is needed. It is not a public
scan endpoint. The unauthenticated POST /public/tags/verify/ is outside this
native authenticated client contract.
Upload procedure
- Request the workflow-specific
evidence-upload-urlwithcontent_type,evidence_type, and optionalfilename. The server accepts but ignoresfilenamefor object naming. - Upload bytes to returned
upload_urlwith returnedmethodand exactheaders. - Send returned
file_refin the later mutation evidence row. Always rely on this returned value, not a filename-derived path. - Keep local bytes until the final mutation succeeds or the user discards it.
Typical intent response:
{"upload_url":"https://upload.example.invalid/signed","method":"PUT","headers":{"Content-Type":"image/jpeg"},"file_ref":"installation_evidence/<subject>/photo-uuid","max_size":10485760}
Do not send the local file path or raw bytes in a JSON evidence row. A common
row is {"evidence_type":"tagging_photo","file_ref":"<returned-ref>","captured_at":"2026-08-09T10:15:00Z","notes":""}.
Field context and V2 equipment payload
Installation, maintenance, and provider-stock context:
{"device_timestamp":"2026-08-09T10:15:00Z","device_metadata":{"device_id":"ios-example","platform":"ios"},"gps":{"latitude":5.6037,"longitude":-0.187,"accuracy_meters":12}}
If GPS is absent, replace gps with non-empty gps_fallback_reason.
V2 client-site tagging context:
{"device_id":"android-example","captured_at":"2026-08-09T10:15:00Z","platform":"android","gps_unavailable_reason":"not_collected"}
V2 requires device_id and captured_at. Send latitude and longitude together,
or send gps_unavailable_reason from permission_denied,
signal_unavailable, device_unavailable, or not_collected. accuracy_m,
app_version, and platform are optional. Capture time cannot be more than 10
minutes in the future or seven days old. Do not send field_context to an
AFCAS Safety report.
For complete V2 units, resolution is new_equipment or existing_equipment.
Existing equipment requires installed_equipment_id; new equipment must not
send it. details_status is confirmed or corrected; corrected requires
correction_reason. New equipment values use top-level keys when
storage=equipment and named values under equipment.specifications when
storage=specifications. Send evidence as a non-empty list.
{"idempotency_key":"tagging-complete:job:unit:1","expected_job_version":5,"expected_unit_version":1,"resolution":"new_equipment","qr_payload":"<opaque-scanned-payload>","details_status":"confirmed","field_context":{"device_id":"android-example","captured_at":"2026-08-09T10:16:00Z","gps_unavailable_reason":"not_collected"},"equipment":{"equipment_type":"11111111-1111-1111-1111-111111111111","serial_number":"SERIAL-42","condition":"good","floor":"Ground","specifications":{"extinguishing_agent":"co2","nominal_capacity_value":5,"nominal_capacity_unit":"kg","fire_classes":["B","C"]}},"evidence":[{"evidence_type":"tagging_photo","file_ref":"tagging/<subject>/photo-uuid"}]}
Equipment property contract
The returned property_contract has version equipment_tagging_properties_v1.
Each field has key, label, control, storage, required, value_type,
help_text, choices, and optional required_when, required_with,
choice_filters, other_value_key, or fixed_value. Render the server
contract, not hard-coded UI rules.
Shared fields for all six groups: condition (good, needs_repair,
unknown), optional manufacturer, model_number, serial_number, floor,
zone, placement_note, and optional last_service_date /
next_service_due_date. Enter at least one placement field, except elevator
landing doors require floor and elevator cabin doors/cabins require
placement_note. manufacture_date and expiry_date are extinguisher-only.
Date values use YYYY-MM-DD: expiry must be after manufacture, last service
cannot be in the future, and next service cannot be before last service. Do not
collect client_asset_number or client asset ID.
| Group and canonical type(s) | Type-specific fields, controls, choices, and conditions |
|---|---|
fire_extinguisher: Fire Extinguisher |
extinguishing_agent: dry_powder, co2, foam, water, wet_chemical, clean_agent, other; nominal_capacity_value decimal >0; unit kg/L; fire_classes multi-select A/B/C/D/F/K, one or more; optional manufacture_date and expiry_date with the date rules above. |
suppression_system: Suppression System |
suppression_system_type: sprinkler, clean_agent, co2, inert_gas, wet_chemical, dry_chemical, foam, water_mist, other. Dependent system_variant: wet_pipe, dry_pipe, deluge, preaction, total_flooding, local_application, kitchen_hood, other. Dependent suppression_agent: fm_200, fk_5_1_12, co2, inert_gas, wet_chemical, dry_chemical, foam, other. activation_modes multi-select automatic/manual, one or more; protected_area text. Variant/agent appear only for compatible types. |
mechanical_active_fire_fighting_system: Fire Hose Reel, Layflat Fire Hose |
hose_diameter_value decimal >0 with fixed mm; hose_length_value decimal >0 with fixed m. |
| same mechanical group: Landing Valve | connection_size_value decimal >0 and connection_size_unit mm/in. |
| same mechanical group: Fire Hydrant | hydrant_configuration: pillar, underground, wall, other; outlet_count integer >0; optional connection size and unit, where unit is required with a value. |
| same mechanical group: Fire Equipment Cabinet | cabinet_use: hose_reel, hydrant, mixed, other. |
| same mechanical group: Fire Brigade Inlet | inlet_configuration: siamese, breeching, other; connection_count integer >0; optional connection size with required paired unit. |
| same mechanical group: Fire Pump | pump_role: main, jockey, standby, other; power_source: electric, diesel, other; optional rated flow with L/min/m3/h, and optional rated pressure with bar/kPa; each unit is required when its value exists. |
fire_rated_doors: Fire Rated Door |
door_configuration: single_leaf, double_leaf, sliding, other; door_material: steel, timber, glass, composite, other; fire_rating_minutes: 30, 60, 90, 120, other. other requires a positive integer companion value. |
fire_alarm: Fire Alarm Control Panel |
alarm_system_type: conventional, addressable, other; zone_count integer >0 required for conventional; loop_count integer >0 required for addressable. |
elevator_system: Elevator Landing Door, Elevator Cabin Door, Elevator Cabin |
Doors have door_material: steel, glass, composite, other and the fire-rating control above. Cabin has no door-specific property. Keep elevator_system as the API group key. |
For every other selection, show and send the companion text key named by
other_value_key. Do not offer excluded equipment variants, a fire blanket,
component-level alarm tags, batch number, commissioning date, or product
variants as equipment types. Known backend gaps: the active-type lookup can
currently allow a non-canonical active equipment type outside this approved
list; client UI must still restrict selection to the canonical group list.
Also, the decimal validator currently accepts NaN and Infinity; reject both
locally and do not send them. These are enforcement gaps, not supported values.
Error, conflict, retry, and offline rules
| Status or condition | Client action |
|---|---|
| 400 | Show serializer field errors. Preserve the draft. Do not mutate values automatically. |
| 401 | Refresh once, retry the original request once, then clear tokens and sign in. |
| 403 | Refresh session/context. Show the missing membership, assignment, or capability gate. |
| 404 | Treat as removed or inaccessible. Return to the list after a refresh. |
| 409 | Version or state conflict. GET job detail, reconcile, then ask the user to retry with current versions. |
| 429 | Observe Retry-After when present. Do not create a new mutation key. |
| 5xx/network timeout | Keep the same intent/key. For V2, GET its operation endpoint before replay. |
There is no server-side pause endpoint. A pause is a local draft only. Offline execution requires an encrypted durable ordered queue. Persist the intent, idempotency key, expected versions, QR payload hash, upload state, and evidence metadata before replay. Never invent a new key because a response is unknown. Do not replay a bulk provider-stock request until the client has reconciled which units succeeded.
Realistic examples
Installation create:
{"installation_work_package":"11111111-1111-1111-1111-111111111111","installation_work_package_item":"22222222-2222-2222-2222-222222222222","equipment_type":"33333333-3333-3333-3333-333333333333","canonical_qr_code":"<opaque-scanned-payload>","stock_allocation":"44444444-4444-4444-4444-444444444444","idempotency_key":"install:package:item:001","serial_number":"SERIAL-42","installation_date":"2026-08-09","evidence":[{"evidence_type":"installation_photo","file_ref":"installation_evidence/<subject>/photo-uuid"}]}
Maintenance service request:
{"installed_equipment":"55555555-5555-5555-5555-555555555555","qr_payload":"<opaque-scanned-payload>","condition_before":"good","condition_after":"good","service_report":"Pressure checked.","notes":"No replacement needed.","evidence":[{"evidence_type":"maintenance_photo","file_ref":"maintenance_evidence/<subject>/photo-uuid"}],"idempotency_key":"maintenance-service:package:equipment:001"}
AFCAS Safety report request (no field_context):
{"qr_payload":"<opaque-scanned-payload>","installed_equipment":"55555555-5555-5555-5555-555555555555","report_type":"unsafe","severity":"medium","summary":"Extinguisher requires attention.","details":"Seal is broken.","evidence":[{"evidence_type":"inspection_photo","file_ref":"fire_service_reports/<subject>/photo-uuid"}],"idempotency_key":"fire-report:equipment:001"}