Webhooks

Gundi · Developer Docs
Webhooks

A Webhook lets an external system - like a tracking device platform, a sensor portal, or any third-party tool - automatically send data to Gundi the moment something happens. Instead of Gundi checking for new data on a schedule, the external system does the work and pushes the data to you in real time.

Endpoint https://hooks.gundiservice.org/webhooks
Format JSON Auth apikey header Delivery real time

What is a Webhook Connection?

Use a Webhook connection when:

  • Your data source supports sending HTTP requests to an external URL
  • You need near real-time data delivery
  • There's no pre-built Gundi integration available for your system

If you're not sure whether your system supports Webhooks, check its documentation or ask your vendor.

Requirements

  • Every data packet must include a timestamp field, preferably in ISO 8601 format, representing when the event was recorded (for example, 2023-07-27T09:34-03:00 or 2023-07-27T09:34Z).
  • Every data packet must include both a latitude field and a longitude field.
  • Movement data must include a device ID or source ID field.

Integration Process Summary

  1. 1 Before starting the integration process
    1.1

    Confirm with the device manufacturer or system developer that the device supports sending data to a Webhook.

    1.2

    Optional, but highly Recommended: Request a sample payload from the device manufacturer or system developer. If the sample is not available, contact our Support team to help you configure this integration.

  2. 2 Prepare a Route in Gundi
    2.1

    Create a Route in Gundi.

    2.2

    Set up your Webhook Schema.

    2.3

    Configure Field Mapping.

    2.4

    Configure Field Mapping Transforms (optional): apply a function to a field to convert its value into the format Gundi expects (e.g. date formatting, unit conversion).

  3. 3 Configure Data Provider
    3.1

    Configure base URL

    3.2

    Configure headers (Gundi Connection API Key and type)

Configuring your Webhook in Gundi

1

Open the webhook configuration

Once your webhook Route has been created, open its Configurations tab and click the data provider node on the flow map. The webhook configuration appears under Webhook → Payload parsing. This section only appears for webhook providers - if you don't see it, please ask our Support Team for clarification.

2

Set up your Webhook Schema

The first step is telling Gundi what your incoming data looks like.

The easiest way to do this is to paste a sample payload (see step 1.2 in the process summary above) - a small example of the data your system sends. Gundi will automatically detect the structure and build the schema for you. You can also use the Schema Builder to define fields manually if you prefer.

What the schema is

The schema is a JSON Schema object that describes the structure of the data your external system will send to Gundi's Webhook endpoint.

It defines:

  • What fields are expected in the incoming payload
  • The type of each field (string, number, boolean, object, array, etc.)
  • Which fields are required vs. optional
  • Optional constraints - formats (date-time, email, uuid), ranges (minimum/maximum), or allowed values (enum)
Why it's used

The schema serves two purposes:

  1. 1 It tells Gundi (and you) exactly what shape of data to expect from the sending system. It can be as vague or granular as you want/need. When Gundi receives a packet, it compares it against the schema and it rejects the packet if it doesn't match it.
  2. 2 The schema also powers the field mapping (transformation) builder. Once you define a schema, Gundi generates a realistic sample payload from it. This sample is what you can use to map incoming fields to Gundi's Observation or Event format when data arrives.
Two ways to build it

The builder lets users either:

  • Build it field-by-field using the visual UI (no JSON needed)
  • Paste a sample JSON and have the schema auto-generated from it
3

Configure Field Mapping

Once the schema is defined, the next configuration step is telling Gundi how to interpret the incoming data.

Choose what type of object to create - an Observation (a location or sensor reading) or an Event (an alert or incident).

Use the Field Mapping UI to tell Gundi what each field means. For example, you might map a field called “y” in your payload to Gundi's “location.lat” field, and “device_id” to Gundi's “source” identifier. The visual builder guides you through this without needing to write any code. Once saved, Gundi will apply this transformation to every incoming message automatically.

