Connect your booking website, CRM or accounting to DiveConnect with a REST API and signed webhooks. Included with Fleet.
The API is included with Fleet. Only the shop owner or an admin can create, rotate or revoke keys, in Settings → API & webhooks.
Send your key in the Authorization header of every request. A key belongs to one shop and only ever sees that shop's data. Keys are shown once: keep them on your server or in a secret manager, never in a browser or mobile app. A revoked key stops working immediately.
curl https://www.diveconnect.ai/api/v1/bookings?limit=25 \
-H "Authorization: Bearer dck_live_xxxxxxxx_..."Give each key only the scopes it needs. A request outside the key's scopes gets 403 insufficient_scope.
read:bookingswrite:bookingsread:tripsread:customerswebhooksEach key has its own limit: 60 requests per minute by default, up to 100. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. Over the limit you get 429 with a Retry-After header.
Lists return up to limit items (default 25, max 100) with has_more and next_cursor. Pass next_cursor as cursor to get the next page.
{
"object": "list",
"data": [ { "id": "…", "object": "booking", "status": "confirmed", … } ],
"has_more": true,
"next_cursor": "WyJ0c191dWlkIiwi…"
}Errors use HTTP status codes and a JSON body with a stable code: 401 missing or invalid key, 402 the shop's plan doesn't include the API or the subscription isn't active, 403 missing scope, 404 not found in this shop, 409 conflict, 429 rate limited.
{
"error": {
"code": "insufficient_scope",
"message": "This API key does not have the scope this request needs.",
"required_scope": "write:bookings"
}
}Base URL: https://www.diveconnect.ai/api/v1. The full reference, with every field, is in the OpenAPI spec.
| GET | /bookings | read:bookings |
| POST | /bookings | write:bookings |
| GET | /bookings/{id} | read:bookings |
| POST | /bookings/{id}/cancel | write:bookings |
| GET | /trips | read:trips |
| GET | /trips/{id} | read:trips |
| GET | /departures | read:trips |
| GET | /departures/{id} | read:trips |
| GET | /availability | read:trips |
| GET | /customers | read:customers |
| GET | /customers/{id} | read:customers |
| GET | /webhook_endpoints | webhooks |
| POST | /webhook_endpoints | webhooks + read:bookings + read:customers |
| DELETE | /webhook_endpoints/{id} | webhooks |
A booking created through the API works like one entered in the dashboard: DiveConnect computes the price from the trip or departure, checks the seats left, and creates the booking as pending (or confirmed) without taking a payment. Send an Idempotency-Key header so a retried request never creates a second booking.
curl -X POST https://www.diveconnect.ai/api/v1/bookings \
-H "Authorization: Bearer dck_live_xxxxxxxx_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: order-1042" \
-d '{
"departure_id": "6f1c…",
"participants": 2,
"customer": { "name": "Ana Reyes", "email": "ana@example.com" }
}'Add an endpoint (HTTPS, port 443) in the dashboard or through the API and choose its events. We POST a JSON event whenever a booking is created, cancelled or completed, wherever it happened: the dashboard, the app, your booking site, the marketplace or the API.
{
"id": "0b9e…",
"object": "event",
"type": "booking.created",
"api_version": "v1",
"created": 1760400000,
"data": { "object": { "id": "…", "object": "booking", "status": "pending", … } }
}Every request has a DiveConnect-Signature header in the form t=TIMESTAMP,v1=SIGNATURE. Compute HMAC-SHA256 of TIMESTAMP.RAW_BODY with the endpoint's signing secret, compare it in constant time, and reject timestamps more than 5 minutes old.
import crypto from 'node:crypto'
// rawBody: the request body exactly as received (a string, before JSON.parse)
export function verifyDiveConnect(rawBody, header, secret, toleranceSec = 300) {
const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')))
const t = Number(parts.t)
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > toleranceSec) return false
const expected = crypto.createHmac('sha256', secret).update(`${t}.${rawBody}`).digest()
const given = Buffer.from(parts.v1 || '', 'hex')
return given.length === expected.length && crypto.timingSafeEqual(given, expected)
}Answer with any 2xx status within 5 seconds. Otherwise we retry with exponential backoff (about 30 seconds, doubling up to 6 hours), for up to 10 attempts. An event can arrive more than once or out of order, so deduplicate on the event id. An endpoint that keeps failing is paused; resume it in the dashboard.