Developer Docs
Place and track drone-survey orders programmatically from your own systems. Authenticate with an API key, call the same order flow the Globhe platform uses, and pull deliverables when they are ready.
Base URL
https://api.globhe.com/api/v1/publicThe Public API exposes Globhe's order flow to your integration. A single API key represents one client account: every order you create, and every order, mission, and deliverable you can read, is scoped to that account automatically. You never pass a client identifier — it is derived from the key.
All requests are made over HTTPS to the base URL above. Request and response bodies are JSON.
Send your key as a bearer token on every request. An x-api-key header is also accepted.
Authorization header
Authorization: Bearer glb_live_ab12cd34_9f8e...
Keys carry scopes that limit what they can do. A call missing the required scope returns 403.
| Scope | Grants |
|---|---|
orders:read | List and read orders, missions, planning. |
orders:write | Create orders and cancel / archive / reject them. |
files:read | List delivery packages and get signed download URLs. |
webhooks:read | List webhook endpoints and read their delivery logs. |
webhooks:write | Register and delete webhook endpoints. |
API keys are issued on approval. From your Globhe dashboard, submit a request describing your intended use. A Globhe admin reviews it, and on approval the key is shown to you exactly once — copy it immediately and store it securely. If it is lost, revoke it and request a new one.
Keys can be revoked at any time from the dashboard and stop working immediately.
Every response uses the same envelope:
Response envelope
{
"success": true,
"message": "Order created successfully",
"data": { /* endpoint payload */ }
}Errors use the same shape with the relevant HTTP status:
| Status | Meaning |
|---|---|
400 | Validation failed, or an unknown field was sent. |
401 | Missing, malformed, expired, or revoked key. |
403 | Key lacks the required scope, or the resource isn't yours. |
404 | Resource not found. |
A typical integration is five steps. Register a webhook first so you never miss an update, create your order, then react to events as the mission progresses — no polling required. The examples use fetch in Node, but any HTTP client works.
Setup (shared by the calling steps)
const BASE = 'https://api.globhe.com/api/v1/public';
const KEY = process.env.GLOBHE_API_KEY;
const headers = {
Authorization: `Bearer ${KEY}`,
'Content-Type': 'application/json',
};Point Globhe at your callback URL and store the returned secret — it is shown only once and is used to verify every event. Do this before creating orders so you catch the earliest events.
Node
const res = await fetch(`${BASE}/webhooks`, {
method: 'POST',
headers,
body: JSON.stringify({ url: 'https://example.com/hooks/globhe' }),
});
const { data } = await res.json();
// Persist data.secret securely — you cannot fetch it again.
saveSecret(data.secret); // e.g. "whsec_..."Have a survey file? Parse it to get ready-made locationMap arrays you can drop straight into the order. Skip this if you already have coordinates.
Node
const form = new FormData();
form.append('file', kmlBlob, 'survey-area.kml');
const res = await fetch(`${BASE}/kml/parse`, {
method: 'POST',
headers: { Authorization: `Bearer ${KEY}` }, // let fetch set multipart Content-Type
body: form,
});
const { data } = await res.json();
const placemarks = data.placemarks; // -> use as your missions' locationMap/labelsSubmit the order and its missions. You get back an orderCodeand the created missions — hang on to each mission's identifiers.
Node
const res = await fetch(`${BASE}/orders`, {
method: 'POST',
headers,
body: JSON.stringify({
order: {
companyName: 'Acme Surveying',
orderName: 'North Field Survey',
orderType: 'singleLocation',
sensors: ['rgbSensor'],
deliverables: ['jpg', '_2dMap'],
missionObjective: 'mapAndSurvey',
industryAssetType: 'agriculture',
startDate: '2026-08-01T00:00:00.000Z',
deadline: '2026-08-15T00:00:00.000Z',
},
missions: [
{
missionName: 'Field A',
missionType: 'area',
country: 'Sweden',
locationLabel: 'Uppsala North Field',
areaSize: 42.5,
locationMap: [
{ lat: 59.858, lng: 17.645 },
{ lat: 59.860, lng: 17.650 },
],
},
],
}),
});
const { data: order } = await res.json();
console.log(order.orderCode); // "ORD-000123"As the mission moves forward you receive callbacks — typically mission.operator_assigned → mission.flight_scheduled → mission.data_delivered → mission.completed. Verify the signature against the raw body, respond 2xx quickly, and de-duplicate on id.
Express handler
import express from 'express';
import { createHmac, timingSafeEqual } from 'crypto';
const app = express();
function isValid(rawBody, header, secret) {
const expected = 'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}
app.post('/hooks/globhe', express.raw({ type: 'application/json' }), (req, res) => {
const raw = req.body.toString('utf8');
if (!isValid(raw, req.header('X-Globhe-Signature'), loadSecret())) {
return res.status(401).end();
}
res.status(200).end(); // ack fast
const evt = JSON.parse(raw);
if (alreadyProcessed(evt.id)) return; // idempotent on delivery id
switch (evt.event) {
case 'mission.data_delivered':
case 'mission.completed':
void downloadDeliverables(evt.data.mission.uuid); // step 5
break;
}
});On mission.data_delivered (or mission.completed), list the mission's delivery packages, then each package's files — every file carries a signed downloadUrl valid for 24 hours.
Node
async function downloadDeliverables(missionUid) {
const pkgRes = await fetch(`${BASE}/missions/${missionUid}/delivery-packages`, { headers });
const { data: packages } = await pkgRes.json();
for (const pkg of packages.data) {
const fileRes = await fetch(`${BASE}/delivery-packages/${pkg.uuid}/files`, { headers });
const { data: files } = await fileRes.json();
for (const file of files.data) {
await download(file.downloadUrl, file.fileName); // 24h signed URL
}
}
}That's the whole loop. This flow uses the webhooks:write, orders:write, orders:read, and files:read scopes — make sure your key carries them.
Have a KML or CSV of your survey area? Upload it and get back ready-to-use mission locations, so you don't have to hand-build coordinate arrays. Each placemark / row returns a locationMap (lat/lng pairs) plus a suggested locationLabel, country, and city — drop each one straight into the missions array when creating an order. The file is parsed in memory and never stored.
/kml/parsescope: orders:writeSend the KML as multipart form field file(max 10 MB).
Request
curl -X POST https://api.globhe.com/api/v1/public/kml/parse \ -H "Authorization: Bearer glb_live_xxx" \ -F "file=@survey-area.kml"
Response
{
"success": true,
"message": "KML parsed successfully",
"data": {
"placemarks": [
{
"locationLabel": "North Field",
"country": "Sweden",
"city": "Uppsala",
"locationMap": [
{ "lat": 59.858, "lng": 17.645 },
{ "lat": 59.860, "lng": 17.650 }
]
}
]
}
}/csv/parsescope: orders:writeSame as above, for a CSV of sites — send it as multipart field file. The response shape is identical (a placemarks array). Use the template below to see the expected columns.
Request
curl -X POST https://api.globhe.com/api/v1/public/csv/parse \ -H "Authorization: Bearer glb_live_xxx" \ -F "file=@sites.csv"
/csv/examplescope: orders:writeReturns a sample CSV template showing the columns the parser expects, so you can format your file correctly.
/ordersscope: orders:writeCreate an order with one or more missions. The order is attached to your account automatically. Required order fields: companyName, orderName, orderType (singleLocation or multiLocation), sensors, deliverables, missionObjective, industryAssetType, startDate, deadline. Each mission needs a locationMap of lat/lng points, country, missionType, locationLabel, areaSize, and missionName.
Request
curl -X POST https://api.globhe.com/api/v1/public/orders \
-H "Authorization: Bearer glb_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"order": {
"companyName": "Acme Surveying",
"orderName": "North Field Survey",
"orderType": "singleLocation",
"sensors": ["rgbSensor"],
"deliverables": ["jpg", "_2dMap"],
"missionObjective": "mapAndSurvey",
"industryAssetType": "agriculture",
"startDate": "2026-08-01T00:00:00.000Z",
"deadline": "2026-08-15T00:00:00.000Z"
},
"missions": [
{
"missionName": "Field A",
"missionType": "area",
"country": "Sweden",
"locationLabel": "Uppsala North Field",
"areaSize": 42.5,
"locationMap": [
{ "lat": 59.858, "lng": 17.645 },
{ "lat": 59.860, "lng": 17.650 }
]
}
]
}'Response
{
"success": true,
"message": "Order created successfully",
"data": {
"uuid": "b6f1...",
"orderCode": "ORD-000123",
"orderName": "North Field Survey",
"status": "biddingWithQuote",
"orderType": "singleLocation",
"startDate": "2026-08-01T00:00:00.000Z",
"deadline": "2026-08-15T00:00:00.000Z",
"createdAt": "2026-07-06T10:00:00.000Z"
}
}Accepted values
These fields are validated against fixed sets — sending any other value returns 400. sensors and deliverables accept multiple values, and jpg is always included in deliverables.
| Field | Accepted values |
|---|---|
orderType | singleLocation, multiLocation |
missionType (per mission) | area, linear, asset |
sensors | rgbSensor, thermalSensor, multispectralSensor, hyperspectralSensor, lidarSensor, groundCapture |
deliverables | jpg, _2dMap, _3dMap, _3dModel, digitalTerrainModel, digitalSurfaceModel, topographicMap, thermalMap, ndvi, multispectral, hyperspectral, streamedData, raw, panoramas, LiDAR, pointCloud |
missionObjective | mapAndSurvey, assetInspection, siteProgress, visualsMarketing |
industryAssetType | telecom, solarPanels, windTurbines, powerLines, construction, forest, agriculture, mining, railways, roadsBridges, oilGas, ports, realEstateProperty, waterManagement, other |
/ordersscope: orders:readList your orders, most recent first. Cursor-paginated via limit, cursor, and direction query parameters.
Request
curl "https://api.globhe.com/api/v1/public/orders?limit=20" \ -H "Authorization: Bearer glb_live_xxx"
/orders/:codescope: orders:readFetch a single order by its code (e.g. ORD-000123), including its missions.
/orders/:code/missionsscope: orders:readList the missions of an order. Use each mission's uuid for the mission-resource endpoints below.
/orders/:uuid/cancelscope: orders:writeCancel an order you placed.
/orders/:uuid/archivescope: orders:writeArchive an order. Send a reason in the body.
Body
{ "reason": "Duplicate request" }/orders/:uuid/rejectscope: orders:writeReject an order. Send a reason in the body.
These are keyed by a mission's uuid, which you get from the order detail or missions list.
/missions/:missionUid/planningscope: orders:readFull planning history for a mission (scheduling status, version, and who scheduled it).
/missions/:missionUid/flight-permissionscope: orders:readThe flight permission for a mission. When a permit document exists, a time-limited permitFileUrl is included — the raw storage path is never exposed.
/missions/:missionUid/delivery-packagesscope: files:readThe delivered data packages for a mission.
/delivery-packages/:deliveryPackageUid/filesscope: files:readList the files in a delivery package. Each file comes with a downloadUrl — a signed link valid for 24 hours. Fetch fresh URLs when they expire; do not cache them long-term.
Response
{
"success": true,
"message": "Files fetched successfully",
"data": {
"data": [
{
"uuid": "f12a...",
"fileName": "orthomosaic.tif",
"fileType": "image/tiff",
"fileDataType": "orthomosaic",
"fileSize": 184320000,
"status": "delivered",
"downloadUrl": "https://storage.googleapis.com/...signed...(24h)"
}
],
"nextCursor": null,
"hasNextPage": false
}
}Instead of polling, subscribe a URL to receive an HMAC-signed callback the moment a mission hits a key milestone. Events are scoped to your account — you only receive callbacks for your own missions.
Events
| Event | Fires when |
|---|---|
mission.operator_assigned | An operator is assigned to the mission. |
mission.flight_scheduled | A flying date is confirmed. |
mission.data_delivered | Captured data passes Globhe review and is released to you. |
mission.completed | You accept the delivered data — the mission is done. |
/webhooksscope: webhooks:writeRegister an endpoint. url must be https. Pass events to subscribe to specific events, or omit it to receive all of them. The response includes the signing secret exactly once — store it securely; it is never shown again.
Request
curl -X POST https://api.globhe.com/api/v1/public/webhooks \
-H "Authorization: Bearer glb_live_xxx" \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/hooks/globhe",
"events": ["mission.operator_assigned", "mission.completed"],
"description": "Prod integration"
}'Response
{
"success": true,
"message": "Webhook endpoint created successfully",
"data": {
"uuid": "a1b2...",
"url": "https://example.com/hooks/globhe",
"events": ["mission.operator_assigned", "mission.completed"],
"description": "Prod integration",
"status": "active",
"lastStatus": null,
"secret": "whsec_...",
"createdAt": "2026-07-07T09:00:00.000Z",
"updatedAt": "2026-07-07T09:00:00.000Z"
}
}/webhooksscope: webhooks:readList your endpoints. Cursor-paginated via limit, cursor, and direction. The secret is never returned here.
/webhooks/:uuidscope: webhooks:readFetch a single endpoint by its uuid.
/webhooks/:uuid/deliveriesscope: webhooks:readThe delivery log for an endpoint — one row per event sent, with the outcome so you can spot failures.
Response
{
"success": true,
"message": "Webhook deliveries fetched successfully",
"data": {
"data": [
{
"uuid": "d4e5...",
"event": "mission.completed",
"success": true,
"responseCode": 200,
"attempts": 1,
"error": null,
"createdAt": "2026-07-07T09:05:00.000Z"
}
],
"nextCursor": null,
"hasNextPage": false
}
}/webhooks/:uuidscope: webhooks:writeDelete an endpoint. Deliveries to it stop immediately.
Event payload
Every callback is a POST with this JSON body:
POST body
{
"id": "evt_delivery_id",
"event": "mission.operator_assigned",
"timestamp": "2026-07-07T09:05:00.000Z",
"data": {
"mission": {
"uuid": "…",
"missionCode": "MSN-000123",
"status": "pilotAssigned",
"orderUid": "…",
"orderCode": "ORD-000045"
}
}
}Request headers
| Header | Value |
|---|---|
X-Globhe-Signature | sha256=<hex> — HMAC-SHA256 of the raw body. |
X-Globhe-Event | The event name. |
X-Globhe-Delivery | The delivery id (matches id in the body). |
X-Globhe-Timestamp | ISO-8601 send time. |
Verifying the signature
Recompute the HMAC over the raw request body with your endpoint secret and compare it, in constant time, against the X-Globhe-Signature header. Reject the request if it does not match.
Node.js
import { createHmac, timingSafeEqual } from 'crypto';
function isValidGlobheSignature(rawBody, header, secret) {
const expected =
'sha256=' + createHmac('sha256', secret).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(header ?? '');
return a.length === b.length && timingSafeEqual(a, b);
}Delivery & retries
Respond with any 2xx within ~5 seconds. On failure Globhe retries up to 3 times with a short backoff; delivery is best-effort (there is no durable queue), so treat the API as the source of truth and use the deliveries log to spot gaps. Retries reuse the same delivery id — make your handler idempotent on it. Ordering is not guaranteed; rely on status in the payload.