Documentazione dell'API

Contratto 1.0 · indirizzo https://api.agileascolto.agile.software · le novità sono in Novità e in GET /health.

Autenticazione

Ogni richiesta porta la chiave nell'header Authorization: Bearer aga_… (oppure X-API-Key). Nel WebSocket, se il client non può mandare header (browser), si usa il sottoprotocollo agileascolto, key.aga_…. La chiave non va mai nell'URL.

Ogni chiave porta uno o più scope — transcribe (file), stream (WebSocket), batch (lavori asincroni), metrics (misure e quadro), test (scope aggiuntivo, mai da solo: marca una chiave di prova, non copre nessuna rotta) — e può avere una scadenza opzionale: oltre, 403 KEY_EXPIRED. Per provare l'API senza onboarding esiste una chiave passe-partout di prova, a termine e con quota bassa.

Modelli

NomeCosa è
agileascolto-turboModello generale per l'italiano, predefinito. Streaming e file.
agileascolto-discoveryModello in valutazione per il telefono (nomi, numeri, codici). Abilitato per chiave, su richiesta.
whisper-1Accettato solo come alias di compatibilità del modello predefinito, per i client che lo mandano per abitudine.

GET /v1/models elenca i modelli che la tua chiave può usare.

Trascrizione da file — POST /v1/audio/transcriptions

Compatibile con l'API OpenAI. Corpo multipart/form-data: file (wav, mp3, m4a, ogg, webm, flac… fino a 200 MB e 60 minuti), model, language (predefinito it), response_format = json | text | verbose_json | srt | vtt (sottotitoli: al massimo due righe da 42 caratteri e 7 secondi per didascalia; le frasi lunghe si spezzano ai confini di parola). words=1 (o timestamp_granularities[]=word, come OpenAI): parole con inizio e fine in verbose_json, e sottotitoli tagliati sulle pause fra le parole. prompt e temperature si accettano e si ignorano. codici=1: i codici dettati (IBAN, POD, PDR, codice fiscale, numeri di pratica letti lettera per lettera) escono compattati in un solo token: «i ti otto uno di zero uno sette zero…» → IT81D0170…, «bi esse trattino uno cinque…» → BS-15767. Il resto della frase non cambia. La risposta porta anche codici: i token trovati col tipo, valido (checksum mod 97 per l'IBAN, carattere di controllo per il codice fiscale, forma per gli altri) e motivo; se il codice non regge e c'è una sola riparazione che lo farebbe reggere, anche riparato e regola — valido resta false: è una proposta da far confermare, non un codice accettato. atteso=cf (con codici=1): il tipo di codice che hai chiesto tu (iban|cf|pod|pdr|telefono|pratica) — il candidato che risponde alla domanda porta atteso: true, e se l'orecchio ha perso un carattere e nessun candidato ha più quella forma il più vicino viene adottato e ricontrollato come quel tipo (adottato: true), col motivo da dire. Un candidato valido non si adotta mai e l'adozione non rende valido niente. ricomponi=<codice sentito prima> + blocco=cognome (con codici=1 e atteso): quando il codice non regge e non c'è una riparazione, chiedi un blocco invece di tutto (codice fiscale: cognome, nome, nascita, comune, controllo; IBAN: paese, banca, sportello, conto; POD e PDR: testa, coda) e manda il turno della risposta con quei due campi: torna ricomposto col codice rimesso insieme e ricontrollato. giudicabile dice se il controllo di quel tipo ha un checksum e può quindi bocciare una ricomposizione sbagliata (IBAN e codice fiscale sì, POD e PDR no: là valido: true vuol dire solo «la forma regge» e il codice va riletto alla persona). Il blocco lo scegli tu: il fronte non sa dove sia l'errore. Chiedine uno per volta nell'ordine del codice e lascia che sia il controllo a dirti quando fermarti: il primo innesto riporta il codice alla lunghezza giusta, e da lì la scala chiude su 30 codici fiscali su 36 e 29 IBAN su 29 senza ridettare niente per intero (mediana 4 domande da 3–5 caratteri, 0 ricomposizioni sbagliate accettate). Conviene perché il pezzo corto si sente: un codice fiscale dettato intero esce esatto 9 volte su 90, uno da 7–10 caratteri sta fra il 74 % e il 93 %.

