cURL e terminale
Smoke test end-to-end da terminale: autenticazione, scritture idempotenti, header di rate limit e verifica firma con openssl.
Prima di scrivere un workflow complesso, verifica il contratto dal terminale. Questa pagina contiene i comandi essenziali per validare credenziali, scritture, limiti di frequenza e firma HMAC.
Prima di iniziare
- curl 8.x, jq, openssl e uuidgen disponibili nel PATH
- La chiave API bp_live_xxxxxxxxxxxxxxxx
- Il segreto whsec_xxxxxxxxxxxxxxxx
- Una shell bash o zsh
Preparazione dell'ambiente
export BP_API_BASE_URL="https://api.blupantheon.com"
export BP_API_KEY="bp_live_xxxxxxxxxxxxxxxx"
export BP_PROJECT_ID="bp_proj_7f3a9c21"
export BP_WEBHOOK_SECRET="whsec_xxxxxxxxxxxxxxxx"Cronologia della shell
Esportare il segreto in una variabile lo salva nella cronologia del terminale. Anteponi uno
spazio al comando (in bash con HISTCONTROL=ignorespace) oppure caricalo da file con
source .env.local e aggiungi quel file a .gitignore.
1. Verificare l'autenticazione
curl -sS "$BP_API_BASE_URL/identity/v1/me" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "Accept: application/json" | jqRisposta attesa:
{
"data": {
"tenantId": "bp_ten_4a91c0e5",
"tenantName": "ACME S.p.A.",
"keyPrefix": "bp_live_",
"keyLast4": "9f2c",
"scopes": [
"identity:sessions:read",
"identity:sessions:write",
"identity:clients:read",
"identity:clients:write",
"identity:bookings:read"
],
"expiresAt": "2027-09-04T00:00:00.000Z"
}
}| Codice ricevuto | Significato |
|---|---|
200 | Chiave valida. Controlla scopes e expiresAt. |
401 | Chiave errata, scaduta o revocata. |
403 | Chiave valida ma senza permessi sull'endpoint. |
2. Elencare le risorse
curl -sS -G "$BP_API_BASE_URL/identity/v1/clients" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "X-BP-Project: $BP_PROJECT_ID" \
--data-urlencode "limit=5" \
--data-urlencode "sort=-createdAt" | jq '.data[] | {id, displayName, email}'Paginazione con cursore:
NEXT_CURSOR="$(curl -sS -G "$BP_API_BASE_URL/identity/v1/clients" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "X-BP-Project: $BP_PROJECT_ID" \
--data-urlencode "limit=50" | jq -r '.pagination.nextCursor // empty')"
if [ -n "$NEXT_CURSOR" ]; then
curl -sS -G "$BP_API_BASE_URL/identity/v1/clients" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "X-BP-Project: $BP_PROJECT_ID" \
--data-urlencode "limit=50" \
--data-urlencode "cursor=$NEXT_CURSOR" | jq '.data | length'
fi3. Creare una risorsa con Idempotency-Key
IDEMPOTENCY_KEY="$(uuidgen | tr 'A-Z' 'a-z')"
curl -sS -X POST "$BP_API_BASE_URL/identity/v1/clients" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "X-BP-Project: $BP_PROJECT_ID" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
--data-raw '{
"type": "azienda",
"displayName": "ACME Impianti S.r.l.",
"legalName": "ACME Impianti S.r.l.",
"vatNumber": "IT01234567890",
"email": "amministrazione@acme.it",
"phone": "+39 02 1234567",
"address": {
"street": "Via Torino 42",
"city": "Milano",
"province": "MI",
"postalCode": "20123",
"country": "IT"
}
}' | jqRipeti lo stesso comando con la stessa Idempotency-Key: ottieni la stessa risposta e
nessun cliente duplicato.
# Seconda esecuzione: stesso risultato, nessuna creazione
curl -sS -o /tmp/bp-second.json -w "HTTP %{http_code}\n" \
-X POST "$BP_API_BASE_URL/identity/v1/clients" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "X-BP-Project: $BP_PROJECT_ID" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $IDEMPOTENCY_KEY" \
--data-raw '{"type":"azienda","displayName":"ACME Impianti S.r.l.","vatNumber":"IT01234567890"}'
jq -r '.data.id' /tmp/bp-second.jsonDurata della chiave di idempotenza
Le chiavi di idempotenza sono valide 24 ore. Oltre quel periodo la stessa chiave produce
una nuova risorsa. Genera chiavi derivate da un identificativo stabile del tuo processo, ad
esempio X-BP-Delivery.
4. Ispezionare gli header di rate limit
curl -sS -D - -o /dev/null \
"$BP_API_BASE_URL/identity/v1/clients?limit=1" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "X-BP-Project: $BP_PROJECT_ID" \
| grep -i "x-ratelimit\|retry-after"Output tipico:
x-ratelimit-limit: 600
x-ratelimit-remaining: 587
x-ratelimit-reset: 1767225660| Header | Significato |
|---|---|
X-RateLimit-Limit | Richieste consentite nella finestra corrente |
X-RateLimit-Remaining | Richieste ancora disponibili |
X-RateLimit-Reset | Istante Unix di azzeramento del contatore |
Retry-After | Presente solo con 429. Secondi da attendere. |
Script che rispetta il limite:
#!/usr/bin/env bash
set -euo pipefail
call_api() {
local path="$1"
local headers_file
headers_file="$(mktemp)"
local status
status="$(curl -sS -D "$headers_file" -o /tmp/bp-body.json -w "%{http_code}" \
"$BP_API_BASE_URL$path" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "X-BP-Project: $BP_PROJECT_ID")"
if [ "$status" = "429" ]; then
local wait_seconds
wait_seconds="$(grep -i '^retry-after:' "$headers_file" | tr -d '\r' | awk '{print $2}')"
echo "Limite raggiunto, attendo ${wait_seconds:-30}s" >&2
sleep "${wait_seconds:-30}"
rm -f "$headers_file"
call_api "$path"
return
fi
local remaining
remaining="$(grep -i '^x-ratelimit-remaining:' "$headers_file" | tr -d '\r' | awk '{print $2}')"
if [ -n "$remaining" ] && [ "$remaining" -lt 20 ]; then
echo "Attenzione: solo $remaining richieste residue" >&2
fi
rm -f "$headers_file"
cat /tmp/bp-body.json
}
call_api "/identity/v1/clients?limit=5" | jq '.data | length'5. Inviare un webhook di prova
Seleziona l'endpoint, scegli l'evento webhook.test e clicca Invia. Il pannello mostra
subito codice di risposta, tempo di risposta e corpo restituito dal tuo endpoint.
Puoi anche scatenare il test via API:
curl -sS -X POST \
"$BP_API_BASE_URL/identity/v1/webhooks/wh_5f2a91c0/test" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "X-BP-Project: $BP_PROJECT_ID" \
-H "Content-Type: application/json" \
--data-raw '{"event": "webhook.test"}' | jq6. Calcolare una firma con openssl
TIMESTAMP="$(date +%s)"
BODY='{"id":"evt_test","event":"webhook.test","apiVersion":"2026-09-01","createdAt":"2026-09-04T09:00:00.000Z","tenantId":"bp_ten_4a91c0e5","projectId":"bp_proj_7f3a9c21","data":{}}'
SIGNATURE="$(printf '%s.%s' "$TIMESTAMP" "$BODY" \
| openssl dgst -sha256 -hmac "$BP_WEBHOOK_SECRET" -r \
| cut -d' ' -f1)"
echo "X-BP-Signature: t=$TIMESTAMP,v1=$SIGNATURE"Usa printf, non echo
echo aggiunge un ritorno a capo finale che altera la stringa firmata e produce una firma
diversa. Usa sempre printf senza \n.
7. Inviare un webhook firmato al tuo endpoint
#!/usr/bin/env bash
set -euo pipefail
ENDPOINT="${1:-http://localhost:3000/webhooks/blupantheon}"
TIMESTAMP="$(date +%s)"
DELIVERY_ID="$(uuidgen | tr 'A-Z' 'a-z')"
BODY="$(cat <<'JSON'
{"id":"evt_01J9X5B0PZQ4T1N7C3H8M2AKE","event":"session.handover.requested","apiVersion":"2026-09-01","createdAt":"2026-09-04T09:26:31.884Z","tenantId":"bp_ten_4a91c0e5","projectId":"bp_proj_7f3a9c21","data":{"sessionId":"bp_sess_3d81f0aa","reason":"richiesta_esplicita","priority":"alta","queue":"assistenza-tecnica","client":{"id":"bp_cli_82ae4f10","displayName":"Mario Rossi","email":"mario.rossi@example.it"},"summary":"Il cliente chiede un intervento tecnico.","traceId":"trc_6b19e0d4c8f24a7f"}}
JSON
)"
SIGNATURE="$(printf '%s.%s' "$TIMESTAMP" "$BODY" \
| openssl dgst -sha256 -hmac "$BP_WEBHOOK_SECRET" -r \
| cut -d' ' -f1)"
curl -sS -i -X POST "$ENDPOINT" \
-H "Content-Type: application/json; charset=utf-8" \
-H "User-Agent: BluPantheon-Webhooks/1.0" \
-H "X-BP-Event: session.handover.requested" \
-H "X-BP-Delivery: $DELIVERY_ID" \
-H "X-BP-Timestamp: $TIMESTAMP" \
-H "X-BP-Attempt: 1" \
-H "X-BP-Api-Version: 2026-09-01" \
-H "X-BP-Signature: t=$TIMESTAMP,v1=$SIGNATURE" \
--data-raw "$BODY"Test negativi da eseguire subito dopo:
# Firma alterata: atteso 401
SIGNATURE_ERRATA="$(printf '%s' "$SIGNATURE" | tr '0-9a-f' '1-9a-f0')"
curl -sS -o /dev/null -w "firma alterata -> %{http_code}\n" -X POST "$ENDPOINT" \
-H "Content-Type: application/json" \
-H "X-BP-Delivery: $(uuidgen | tr 'A-Z' 'a-z')" \
-H "X-BP-Timestamp: $TIMESTAMP" \
-H "X-BP-Signature: t=$TIMESTAMP,v1=$SIGNATURE_ERRATA" \
--data-raw "$BODY"
# Timestamp vecchio: atteso 400
OLD_TIMESTAMP="$(( TIMESTAMP - 900 ))"
OLD_SIGNATURE="$(printf '%s.%s' "$OLD_TIMESTAMP" "$BODY" \
| openssl dgst -sha256 -hmac "$BP_WEBHOOK_SECRET" -r | cut -d' ' -f1)"
curl -sS -o /dev/null -w "timestamp vecchio -> %{http_code}\n" -X POST "$ENDPOINT" \
-H "Content-Type: application/json" \
-H "X-BP-Delivery: $(uuidgen | tr 'A-Z' 'a-z')" \
-H "X-BP-Timestamp: $OLD_TIMESTAMP" \
-H "X-BP-Signature: t=$OLD_TIMESTAMP,v1=$OLD_SIGNATURE" \
--data-raw "$BODY"
# Header firma assente: atteso 401
curl -sS -o /dev/null -w "firma assente -> %{http_code}\n" -X POST "$ENDPOINT" \
-H "Content-Type: application/json" \
-H "X-BP-Delivery: $(uuidgen | tr 'A-Z' 'a-z')" \
--data-raw "$BODY"8. Script di verifica di una firma ricevuta
Utile per analizzare una consegna estratta dai log della Console.
#!/usr/bin/env bash
set -euo pipefail
# Uso: ./verifica-firma.sh corpo.json "t=1767225600,v1=9f2c..." "whsec_xxxxxxxxxxxxxxxx"
BODY_FILE="${1:?indicare il file con il corpo grezzo}"
SIGNATURE_HEADER="${2:?indicare il valore di X-BP-Signature}"
SECRET="${3:-$BP_WEBHOOK_SECRET}"
TOLERANCE=300
TIMESTAMP="$(printf '%s' "$SIGNATURE_HEADER" | tr ',' '\n' \
| grep '^t=' | head -n 1 | cut -d= -f2)"
if [ -z "$TIMESTAMP" ]; then
echo "ESITO: timestamp assente nell header" >&2
exit 2
fi
NOW="$(date +%s)"
AGE=$(( NOW - TIMESTAMP ))
AGE_ABS=${AGE#-}
if [ "$AGE_ABS" -gt "$TOLERANCE" ]; then
echo "ESITO: timestamp fuori tolleranza (differenza ${AGE_ABS}s)" >&2
exit 3
fi
EXPECTED="$(printf '%s.' "$TIMESTAMP" | cat - "$BODY_FILE" \
| openssl dgst -sha256 -hmac "$SECRET" -r | cut -d' ' -f1)"
MATCH=0
while IFS= read -r candidate; do
if [ "$candidate" = "$EXPECTED" ]; then
MATCH=1
break
fi
done < <(printf '%s' "$SIGNATURE_HEADER" | tr ',' '\n' | grep '^v1=' | cut -d= -f2)
if [ "$MATCH" -eq 1 ]; then
echo "ESITO: firma valida"
echo " timestamp: $TIMESTAMP (età ${AGE_ABS}s)"
echo " attesa: $EXPECTED"
exit 0
fi
echo "ESITO: firma NON valida" >&2
echo " attesa: $EXPECTED" >&2
echo " ricevute: $(printf '%s' "$SIGNATURE_HEADER" | tr ',' ' ')" >&2
exit 1Uso:
chmod +x verifica-firma.sh
./verifica-firma.sh corpo.json "t=1767225600,v1=9f2c1e7b40a3d6c5b8e1f0a2d4c7b9e3f5a8c1d0e2b4f6a9c3d5e7b1f0a2c41d"Il file del corpo deve essere byte-identico
Salva il corpo con curl -o o con la funzione Scarica corpo grezzo dei log consegne. Non
copiarlo dal browser: verrebbero aggiunti ritorni a capo o rientri che invalidano la firma.
9. Smoke test completo
#!/usr/bin/env bash
set -euo pipefail
: "${BP_API_KEY:?BP_API_KEY non impostata}"
: "${BP_PROJECT_ID:?BP_PROJECT_ID non impostata}"
BASE="${BP_API_BASE_URL:-https://api.blupantheon.com}"
pass=0
fail=0
check() {
local label="$1"
local expected="$2"
local actual="$3"
if [ "$actual" = "$expected" ]; then
printf 'OK %-40s (%s)\n' "$label" "$actual"
pass=$(( pass + 1 ))
else
printf 'KO %-40s atteso %s, ricevuto %s\n' "$label" "$expected" "$actual"
fail=$(( fail + 1 ))
fi
}
status() {
curl -sS -o /dev/null -w "%{http_code}" "$@"
}
check "autenticazione valida" "200" \
"$(status "$BASE/identity/v1/me" -H "Authorization: Bearer $BP_API_KEY")"
check "chiave errata respinta" "401" \
"$(status "$BASE/identity/v1/me" -H "Authorization: Bearer bp_live_nonvalida")"
check "chiave assente respinta" "401" \
"$(status "$BASE/identity/v1/me")"
check "lettura clienti" "200" \
"$(status "$BASE/identity/v1/clients?limit=1" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "X-BP-Project: $BP_PROJECT_ID")"
check "risorsa inesistente" "404" \
"$(status "$BASE/identity/v1/clients/bp_cli_inesistente" \
-H "Authorization: Bearer $BP_API_KEY" \
-H "X-BP-Project: $BP_PROJECT_ID")"
echo
echo "Risultato: $pass superati, $fail falliti"
[ "$fail" -eq 0 ]Diagnostica di rete
# Verifica del certificato TLS del tuo endpoint
openssl s_client -connect automation.acme.it:443 -servername automation.acme.it </dev/null 2>/dev/null \
| openssl x509 -noout -subject -issuer -dates
# Verifica assenza di redirect
curl -sS -o /dev/null -w "%{http_code} -> %{redirect_url}\n" \
-X POST "https://automation.acme.it/webhooks/blupantheon" \
-H "Content-Type: application/json" --data-raw '{}'
# Misura del tempo di risposta
curl -sS -o /dev/null -w "connessione %{time_connect}s, TLS %{time_appconnect}s, totale %{time_total}s\n" \
-X POST "https://automation.acme.it/webhooks/blupantheon" \
-H "Content-Type: application/json" --data-raw '{}'Soglia da rispettare
Il valore time_total deve restare ben sotto i 10 secondi anche sotto carico. Se supera i
2 secondi, sposta l'elaborazione in una coda asincrona.