Get started
First integration in 15 minutes
Get a sandbox token, create a member, redeem a voucher and receive the signed voucher.redeemed webhook, step by step.
This walkthrough takes you from nothing to a working round trip in the sandbox: get an access token, create a member, give the member a voucher, redeem it at a branch and receive the signed voucher.redeemed webhook. Every call is a copy-and-paste curl command.
Before you start
From your Clive partnerships contact (Sandbox):
- a sandbox API client (client ID and secret) with the scopes
members:write,venues:read,vouchers:read,vouchers:writeandwebhooks:manage; - your inbound URL and inbound signing secret for your provider code. The clinic admin finds both under Settings › Integrations. This walkthrough uses
partner_v1, the provider code of the bare Partner contract v1; use yours wherever it appears.
On your machine: curl, jq, openssl, uuidgen (any unique string works as an idempotency key), Node.js 18 or newer, and a way to expose a local port over HTTPS, such as ngrok http 3000 or cloudflared tunnel --url http://localhost:3000.
Keep one terminal for the commands below and set these once:
export API=https://sandbox-api.clivehealth.app
export CLIENT_ID=cli_… # your sandbox client ID
export CLIENT_SECRET=sk_… # your sandbox client secret
export INBOUND_URL=https://sandbox-api.clivehealth.app/webhooks/inbound/partner_v1/… # from Settings › Integrations
export INBOUND_SECRET=whsec_… # the inbound signing secret1. Get an access token (1 minute)
export TOKEN=$(curl -sS -X POST "$API/oauth/token" -u "$CLIENT_ID:$CLIENT_SECRET" \
-d grant_type=client_credentials | jq -r .access_token)
curl -sS "$API/public/v1/me" -H "Authorization: Bearer $TOKEN"The second command prints your client ID, your clinic ID and your scopes. If it prints INVALID_TOKEN, check the client ID and secret. The token lasts an hour; run the first command again when it expires. More in Getting started.
2. Start a webhook receiver (4 minutes)
Clive delivers webhooks to a public HTTPS URL. Start your tunnel to port 3000 in a second terminal and note its https:// URL:
ngrok http 3000Save this receiver as receiver.mjs. It has no dependencies. It checks the X-Clive-Signature header exactly as described in Verifying signatures and prints each event:
import crypto from 'node:crypto'
import http from 'node:http'
const SECRET = process.env.CLIVE_WEBHOOK_SECRET // whsec_…
const TOLERANCE_SECONDS = 300
function verify(rawBody, header) {
if (!header) return false
const parts = header.split(',').map((p) => p.trim().split('='))
const t = Number(parts.find(([k]) => k === 't')?.[1])
const sigs = parts.filter(([k]) => k === 'v1').map(([, v]) => v)
if (!Number.isInteger(t) || sigs.length === 0) return false
if (Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false
const expected = crypto.createHmac('sha256', SECRET).update(`${t}.${rawBody}`).digest('hex')
return sigs.some((s) => s.length === expected.length &&
crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)))
}
http.createServer((req, res) => {
const chunks = []
req.on('data', (chunk) => chunks.push(chunk))
req.on('end', () => {
const raw = Buffer.concat(chunks).toString('utf8')
if (!verify(raw, req.headers['x-clive-signature'])) {
console.log('Rejected a request with a missing or invalid signature')
res.writeHead(400).end()
return
}
const event = JSON.parse(raw)
console.log(`${event.type} (event ${event.id})`)
console.log(JSON.stringify(event.data, null, 2))
res.writeHead(204).end()
})
}).listen(3000, () => console.log('Listening on port 3000'))Register the tunnel URL as a webhook endpoint for voucher.redeemed:
export RECEIVER_URL=https://….ngrok-free.app # your tunnel's HTTPS URL
curl -sS -X POST "$API/public/v1/webhooks/endpoints" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data @- <<EOF | tee endpoint.json
{
"url": "$RECEIVER_URL",
"description": "Quickstart receiver",
"event_types": ["voucher.redeemed"]
}
EOF
export ENDPOINT_ID=$(jq -r .id endpoint.json)The response includes the endpoint's signing secret (whsec_…). It is shown only once; the commands above keep it in endpoint.json. Start the receiver with that secret in a third terminal, in the same directory:
CLIVE_WEBHOOK_SECRET=$(jq -r .secret endpoint.json) node receiver.mjsCheck the whole path with a test event. The receiver prints webhook.test within a few seconds:
curl -sS -X POST "$API/public/v1/webhooks/endpoints/$ENDPOINT_ID/test" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)"3. Create a member (2 minutes)
Members are keyed on your provider code and your own member id, so this call creates the member the first time and updates it on every later call:
export EXTERNAL_ID=QS-$(date +%s)
curl -sS -X POST "$API/public/v1/members" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data @- <<EOF | tee member.json
{
"provider": "partner_v1",
"external_id": "$EXTERNAL_ID",
"membership_no": "$EXTERNAL_ID",
"full_name": "Ujian Quickstart",
"email": "quickstart@example.com",
"date_of_birth": "1990-01-01"
}
EOF
export MEMBER_ID=$(jq -r .id member.json)The answer is 201 Created with the member, including its Clive id and your id under memberships. Send the same request again and you get the stored response back with Idempotent-Replayed: true: that is what makes retries safe (Pagination and idempotency).
4. Give the member a voucher (3 minutes)
Vouchers come from the partner platform. When one of your members buys a package, your platform sends Clive a signed voucher.purchased event (Partner contract v1). Send one now, for the member from step 3:
export VOUCHER_CODE=QS$(date +%s)
BODY=$(cat <<EOF
{"id":"evt-$VOUCHER_CODE","type":"voucher.purchased","occurred_at":"$(date -u +%Y-%m-%dT%H:%M:%SZ)","data":{"voucher_id":"V-$VOUCHER_CODE","code":"$VOUCHER_CODE","member":{"member_id":"$EXTERNAL_ID"}}}
EOF
)
T=$(date +%s)
SIG=$(printf '%s.%s' "$T" "$BODY" | openssl dgst -sha256 -hmac "$INBOUND_SECRET" | sed 's/^.*= //')
curl -sS -X POST "$INBOUND_URL" \
-H "Content-Type: application/json" \
-H "X-Partner-Signature: t=$T,v1=$SIG" \
--data-binary "$BODY"The answer is 202 with { "received": 1, "duplicates": 0, … }: Clive has stored the event and applies it within a few seconds. The signature is the same scheme Clive uses for its own webhooks: an HMAC-SHA256 of "<timestamp>.<raw body>". A 401 means the secret or your clock is wrong.
Check that the voucher has arrived and is issued:
curl -sS -G "$API/public/v1/vouchers" \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode "member_id=$MEMBER_ID" | jq '.data[] | { id, code, status }'If the list is empty, wait a few seconds and ask again.
5. Redeem the voucher (2 minutes)
A voucher is redeemed at a branch (venue). Take the first one:
export VENUE_ID=$(curl -sS "$API/public/v1/venues" -H "Authorization: Bearer $TOKEN" | jq -r '.data[0].id')Redeem the voucher there:
curl -sS -X POST "$API/public/v1/vouchers/redeem" \
-H "Authorization: Bearer $TOKEN" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
--data @- <<EOF
{
"code": "$VOUCHER_CODE",
"venue_id": "$VENUE_ID"
}
EOFThe answer is 201 with the voucher, now redeemed, and its redemption. A voucher is redeemed only once: run the command again with a new Idempotency-Key and you get 409 VOUCHER_ALREADY_REDEEMED, with details of the first redemption.
6. Receive the webhook (1 minute)
Within a few seconds the receiver prints the event:
voucher.redeemed (event 8f6c2d4e-…)
{
"voucher_id": "3f6b8d0a-…",
"external_id": "V-QS1792807209",
"member": { "id": "c41f7a52-…", "membership_no": "QS-1792807209" },
"appointment_id": null,
"venue_id": "5a1d9e2b-…",
"redeemed_at": "2026-11-02T01:02:41.517Z"
}That is the whole loop. If nothing arrives, the delivery log says why (the HTTP status your receiver answered, or the connection error):
curl -sS -G "$API/public/v1/webhooks/deliveries" \
-H "Authorization: Bearer $TOKEN" \
--data-urlencode "event_type=voucher.redeemed" | jq '.data[] | { status, response_status, error }'Failed deliveries are retried for about 24 hours, so once your receiver is fixed the event still arrives.
Next steps
- Webhooks: every event, retries, replays and secret rotation.
- API reference: all operations, including appointments and availability.
- Partner contract v1: the other events your platform can send, and the feeds Clive reads.
- Consent: which data needs which consent from the member.