GET /v1/codici/blocchi?tipo=cf&sentito=<codice sentito prima> dà i blocchi di quel tipo con la frase da dire e, per ciascuno, posizionabile: un blocco si innesta solo se il codice sentito è lungo abbastanza, e chiederne uno che non si innesta butta via un turno. Senza sentito torna la sola tavola dei blocchi.

from openai import OpenAI
c = OpenAI(base_url="https://api.agileascolto.agile.software/v1", api_key="aga_…")
r = c.audio.transcriptions.create(model="agileascolto-turbo", file=open("chiamata.wav", "rb"), language="it")
print(r.text)
curl https://api.agileascolto.agile.software/v1/audio/transcriptions \
  -H "Authorization: Bearer aga_…" -F file=@chiamata.wav -F model=agileascolto-turbo -F language=it
{"text": "…"}

Con verbose_json: {"task","language","duration","text","segments":[{"id","start","end","text"}],"model","processing_ms"}. I segmenti sono le frasi come le ha chiuse il motore, con inizio e fine in secondi. Con words=1 c'è anche "words":[{"word","start","end"}], in cima (come OpenAI) e dentro ogni segmento. Per i lavori lunghi (scope batch), POST /v1/audio/jobs accetta il file e GET /v1/audio/jobs/{id}?format=srt (o vtt, text) consegna il file pronto.

Streaming — WS /v1/audio/stream?model=…

Il client apre il WebSocket, manda un messaggio di avvio, poi l'audio PCM a 16 bit mono in frame da 20 ms, e riceve parziali e finali mentre parla.