Tip: to further reshape data per destination after it enters Gundi (for example, translating event types), use Route Rules.

Note: Some Connections with an existing Webhook configuration may open in Code mode instead of Builder mode. This happens when the configuration uses a JQ filter that Builder mode doesn't yet support. The For Developers section at the end shows what a Code mode filter looks like.

After mapping all the key fields into the fields required by Gundi, and adding any optional data, use the Output Preview section to confirm that everything looks correct. You will see validation errors there if Gundi detects missing or incorrect field mappings.

When ready, click Save.

4

Applying Transforms (Optional)

When a mapped field's raw value doesn't match the format Gundi expects, click the wand button next to the mapped field to apply one or more Transforms. Transforms run in the order they're added and show up as pills under the field.

Type & dates
Function What it does Example
To number Converts text to a number "-27.57" → -27.57
To text Converts a value to text 359441 → "359441"
Epoch → ISO (seconds) Converts a seconds timestamp to an ISO date 1719403200 → "2024-06-26T12:00:00Z"
Epoch → ISO (milliseconds) Converts a milliseconds timestamp to an ISO date 1719403200000 → "2024-06-26T12:00:00Z"
Math & text
Function What it does Example
Divide Divides the value by a number you set -1234567 ÷ 1e6 → -1.234567
Split Splits text into a list on a delimiter "-27.5,31.6" → ["-27.5","31.6"]
Value
Function What it does Example
Default if empty Uses a value you set when the field is missing null → "unknown"
Value map Replaces listed values with new ones "ENTERED" → "ew_entered"
Fallback field Reads another field when this one is empty null → .cached.lat

Use Output Preview to confirm the transformed value before saving.

5

Share API Key

Share the Route’s API key (found by selecting the data provider node on the flow map, under Connect → API key) with the party responsible for configuring the Webhook in the data source.

Configuring Webhook in your Data Source

Follow these steps in your source system (e.g., ChirpStack, The Things Network, Digital Matter):

1

Create the Webhook

  • Name: Any label (e.g., gundi-webhook)
  • Format: JSON (if prompted)
2

Set the endpoint

Base URL
POST https://hooks.gundiservice.org/webhooks
3

Add required headers

Header Value
apikey Your Route’s API key (see step 5 above)
x-gundi-integration-type generic_webhooks. If your system cannot send custom headers, append ?integration_type=generic_webhooks to the URL instead.
What the responses mean
Response Meaning
200 OK Gundi accepted the packet (the body is an empty {}). It is then validated against your schema, mapped, and sent on to your destinations. If something goes wrong after that, the reason shows in the Route's Activity Logs.
400 Bad Request The integration type is missing: add the x-gundi-integration-type header or the ?integration_type query parameter.
401 Unauthorized The apikey header is missing or does not match a Route.
4

Save

Save the configuration in your source system.

Integration Testing

  • Trigger a sample event from your system (or wait for the next transmission).
  • Confirm delivery:
    • The Route's Activity Logs (open the Route, then the Logs tab) show each delivery, or the reason it didn't go through
    • Data is received by the destination set up in your Gundi Connection

If your testing is unsuccessful or any steps are unclear, please contact our team for further guidance or troubleshooting.

For Developers

Not a developer? You can skip this section. Everything you need to set up a webhook is above.

This section gives the technical details: a test request, a full worked example, the Gundi fields you can map to, and how to send many records at once.

Send a test request

The body can be any JSON your system sends. The method, URL and headers are always the same. Replace the body below with a real payload from your own system to test your Route before the data source is set up:

curl -X POST "https://hooks.gundiservice.org/webhooks" \
  -H "Content-Type: application/json" \
  -H "apikey: <your Route API key>" \
  -H "x-gundi-integration-type: generic_webhooks" \
  -d '{"device_id": "collar-0042", "ts": 1719403200, "y": -27.5712, "x": 31.6044, "battery": 87}'

