Keep your fleet system
Map your existing driver IDs to PetroSupport accounts. Drivers use our app; your system remains your operational source.
Fleet integration guide →
developersIntegrate fleet drivers, incident activation, response progress, and signed events into your own software. Start with synthetic sandbox data, then request access to your verified organization.
Map your existing driver IDs to PetroSupport accounts. Drivers use our app; your system remains your operational source.
Fleet integration guide →Create one incident with independent medical, security, and recovery tasks. Read progress or reconcile signed events.
Incident API →The credential determines the environment and permitted organization. Sandbox data never dispatches real responders.
Register to receive a sandbox application automatically. Open your console and create a credential.
Set PETROSUPPORT_BASE_URL to https://petrosupportea.com/api/partner/v1, PETROSUPPORT_TOKEN to your credential, and PETROSUPPORT_IDEMPOTENCY_KEY to a unique operation ID. Persist the key and request bytes before sending.
The test UUID below works in the sandbox. Production requires an active driver in your approved fleet. These are native HTTP reference examples, not packaged SDKs.
# Set PETROSUPPORT_BASE_URL, PETROSUPPORT_TOKEN and PETROSUPPORT_IDEMPOTENCY_KEY.
# The base URL includes /api/partner/v1. Use a fresh key for each new operation.
curl --fail-with-body --max-time 20 \
"$PETROSUPPORT_BASE_URL/incidents" \
-H "Authorization: Bearer $PETROSUPPORT_TOKEN" \
-H "Idempotency-Key: $PETROSUPPORT_IDEMPOTENCY_KEY" \
-H 'Content-Type: application/json' \
--data-binary '{"title":"Fleet roadside incident","priority":"high","requested_by":"11111111-1111-4111-a111-111111111111","latitude":-1.292100000000000026290081223123706877231597900390625,"longitude":36.821899999999999408828443847596645355224609375,"services":["recovery"],"external_reference":"FLEET-INC-001"}'<?php // PHP 8+ with the cURL extension; run on your server.
$base = rtrim(getenv('PETROSUPPORT_BASE_URL'), '/');
$body = '{"title":"Fleet roadside incident","priority":"high","requested_by":"11111111-1111-4111-a111-111111111111","latitude":-1.292100000000000026290081223123706877231597900390625,"longitude":36.821899999999999408828443847596645355224609375,"services":["recovery"],"external_reference":"FLEET-INC-001"}'; // Persist these exact bytes and key for retries.
$ch = curl_init($base . '/incidents');
curl_setopt_array($ch, [CURLOPT_POST => true, CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 5, CURLOPT_TIMEOUT => 20,
CURLOPT_HTTPHEADER => ['Content-Type: application/json',
'Authorization: Bearer ' . getenv('PETROSUPPORT_TOKEN'),
'Idempotency-Key: ' . getenv('PETROSUPPORT_IDEMPOTENCY_KEY')],
CURLOPT_POSTFIELDS => $body]);
$response = curl_exec($ch);
if ($response === false) { throw new RuntimeException('Transport failed; reconcile before retrying.'); }
$status = curl_getinfo($ch, CURLINFO_HTTP_CODE);
curl_close($ch);
if ($status >= 300) { throw new RuntimeException("API returned HTTP $status: $response"); }
echo $response;# Ruby 3+; standard library only.
require 'net/http'
require 'uri'
base = ENV.fetch('PETROSUPPORT_BASE_URL').sub(%r{/$}, '')
uri = URI("#{base}/incidents")
request = Net::HTTP::Post.new(uri)
request['Authorization'] = "Bearer #{ENV.fetch('PETROSUPPORT_TOKEN')}"
request['Content-Type'] = 'application/json'
request['Idempotency-Key'] = ENV.fetch('PETROSUPPORT_IDEMPOTENCY_KEY')
request.body = '{"title":"Fleet roadside incident","priority":"high","requested_by":"11111111-1111-4111-a111-111111111111","latitude":-1.292100000000000026290081223123706877231597900390625,"longitude":36.821899999999999408828443847596645355224609375,"services":["recovery"],"external_reference":"FLEET-INC-001"}'
http = Net::HTTP.new(uri.host, uri.port)
http.use_ssl = uri.scheme == 'https'
http.open_timeout = 5
http.read_timeout = 20
response = http.request(request)
raise "HTTP #{response.code}: #{response.body}" unless response.is_a?(Net::HTTPSuccess)
puts response.body// Go 1.20+; standard library only.
package main
import (
"fmt"
"io"
"net/http"
"os"
"strings"
"time"
)
func main() {
body := `{"title":"Fleet roadside incident","priority":"high","requested_by":"11111111-1111-4111-a111-111111111111","latitude":-1.292100000000000026290081223123706877231597900390625,"longitude":36.821899999999999408828443847596645355224609375,"services":["recovery"],"external_reference":"FLEET-INC-001"}`
url := strings.TrimRight(os.Getenv("PETROSUPPORT_BASE_URL"), "/") + "/incidents"
req, err := http.NewRequest("POST", url, strings.NewReader(body))
if err != nil { panic(err) }
req.Header.Set("Authorization", "Bearer " + os.Getenv("PETROSUPPORT_TOKEN"))
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Idempotency-Key", os.Getenv("PETROSUPPORT_IDEMPOTENCY_KEY"))
client := &http.Client{Timeout: 20 * time.Second,
CheckRedirect: func(*http.Request, []*http.Request) error { return http.ErrUseLastResponse }}
res, err := client.Do(req)
if err != nil { panic("Transport failed; reconcile before retrying") }
defer res.Body.Close()
data, err := io.ReadAll(res.Body)
if err != nil { panic(err) }
if res.StatusCode >= 300 { panic(fmt.Sprintf("HTTP %d: %s", res.StatusCode, data)) }
fmt.Println(string(data))
}// Node.js 20+ server-side JavaScript. Never expose the bearer credential in a browser.
const base = process.env.PETROSUPPORT_BASE_URL.replace(/\/$/, '');
const body = '{"title":"Fleet roadside incident","priority":"high","requested_by":"11111111-1111-4111-a111-111111111111","latitude":-1.292100000000000026290081223123706877231597900390625,"longitude":36.821899999999999408828443847596645355224609375,"services":["recovery"],"external_reference":"FLEET-INC-001"}';
const response = await fetch(`${base}/incidents`, {
method: 'POST', redirect: 'error', signal: AbortSignal.timeout(20000),
headers: {
Authorization: `Bearer ${process.env.PETROSUPPORT_TOKEN}`,
'Content-Type': 'application/json',
'Idempotency-Key': process.env.PETROSUPPORT_IDEMPOTENCY_KEY
},
body
});
const result = await response.json();
if (!response.ok) throw new Error(`HTTP ${response.status}: ${JSON.stringify(result)}`);
console.log(result);# Python 3.10+; standard library only.
import os
import urllib.request
import urllib.error
base = os.environ['PETROSUPPORT_BASE_URL'].rstrip('/')
body = b'{"title":"Fleet roadside incident","priority":"high","requested_by":"11111111-1111-4111-a111-111111111111","latitude":-1.292100000000000026290081223123706877231597900390625,"longitude":36.821899999999999408828443847596645355224609375,"services":["recovery"],"external_reference":"FLEET-INC-001"}'
request = urllib.request.Request(base + '/incidents', data=body, method='POST', headers={
'Authorization': 'Bearer ' + os.environ['PETROSUPPORT_TOKEN'],
'Content-Type': 'application/json',
'Idempotency-Key': os.environ['PETROSUPPORT_IDEMPOTENCY_KEY'],
})
class NoRedirect(urllib.request.HTTPRedirectHandler):
def redirect_request(self, req, fp, code, msg, headers, newurl):
return None
opener = urllib.request.build_opener(NoRedirect)
try:
with opener.open(request, timeout=20) as response:
print(response.read().decode())
except urllib.error.HTTPError as error:
raise RuntimeError(f'HTTP {error.code}: {error.read().decode()}') from errorDownload an example: cURLPHPRubyGoJavaScriptPython
Save data.id and data.tasks[].id. Read GET /incidents/{incident} for current state. Advance your test response with the simulator.
{
"data": {
"id": "22222222-2222-4222-a222-222222222222",
"environment": "sandbox",
"external_reference": "FLEET-INC-001",
"title": "Fleet roadside incident",
"priority": "high",
"status": "open",
"tasks": [{"id": "33333333-3333-4333-a333-333333333333", "service": "recovery", "status": "requested"}],
"latest_sequence": 1,
"created_at": "2026-10-21T09:00:00.000000Z"
}
}Send Authorization: Bearer <credential> on every API request. The credential contains an identifier and a secret. Console-issued credentials expire after 90 days and can be revoked immediately. This release uses scoped bearer credentials; there is no OAuth token exchange.
Authorization: Bearer YOUR_CREDENTIAL
Accept: application/json
Content-Type: application/json
Idempotency-Key: your-persisted-operation-idStore credentials on your server. Your mobile app or browser calls your backend, which calls PetroSupport. Create a replacement, switch your backend, then revoke the old key. A credential cannot select another organization through a header or request field.
Open the visual simulator · Open the authenticated API playground
The simulator uses internal sandbox lifecycle actions with synthetic route markers. Delay and unavailable-responder exercises are local training controls, not recorded availability outcomes. The playground authenticates your supplied sandbox bearer credential through normal API checks. Neither tool certifies your external HTTP client or receiver.
Test safelySandbox incidents are isolated. They do not notify responders, dispatch vehicles, or create real driver accounts. Sandbox webhooks can reach your receiver; use a dedicated test endpoint.
Call POST /simulations with a new idempotency key for each transition:
{
"incident_id": "22222222-2222-4222-a222-222222222222",
"task_id": "33333333-3333-4333-a333-333333333333",
"status": "accepted"
}requested → accepted → en_route → arrived → working → completed. An arrived task may also complete directly. All tasks must complete before the incident closes. Production credentials cannot use the simulator.
Exercise activation, duplicate retries, event reconciliation, errors, and signature validation. Fleet customers should also test driver synchronization and invitations.
Use your console to submit your legal organization, website, use case, test evidence, and requested scopes. Track review notes there.
An administrator verifies your authority, links a verified organization and service identity, and grants permissions. Approval creates a separate production application. Sandbox data stays isolated. Deployment activation is also required before live credentials can be issued.
Coverage is not a reservation. Incident creation confirms persistence. Dispatch, acceptance, and physical arrival are separate outcomes.
Keep your existing fleet management system. Synchronize a driver using a stable external ID, invite that driver into our app, and activate incidents using the returned PetroSupport driver ID. Your ERP or fleet system can remain the roster source.
| Step | Action | Result |
|---|---|---|
| Synchronize | PUT /fleet/drivers/EMP-0042 | Map your ID to a driver in your approved organization. |
| Invite | POST /fleet/drivers/EMP-0042/invitation | Send a single-use password setup link; no password is returned. |
| Operate | Driver signs into the PetroSupport Driver app | Use app workflows for checks, journeys, and emergency response. |
| Coordinate | POST /incidents with returned driver_id | Follow response tasks and events in your system. |
| Report | Console and organization reports | Export scoped usage alongside existing operational reports. |
{
"first_name": "Test",
"last_name": "Driver",
"email": "driver42@example.com",
"phone": "+254700000042",
"status": "active"
}Requires fleet:write and an idempotency key. Sandbox returns a simulated ID. Production creates a driver awaiting invitation acceptance. Send {} to the invitation endpoint. A 202 response records the request; it does not prove email delivery. Invitations expire after 24 hours. If delivery fails, request another with a new key after the five-minute cooldown.
An external ID is scoped to one application. Existing email/phone identities cannot be claimed automatically; an administrator must resolve conflicts. Contact identity changes require review. Setting status: inactive deactivates only this organization's membership. Vehicle synchronization, telematics ingestion, partner journey assignment, and linking pre-existing accounts are further extensions.
Open the webhook inspector to review delivery outcomes and attempt history recorded since installation. Terminal sandbox deliveries can be queued again with the original event ID. Production replay remains an administrator action. Test duplicate and older-event delivery at your receiver; queue order is not guaranteed. HTTP acknowledgement does not prove signature verification or business processing.
Event-driven integrationsRegister a public HTTPS receiver with POST /webhook-endpoints. Store the signing secret returned by registration. Verify the exact raw bytes before parsing:
X-PetroSupport-Event-ID: <event UUID>
X-PetroSupport-Delivery-ID: <delivery UUID>
X-PetroSupport-Timestamp: <Unix seconds>
X-PetroSupport-Signature: v1=<hex digest>
signed_message = timestamp + "." + raw_body
digest = HMAC_SHA256(signing_secret, signed_message)Compare signatures in constant time. Reject timestamps outside your accepted window, for example five minutes. Persist event IDs before processing. Events may repeat or arrive out of order; reconcile sequence gaps with GET /incidents/{incident}/events?after=LAST_SEQUENCE. Return 2xx after durable acceptance.
Endpoints require public HTTPS DNS on port 443, without URL credentials, queries, fragments, IP literals, or redirects. Up to ten enabled endpoints are supported. Failed deliveries retry after 30, 60, 120, 240, and 480 seconds, subject to worker cadence, before the sixth failure becomes terminal. Read GET /webhook-deliveries for outcomes. Rotate secrets by registering a replacement and disabling the old endpoint after switching.
Every write requires an Idempotency-Key: 8–128 letters, digits, dots, underscores, colons, or hyphens. Persist it with the exact request bytes. Retry uncertain results using the same method, path, key, and bytes. Different content with the same key returns 409. Successful replays include Idempotent-Replayed: true; read current state separately. Failed validation is not retained. Keys stay reserved for the application's lifetime.
Bodies are limited to 64 KiB. Rate limits appear in X-RateLimit-Limit and X-RateLimit-Remaining. Respect Retry-After on 429 and use bounded backoff for transient failures. Timestamps use UTC. Incident lists accept page and per_page (up to 100); driver lists use 50 per page. Events use after and limit (up to 100).
| HTTP | Action |
|---|---|
| 401 | Check expiry, revocation, and bearer credential. |
| 403 | Check scopes, environment, account status, and organization authorization. |
| 404 | Resource is missing or belongs to another application. |
| 409 | Reconcile idempotency, lifecycle, or identity conflicts. |
| 422 | Correct validation errors before retrying. |
| 429 | Wait for Retry-After. |
| 503 | Access is unavailable; retain the operation and reconcile safely. |
Responses include X-Request-ID. Provide support the request ID, application ID, endpoint, and UTC time. Exclude credentials and patient records. Clients must tolerate additive fields and new event types; breaking contracts require a new major API version.
Base URL: https://petrosupportea.com/api/partner/v1. This reference shares its contract with the OpenAPI 3.1 download.
Required scope fleet:read
| Name | Location | Type | Required |
|---|---|---|---|
page | query | integer | No |
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | array | |
meta | PageMeta | meta.page · integermeta.last_page · integer |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope fleet:write
| Name | Location | Type | Required |
|---|---|---|---|
external | path | string | Yes |
Idempotency-Key | header | string | Yes |
| Field | Type | Required | Rules |
|---|---|---|---|
first_name | string | Yes | Max 100 characters. |
last_name | string | Yes | Max 100 characters. |
email | string | Yes | Max 191 characters. |
phone | string | Yes | Max 20 characters. |
status | string | Yes | active, inactive |
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | FleetDriver | data.external_id · stringdata.driver_id · stringdata.environment · stringdata.first_name · stringdata.last_name · stringdata.email · stringdata.phone · stringdata.status · stringdata.account_ready · boolean |
201 · Driver mapping created
| Field | Type | Description |
|---|---|---|
data | FleetDriver | data.external_id · stringdata.driver_id · stringdata.environment · stringdata.first_name · stringdata.last_name · stringdata.email · stringdata.phone · stringdata.status · stringdata.account_ready · boolean |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope fleet:write
| Name | Location | Type | Required |
|---|---|---|---|
external | path | string | Yes |
Idempotency-Key | header | string | Yes |
Send an empty JSON object: {}.
202 · Successful operation
| Field | Type | Description |
|---|---|---|
data | DriverInvitation | data.external_id · stringdata.invitation_status · stringdata.email_sent · boolean | nulldata.expires_in_seconds · integer |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope services:read
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | ServiceCatalogue | data.version · stringdata.services · arraydata.dispatch · string |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope coverage:read
| Name | Location | Type | Required |
|---|---|---|---|
latitude | query | number | Yes |
longitude | query | number | Yes |
service | query | string | Yes |
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | CoverageAssessment | data.environment · stringdata.service · stringdata.coverage_status · stringdata.candidate_count · integerdata.availability_confirmed · booleandata.observed_at · string |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope incidents:read
| Name | Location | Type | Required |
|---|---|---|---|
page | query | integer | No |
per_page | query | integer | No |
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | array | |
meta | object |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope incidents:write
| Name | Location | Type | Required |
|---|---|---|---|
Idempotency-Key | header | string | Yes |
| Field | Type | Required | Rules |
|---|---|---|---|
title | string | Yes | Max 180 characters. |
priority | string | Yes | critical, high, normal |
requested_by | string | Yes | Active fleet driver UUID; sandbox accepts a test UUID. |
latitude | number | Yes | Minimum -90. Maximum 90. |
longitude | number | Yes | Minimum -180. Maximum 180. |
services | array | Yes | Values: medical, security, recovery. |
external_reference | string | No | Max 120 characters. |
requirements | object | No |
201 · Successful operation
| Field | Type | Description |
|---|---|---|
data | Incident | data.id · stringdata.environment · stringdata.external_reference · string | nulldata.title · stringdata.priority · stringdata.status · stringdata.tasks · arraydata.latest_sequence · integerdata.created_at · string |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope incidents:read
| Name | Location | Type | Required |
|---|---|---|---|
incident | path | string | Yes |
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | Incident | data.id · stringdata.environment · stringdata.external_reference · string | nulldata.title · stringdata.priority · stringdata.status · stringdata.tasks · arraydata.latest_sequence · integerdata.created_at · string |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope incidents:read
| Name | Location | Type | Required |
|---|---|---|---|
incident | path | string | Yes |
after | query | integer | No |
limit | query | integer | No |
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | array | |
meta | object |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope dispatch:write
| Name | Location | Type | Required |
|---|---|---|---|
incident | path | string | Yes |
task | path | string | Yes |
Idempotency-Key | header | string | Yes |
Send an empty JSON object: {}.
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | DispatchResult | data.task_id · stringdata.offered_count · integerdata.message · string |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope incidents:write
| Name | Location | Type | Required |
|---|---|---|---|
Idempotency-Key | header | string | Yes |
| Field | Type | Required | Rules |
|---|---|---|---|
incident_id | string | Yes | |
task_id | string | Yes | |
status | string | Yes | accepted, en_route, arrived, working, completed |
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | Incident | data.id · stringdata.environment · stringdata.external_reference · string | nulldata.title · stringdata.priority · stringdata.status · stringdata.tasks · arraydata.latest_sequence · integerdata.created_at · string |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope webhooks:manage
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | array |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope webhooks:manage
| Name | Location | Type | Required |
|---|---|---|---|
Idempotency-Key | header | string | Yes |
| Field | Type | Required | Rules |
|---|---|---|---|
url | string | Yes | Max 2048 characters. |
events | array | Yes | Values: incident.created, incident.closed, task.offered, task.accepted, task.en_route, task.arrived, task.working, task.completed, task.status_changed, incident.simulated. |
201 · Successful operation
| Field | Type | Description |
|---|---|---|
data | WebhookIssued | data.id · stringdata.url · stringdata.signing_secret · stringdata.events · array |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope webhooks:manage
| Name | Location | Type | Required |
|---|---|---|---|
webhook | path | string | Yes |
Idempotency-Key | header | string | Yes |
Send an empty JSON object: {}.
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | WebhookDisabled | data.id · stringdata.enabled · boolean |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.
Required scope webhooks:manage
| Name | Location | Type | Required |
|---|---|---|---|
page | query | integer | No |
200 · Successful operation
| Field | Type | Description |
|---|---|---|
data | array | |
meta | PageMeta | meta.page · integermeta.last_page · integer |
Other statuses: 400, 401, 403, 404, 409, 413, 415, 422, 429, 500, 503. See error handling.