DirezioneMessaggioSignificato
client → server{"type":"start","language":"it","sample_rate":16000}Avvio. sample_rate 8000 (telefono) o 16000. Facoltativo "codici":true (o ?codici=1 nell'URL): i final escono coi codici dettati compattati, i partial no.
server → client{"type":"started","model":"agileascolto-turbo"}Pronto a ricevere audio. model dice quale orecchio ascolta (ob. 6): il nome nostro, quello del registro e di /v1/usage.
client → serverframe binari PCM s16le mono20 ms per frame (640 byte a 16 kHz, 320 a 8 kHz), a ritmo reale.
server → client{"type":"partial","text":"…"}Testo provvisorio della frase in corso.
server → client{"type":"final","text":"…","speech_final":true}Frase chiusa. speech_final vero = la persona ha finito di parlare.
client → server{"type":"flush"}Chiudi subito la frase in corso (fine turno decisa dal client), la sessione resta aperta.
client → server{"type":"stop"}Fine sessione: arriva l'ultimo finale, poi {"type":"closed"}.
server → client{"type":"error","code":"…","status":429,"message":"…"}Errore del fronte, poi chiusura con codice 1008.
server → client{"type":"closing","code":"SHUTTING_DOWN","retry_after_s":10}Il fronte si sta fermando (un ridispiego): subito dopo arriva il finale di ciò che hai già detto, poi la chiusura col codice 1012 («il servizio riparte»). Riapri la sessione dopo retry_after_s: le librerie WebSocket riprovano da sole sul 1012.
import asyncio, json, websockets
async def main():
    async with websockets.connect("wss://api.agileascolto.agile.software/v1/audio/stream?model=agileascolto-turbo",
                                  additional_headers={"Authorization": "Bearer aga_…"}) as ws:
        await ws.send(json.dumps({"type": "start", "language": "it", "sample_rate": 16000}))
        # … await ws.send(frame_pcm) ogni 20 ms …
        await ws.send(json.dumps({"type": "stop"}))
        async for m in ws:
            print(m)
asyncio.run(main())

Consumi — GET /v1/usage

La chiave vede solo i propri consumi: per mese, operazione (transcribe, stream, batch) e modello, con richieste, secondi di audio e millisecondi di elaborazione. Il campo used_s_this_month si confronta con quota_s_month. Senza scrivere codice: la pagina d'uso mostra gli stessi numeri incollando la chiave (che viaggia solo nell'intestazione, mai nell'indirizzo).

Quadro — GET /v1/quadro

Per chi vigila (chiave con scope metrics): le chiavi con richieste, secondi e percentuale di quota del mese (?mese=AAAA-MM, predefinito il corrente), i totali e lo stato dei motori. Il perimetro è quello della chiave e sta scritto nella risposta (perimetro): una chiave con un tenant vede la sua azienda, una chiave della regia (senza tenant) il servizio intero. I contatori di /metrics sono del servizio e non si restringono a un'azienda: a una chiave con un tenant rispondono 403 METRICS_TENANT_FORBIDDEN. Solo numeri: mai segreti, mai testi. Lo stesso quadro si legge sul server con agileascolto-keys.py quadro e, con una chiave metrics, nella pagina d'uso.

Stato — GET /health

Senza chiave. Dice se il fronte e i motori rispondono, la versione del contratto, i flussi attivi e il link alle novità.

Console self-service — /v1/console

Rotte a parte dal contratto di trascrizione: non si autenticano con una chiave aga_… ma con un token utente agt_… del tuo tenant (Authorization: Bearer agt_…). Servono all'amministratore di un'azienda (o a una persona individuale) per gestire da solo le proprie chiavi e le proprie persone, senza chiedere niente a noi. Vedi e tocchi solo il tuo tenant (403 TENANT_FORBIDDEN); ogni operazione finisce nel registro, mai col segreto di una chiave né col token di un invito.

RottaCosa fa
POST /v1/console/keysEmette una chiave del tuo tenant. Il segreto si vede una volta sola, qui. 403 PLAN_EXCEEDED se la chiave da sola prometterebbe più del piano dell'azienda. Self-service sono transcribe, stream e batch: metrics e test li emette la regia (403 SCOPE_NOT_SELF_SERVICE).
GET /v1/console/keysLe tue chiavi con stato e uso del mese, più il piano dell'azienda e i suoi secondi. Ognuna dice di chi è: owner_email e owner_stato accanto all'owner (una chiave attiva di una persona sospesa si vede a occhio); ?owner= restringe a una persona (id o email) o a un prodotto. Mai il segreto, mai il token di una persona.
POST /v1/console/keys/{key_id}/rotateChiave nuova con gli stessi diritti; grazia_ore tiene viva la vecchia per il periodo di grazia.
POST /v1/console/keys/{key_id}/revokeRevoca immediata.
POST /v1/console/keys/revoke-by-ownerToglie in una mossa tutte le chiavi attive di una persona o di un prodotto (owner: id, email o nome del prodotto) — è il gesto per chi ha lasciato l'azienda. Senza grazia: una revoca non consegna un segreto nuovo, quindi chi vuole tenere viva l'integrazione ruota la chiave. 404 OWNER_NOT_FOUND se quel nome non ha nessuna chiave.
POST /v1/console/usersInvita una persona nel tuo tenant (email, ruolo, scadenza_giorni). Il token dell'invito si vede una volta sola, qui, e scade: 7 giorni di serie, 90 il massimo (expires_at nella risposta). 409 USER_EXISTS se è già una tua persona.
POST /v1/console/users/acceptChi ha ricevuto un invito lo accetta col proprio token: non si accetta per conto di altri.
GET /v1/console/usersLe tue persone con ruolo (amministratore, utente) e stato (invitato, attivo, sospeso).
POST /v1/console/users/{utente_id}/suspendSospende una persona: il suo token smette di entrare in console. Le sue chiavi non si toccano da sole, mai: chiavi nel corpo è la scelta — "lascia" (di serie: restano, e il registro dice quali) o "revoca" (le sue chiavi attive vanno via nella stessa mossa). 409 LAST_ADMIN sull'ultimo amministratore attivo: la tua azienda non può chiudersi fuori da sola.
POST /v1/console/users/{utente_id}/reactivateRiattiva chi torna.
PUT /v1/console/users/{utente_id}/roleCambia il ruolo. Invitare, sospendere e assegnare ruoli è dell'amministratore (403 ADMIN_REQUIRED); l'elenco lo legge chiunque del tenant.
POST /v1/console/users/{utente_id}/rotate-tokenToken nuovo per una persona, identità invariata (stesso id, stesso ruolo, stesse chiavi, stessa storia nel registro): si vede una volta sola, qui. Il tuo lo cambi tu, anche se sei un utente semplice; quello delle tue persone lo cambia un amministratore. Di serie il vecchio muore subito (grazia_ore: 0) e poi dà 403 TOKEN_ROTATED. 409 USER_NOT_ACTIVE se la persona è sospesa.
GET /v1/console/auditRegistro delle operazioni del tuo tenant: chi, cosa, su quale chiave o su quale persona, con che esito. Una riga che parla di una tua chiave c'è anche quando l'ha scritta la regia, e dice di chi è chi ha agito. Filtri key_id, persona (id o email) e operazione; limite da 1 a 1000 (100 di serie), oltre è 400 AUDIT_LIMIT_INVALID — niente tagli silenziosi — e la risposta dice quante righe combaciano e se è troncato. Mai un token, mai un'impronta, mai un'email.
GET /v1/console/tenants/{tenant}/pianoIl piano dell'azienda (richieste al minuto e secondi al mese concordati) e i suoi secondi del mese.

Il piano dell'azienda — il tetto del tenant

rate_per_min e quota_s_month stanno sulla chiave, e le chiavi le fa l'amministratore dalla console: sono una scelta tua, non il contratto. Il contratto è il piano dell'azienda (il piano del tenant): richieste al minuto e secondi al mese dell'azienda intera, somma di tutte le sue chiavi — le revocate comprese, per i secondi del mese.

Il piano lo scrive solo la regia (PUT /v1/console/tenants/{tenant}/piano, header X-Regia-Superadmin): dalla console un'azienda non si allarga il proprio tetto, perché è la parte concordata. Lo legge quando vuole: l'amministratore in GET /v1/console/keys (piano, tenant_used_s_mese) e in GET /v1/console/tenants/{tenant}/piano, chi possiede una chiave in GET /v1/usage (piano_tenant, tenant_used_s_this_month).

Morde in due punti. Alla creazione di una chiave che da sola prometterebbe più del piano: 403 PLAN_EXCEEDED, e vale anche per una chiave «senza limite». E a ogni richiesta, sulla somma di tutte le chiavi dell'azienda: 429 TENANT_RATE_LIMIT e 429 TENANT_QUOTA_EXCEEDED. Questi due dicono una cosa diversa dai loro omonimi della chiave: RATE_LIMIT e QUOTA_EXCEEDED sono di quella chiave (l'amministratore può darne un'altra o allargarla), i TENANT_* sono dell'azienda, e una chiave in più non serve a niente. Il ritmo al minuto è su una finestra scorrevole (gli ultimi 60 secondi), non sul minuto d'orologio.

0 vuol dire «nessun tetto», come per le chiavi, e un tenant senza piano scritto prende quello di partenza del dispiegamento: 0 e 0 di serie, quindi chi non usa i piani non cambia comportamento.

Perché esiste, misurato: col piano «6 richieste al minuto, 6 secondi al mese» e un amministratore che si fa 3 chiavi in buona fede dalla console, senza il piano l'azienda passava 18 richieste al minuto su un tetto di 6 (3,0x) e 18 secondi nel mese su una quota di 6 (3,0x); col piano, 6 e 6 (1,0x). È la misura del nostro banco di prova, non una promessa di capacità.

Chiedere una licenza — POST /v1/richieste-licenza

Rotta pubblica, senza chiave: la chiede chi ancora non ne ha una. Corpo JSON con nome, email e uso obbligatori, azienda, piano (prova, standard o enterprise, senza vale prova) e volume_stimato facoltativi. Il campo sito_web va lasciato vuoto: è la trappola contro i robot.

Risposta 201 col numero da citare quando ci si scrive: {"numero":7,"id":"req_1a2b3c4d","stato":"registrata","messaggio":"richiesta registrata n. 7"}.

Nessuna richiesta inghiottita: la riga si scrive prima di provare qualunque recapito, e dal momento in cui esiste la risposta è 201 qualunque cosa succeda dopo. Un giro sul nostro box la porta a chi deve leggerla e la segna fatta solo a consegna riuscita: se il recapito è giù, la richiesta resta e il giro dopo riprova.

I rifiuti hanno un nome: 400 LICENSE_REQUEST_INVALID (dice quale campo), 400 LICENSE_REQUEST_REJECTED (la trappola era piena: nessuna riga scritta), 413 LICENSE_REQUEST_TOO_LARGE (corpo oltre 4096 byte, controllato mentre si legge), 429 RATE_LIMIT col Retry-After (massimo 5 al minuto per indirizzo).

Privacy: dell'indirizzo conserviamo solo lo SHA-256, e nel nostro registro finiscono numero, id e piano — mai il nome, mai l'email, mai il testo di quello che hai scritto. L'elenco delle richieste (GET /v1/richieste-licenza) è solo nostro, e senza il token della regia configurato è chiuso anche a noi (403 SUPERADMIN_REQUIRED).

Errori

StatoCodiceQuando
401KEY_MISSING, KEY_INVALIDChiave assente o sconosciuta.
403KEY_REVOKED, KEY_EXPIRED, SCOPE_NOT_ALLOWED, MODEL_NOT_ALLOWEDChiave revocata, scaduta, o che non copre l'operazione o il modello.
403INVITE_EXPIRED, TOKEN_ROTATEDIn console: invito non accettato e scaduto, o token ritirato da una rotazione. Il segreto è stato riconosciuto — chiedine uno nuovo: non è un 401.
404MODEL_UNKNOWNNome di modello inesistente.
400AUDIO_INVALID, FORMAT_UNSUPPORTEDFile non decodificabile o troppo corto; response_format (o format dei lavori) non previsto.
501TIMESTAMPS_UNAVAILABLESottotitoli chiesti a un modello il cui motore non dà i tempi dei segmenti: non si inventano.
413AUDIO_TOO_LONG, SESSION_TOO_LONG, AUDIO_TOO_LARGEOltre 60 minuti per file o 4 ore per sessione; AUDIO_TOO_LARGE = corpo oltre i 200 MB.
429RATE_LIMIT, QUOTA_EXCEEDED, TENANT_RATE_LIMIT, TENANT_QUOTA_EXCEEDEDTroppe richieste al minuto, o secondi del mese esauriti (anche a metà di uno streaming). I TENANT_* sono il tetto dell'azienda intera (il piano del tenant), non di questa chiave: farsene un'altra non serve.
503ENGINE_DOWN, CAPACITY, SHUTTING_DOWNSHUTTING_DOWN = il fronte si sta fermando (un ridispiego): non è un guasto e non è un pieno, torna fra pochi secondi, e chi era già dentro riceve prima il suo finale. Motore non raggiungibile, o troppi flussi in parallelo su quel motore (il messaggio lo nomina; oltre c'è il tetto di tutto il fronte), o coda dei lavori di quel motore piena — per numero o per secondi d'audio in RAM di tutto il fronte (il messaggio dice quanti sono liberi), o troppe richieste con audio in volo insieme (si aspetta il proprio turno, poi 503). Non c'è mai un ripiego silenzioso su un altro modello o su un altro servizio: se il modello sta su più macchine il fronte le prova tutte, e ENGINE_DOWN vuol dire che nessuna risponde.
502ENGINE_ERRORIl motore ha rifiutato la richiesta.

Corpo degli errori: {"error":{"code":"…","message":"…"}}.

La quota (e il piano dell'azienda) conta anche l'audio entrato e non ancora registrato: una telefonata aperta, un file in lavorazione, un lavoro in coda. Tre linee insieme non passano tre volte la quota, e GET /v1/usage dice quei secondi in in_corso_s e tenant_in_corso_s. Il blocco d'audio rifiutato non arriva al motore e non si paga.

Quando ha senso riprovare, il rifiuto dice anche quando: i 429 e il 503 CAPACITY portano l'intestazione Retry-After (secondi interi) e lo stesso numero in error.retry_after_s — sul WebSocket, dove le intestazioni non esistono, solo il campo. Sul tetto al minuto è l'istante in cui si libera un posto, sulla quota è l'inizio del mese prossimo, sul pieno è un consiglio di pochi secondi, su un ridispiego (SHUTTING_DOWN) è il tempo di fermarsi e ripartire. Gli altri rifiuti non lo portano: riprovare non cambierebbe niente.

Privacy

L'audio viaggia in memoria e non viene scritto su disco; il testo non viene registrato. Del traffico restano solo, per chiave: secondi, richieste, tempi. Nessun dato lascia l'Unione Europea.