The reply is 200 with the body {}. A 200 only means Gundi received the request. Gundi checks it against your schema and maps it afterwards, so a payload that doesn't match shows up in the Route's Activity Logs, not in the reply.

Example: from payload to EarthRanger

1. The payload your system sends
{
  "device_id": "collar-0042",
  "ts": 1719403200,
  "y": -27.5712,
  "x": 31.6044,
  "battery": 87
}
2. The schema Gundi generates from it
{
  "type": "object",
  "properties": {
    "device_id": { "type": "string" },
    "ts": { "type": "integer" },
    "y": { "type": "number" },
    "x": { "type": "number" },
    "battery": { "type": "integer" }
  }
}
3. The field mapping
Payload field Gundi field Transform
device_id source None
ts recorded_at Epoch → ISO (seconds)
y location.lat None
x location.lon None
battery additional.battery None
4. The same mapping in Code mode (JQ)
{
  source: .device_id,
  recorded_at: (.ts | (tonumber | todate)),
  location: { lat: .y, lon: .x },
  "additional": {
    "battery": .battery
  }
}
5. What Gundi creates
{
  "source": "collar-0042",
  "recorded_at": "2024-06-26T12:00:00Z",
  "location": { "lat": -27.5712, "lon": 31.6044 },
  "additional": { "battery": 87 }
}

Gundi fields you can map to

Observations
Field What it holds
source
string required
The device ID
recorded_at
string required
When the reading was taken, in ISO 8601
location.lat
number required
Latitude
location.lon
number required
Longitude
type
string optional
Observation type, for example tracking-device
source_name
string optional
A readable name for the device
subject_type
string optional
What is being tracked, for example giraffe
additional
object optional
Any extra values, such as battery level
annotations
object optional
Annotation metadata
Events
Field What it holds
event_type
string required
The event type, for example animal_sighting. It has to match an event type that exists on the EarthRanger site.
recorded_at
string required
When the event happened, in ISO 8601
location.lat
number required
Latitude
location.lon
number required
Longitude
title
string optional
The event title
event_details
object optional
The values for the event type's form fields
geometry
object optional
A GeoJSON polygon
status
string optional
The event status
annotations
object optional
Annotation metadata

Sending many records in one request

A list of records

The body can be one JSON object or a JSON array of objects. With an array, Gundi checks each item against your schema. If any item doesn't match, Gundi turns away the whole request, and the reason shows in Activity Logs.

[
  { "device_id": "collar-0042", "ts": 1719403200, "y": -27.5712, "x": 31.6044, "battery": 87 },
  { "device_id": "collar-0043", "ts": 1719403260, "y": -27.6105, "x": 31.5528, "battery": 64 }
]

The JQ filter receives the whole array, so it has to start with .[] | to map each item. Add it in Code mode in front of the filter from the example above:

.[] | {
  source: .device_id,
  recorded_at: (.ts | (tonumber | todate)),
  location: { lat: .y, lon: .x },
  "additional": { "battery": .battery }
}
A list inside one record

One object can also become many records. In Code mode, a filter like .readings[] | {...} turns each entry of a list inside the payload into its own record. For this payload:

{
  "device_id": "collar-0042",
  "readings": [
    { "ts": 1719403200, "y": -27.5712, "x": 31.6044 },
    { "ts": 1719406800, "y": -27.5698, "x": 31.6071 }
  ]
}

this filter creates two observations:

.device_id as $id | .readings[] | {
  source: $id,
  recorded_at: (.ts | todate),
  location: { lat: .y, lon: .x }
}
Observations and events in one request

The Route has a default output type (Observation or Event). A record can override it with "__gundi_output_type": "obv" (observation) or "__gundi_output_type": "ev" (event) in the mapped output. Gundi removes that field before sending the record on.

Need a hand?

Not sure whether your system supports Webhooks, or stuck on the schema or field mapping? Our team can help.

Contact Gundi Support

Last update: Nov 5, 2026