Webhooks
Flow Access API uses webhooks to notify your system when important events occur in real time.
Instead of polling our system for updates, we push event data directly to your server whenever something happens — such as a meter sending uplinks, a recharge being completed, or water being fetched.
Webhooks are asynchronous and event-driven.
Polling vs Webhooks
Polling
- Your system repeatedly asks Flow for updates
- Increased latency and unnecessary requests
- Not suitable for real-time events
Webhooks
- Flow sends data only when something happens
- Near real-time delivery
- Lower infrastructure and network cost
How Webhooks Work
The webhook delivery flow follows these steps:
-
Register a Webhook URL When creating or configuring an API client, you provide a
webhookURL. -
An Event Occurs Examples include:
- A meter sends uplink data
- A recharge invoice is created or completed
- A water fetch is recorded
-
Flow Sends a Webhook Flow sends an HTTP
POSTrequest with a JSON payload to your webhook URL. -
Your Server Acknowledges Your server must respond with HTTP
201 Createdto confirm successful processing.
If your webhook endpoint:
- Returns any status code other than
201 Created, or - Takes longer than 10 seconds to respond
the webhook delivery is considered failed.
When Webhooks Are Sent
Webhooks are sent automatically when Flow processes events from its internal event bus.
You do not need to call any API to trigger webhooks.
Internally, Flow listens to system events and forwards them to the correct client based on:
- Business ID
- Event data type
- Webhook activation status
Supported Event Types
Each webhook event has a specific purpose and payload structure.
Types of Events
Here are the webhook events currently sent by Flow. We may add more events as we hook into additional actions in the future.
| Event | Description |
|---|---|
device_log | A device (meter) has sent uplinks |
recharges_log | A recharge, invoice has been created, succeeded, or failed |
fetches_log | A water fetch transaction has been recorded |
Device Uplink (device_log)
This event is sent whenever a meter or device sends uplinks data to Flow.
Payload Structure
- JSON Payload
- Go Struct
{
"battery1Alarm": false,
"battery2Alarm": false,
"eeAlarm": false,
"emptyTubeAlarm": false,
"flowRate": 1.23,
"overRangeAlarm": false,
"pipeLeakageAlarm": false,
"pipeBurstAlarm": false,
"receivedAt": "2025-12-15T10:00:00Z",
"deviceTime": "2025-12-15T09:59:58Z",
"rechargeBalance": 1234.5,
"rechargeTimes": 10,
"reverseFlowAlarm": false,
"address": "device-serial-number-123",
"totalConsumption": 54321.9,
"valveStatus": 1,
"waterTemperature": 25.5,
"waterTemperatureAlarm": false,
"valveMode": 1,
"closeThreshold": 100,
"lastPurchaseCredit": 50.0
}
type DeviceMeterUpLinkClientDomain struct {
Battery1Alarm bool `json:"battery1Alarm"`
Battery2Alarm bool `json:"battery2Alarm"`
EeAlarm bool `json:"eeAlarm"`
EmptyTubeAlarm bool `json:"emptyTubeAlarm"`
FlowRate float64 `json:"flowRate"`
OverRangeAlarm bool `json:"overRangeAlarm"`
PipeLeakageAlarm bool `json:"pipeLeakageAlarm"`
PipeBurstAlarm bool `json:"pipeBurstAlarm"`
ReceivedAt *time.Time `json:"receivedAt"`
DeviceTime *time.Time `json:"deviceTime"`
RechargeBalance float64 `json:"rechargeBalance"`
RechargeTimes int64 `json:"rechargeTimes"`
ReverseFlowAlarm bool `json:"reverseFlowAlarm"`
Address string `json:"address"`
TotalConsumption float64 `json:"totalConsumption"`
ValveStatus float64 `json:"valveStatus"`
WaterTemperature float64 `json:"waterTemperature"`
WaterTemperatureAlarm bool `json:"waterTemperatureAlarm"`
ValveMode int64 `json:"valveMode"`
CloseThreshold int64 `json:"closeThreshold"`
LastPurchaseCredit float64 `json:"lastPurchaseCredit"`
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
battery1Alarm | boolean | Alarm status for battery 1. |
battery2Alarm | boolean | Alarm status for battery 2. |
eeAlarm | boolean | EEPROM alarm. |
emptyTubeAlarm | boolean | Alarm for an empty tube. |
flowRate | number | Current flow rate of water. |
overRangeAlarm | boolean | Alarm for when the flow is over range. |
pipeLeakageAlarm | boolean | Alarm for a detected pipe leakage. |
pipeBurstAlarm | boolean | Alarm for a detected pipe burst. |
receivedAt | string | Timestamp (ISO 8601) when the server received the data. |
deviceTime | string | Timestamp (ISO 8601) from the device itself. |
rechargeBalance | number | The current remaining water credit balance. |
rechargeTimes | integer | The total number of times the device has been recharged. |
reverseFlowAlarm | boolean | Alarm for water flowing in the reverse direction. |
address | string | The unique serial number or identifier for the device. |
totalConsumption | number | Total water consumption recorded by the device. |
valveStatus | number | The current status of the valve. |
waterTemperature | number | The temperature of the water. |
waterTemperatureAlarm | boolean | Alarm for water temperature exceeding limits. |
valveMode | integer | The operational mode of the valve. |
closeThreshold | integer | The threshold at which the valve will close. |
lastPurchaseCredit | number | The amount of the last credit purchase. |
Recharge Receipt (recharges_log)
This feed provides information about sales transactions, specifically when a user recharges their water credit balance.
Payload Structure
- JSON Payload
- Go Struct
{
"id": "recharge-id-456",
"amount": 20.0,
"litres": 2000.0,
"pricePerLitre": 0.01,
"currency": "USD",
"paymentMode": "MOBILE_MONEY",
"paymentType": "PREPAID",
"status": "COMPLETED",
"reason": "",
"serialNumber": "device-serial-number-123",
"deviceType": "METER_V1",
"volume": 2000,
"device": {
"id": "device-id-789",
"name": "Main Street Meter",
"serialNumber": "device-serial-number-123",
"status": "ACTIVE",
"category": "RESIDENTIAL",
"coordinate": {
"latitude": -1.286389,
"longitude": 36.817223
},
"zone": {
"id": "zone-id-abc",
"name": "Downtown",
"location": "Nairobi",
"currency": "KES",
"pricePerLitre": 1.2,
"supportPhoneNumber": "+254700000000"
}
},
"card": {
"id": "card-id-xyz",
"serialNumber": "card-serial-12345",
"type": "NFC_CARD",
"linkedAt": "2025-12-15T10:00:00Z",
"createdAt": "2025-12-15T10:00:00Z",
"active": true,
"customer": {
"id": "user-id-def",
"firstName": "Jane",
"lastName": "Doe",
"username": "janedoe",
"phoneNumber": "+254712345678",
"emailAddress": "jane.doe@example.com"
}
},
"user": {
"id": "user-id-def",
"firstName": "Jane",
"lastName": "Doe",
"username": "janedoe",
"phoneNumber": "+254712345678",
"emailAddress": "jane.doe@example.com"
},
"performedBy": {
"id": "user-id-ghi",
"firstName": "John",
"lastName": "Smith",
"username": "johnsmith",
"phoneNumber": "+254787654321",
"emailAddress": "john.smith@example.com"
},
"completedAt": "2025-12-15T11:30:00Z",
"createdAt": "2025-12-15T11:29:00Z",
"zone": {
"id": "zone-id-abc",
"name": "Downtown",
"location": "Nairobi",
"currency": "KES",
"pricePerLitre": 1.2,
"supportPhoneNumber": "+254700000000"
},
"type": "RECHARGE"
}
type RechargeClientDomain struct {
ID string `json:"id"`
Amount float64 `json:"amount"`
Litres float64 `json:"litres"`
PricePerLitre float64 `json:"pricePerLitre"`
Currency string `json:"currency"`
PaymentMode string `json:"paymentMode"`
PaymentType string `json:"paymentType"`
Status string `json:"status"`
Reason string `json:"reason,omitempty"`
SerialNumber string `json:"serialNumber,omitempty"`
DeviceType string `json:"deviceType"`
Volume int `json:"volume"`
Device *DeviceClientDomain `json:"device"`
Card *CardClientDomain `json:"card"`
User *UserClientDomain `json:"user,omitempty"`
PerformedBy *UserClientDomain `json:"performedBy"`
CompletedAt *time.Time `json:"completedAt"`
CreatedAt *time.Time `json:"createdAt"`
Zone *ZoneClientDomain `json:"zone,omitempty"`
Type string `json:"type"`
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
id | string | The unique ID for this recharge transaction. |
amount | number | The monetary amount of the recharge. |
litres | number | The number of litres of water credited. |
pricePerLitre | number | The price per litre at the time of the transaction. |
currency | string | The currency of the transaction. |
paymentMode | string | How the payment was made (e.g., "MOBILE_MONEY", "CARD"). |
paymentType | string | The type of payment (e.g., "PREPAID"). |
status | string | The status of the transaction (e.g., "COMPLETED"). |
device | object | Details about the device. |
card | object | Details about the card used. |
user | object | The user who checks the account. |
performedBy | object | The user who performed the recharge. |
completedAt | string | Timestamp (ISO 8601) when the transaction was completed. |
createdAt | string | Timestamp (ISO 8601) when the transaction was created. |
zone | object | Details about the zone. |
type | string | The type of transaction. |
Water Fetch Receipt (fetches_log)
This feed provides a receipt every time water is fetched from a device.
Payload Structure
- JSON Payload
- Go Struct
{
"id": "receipt-id-789",
"type": "FETCH",
"user": {
"id": "user-id-def",
"firstName": "Jane",
"lastName": "Doe",
"username": "janedoe",
"phoneNumber": "+254712345678",
"emailAddress": "jane.doe@example.com"
},
"serialNumber": "device-serial-number-123",
"litres": 15.5,
"pricePerLiter": 0.01,
"amount": 0.155,
"createdAt": "2025-12-15T12:00:00Z",
"zone": {
"id": "zone-id-abc",
"name": "Downtown",
"location": "Nairobi",
"currency": "KES",
"pricePerLitre": 1.2,
"supportPhoneNumber": "+254700000000"
},
"device": {
"id": "device-id-789",
"name": "Main Street Meter",
"serialNumber": "device-serial-number-123",
"status": "ACTIVE",
"category": "RESIDENTIAL",
"coordinate": {
"latitude": -1.286389,
"longitude": 36.817223
},
"zone": {
"id": "zone-id-abc",
"name": "Downtown",
"location": "Nairobi",
"currency": "KES",
"pricePerLitre": 1.2,
"supportPhoneNumber": "+254700000000"
}
},
"card": {
"id": "card-id-xyz",
"serialNumber": "card-serial-12345",
"type": "NFC_CARD",
"linkedAt": "2025-12-15T10:00:00Z",
"createdAt": "2025-12-15T10:00:00Z",
"active": true,
"customer": {
"id": "user-id-def",
"firstName": "Jane",
"lastName": "Doe",
"username": "janedoe",
"phoneNumber": "+254712345678",
"emailAddress": "jane.doe@example.com"
}
},
"balance": 1219.0,
"businessId": "business-id-123"
}
type WaterReceiptClientDomain struct {
ID string `json:"id"`
Type string `json:"type"`
User *UserClientDomain `json:"user"`
SerialNumber string `json:"serialNumber"`
Litres float64 `json:"litres"`
LitrePrice float64 `json:"pricePerLiter"`
Amount float64 `json:"amount"`
CreatedAt *time.Time `json:"createdAt"`
Zone *ZoneClientDomain `json:"zone"`
Device *DeviceClientDomain `json:"device"`
Card *CardClientDomain `json:"card"`
Balance float64 `json:"balance"`
BusinessId string `json:"businessId"`
}
Field Descriptions
| Field | Type | Description |
|---|---|---|
id | string | The unique ID for this water fetch receipt. |
type | string | The type of event (e.g., "FETCH"). |
user | object | The user who fetched the water. |
serialNumber | string | The serial number of the device. |
litres | number | The amount of water fetched in litres. |
pricePerLiter | number | The price per litre at the time of fetching. |
amount | number | The total cost of the water fetched. |
createdAt | string | Timestamp (ISO 8601) when the water was fetched. |
zone | object | The zone where the fetch occurred. |
device | object | The device used for fetching. |
card | object | The card used for fetching. |
balance | number | The remaining balance on the account. |
businessId | string | The ID of the business associated with this transaction. |
Handling Webhooks Correctly
Your webhook endpoint should:
- Accept
POSTrequests - Parse JSON payloads
- Process the event idempotently
- Respond within 10 seconds
Example Integration
- Go
func HandleWebhook(c echo.Context) error {
// ... (see api.md for full struct definitions)
return c.NoContent(http.StatusCreated)
}