Inviare un’Adaptive Card a un canale Microsoft Teams tramite Incoming Webhook con Python è semplice, ma un singolo errore di formattazione può far apparire la carta “vuota”. In questa guida trovi il formato corretto del payload, esempi pronti, checklist di troubleshooting e buone pratiche per un’integrazione robusta.
Scenario e sintomi
Un’applicazione Python genera dinamicamente il JSON di un’Adaptive Card e lo invia all’URL di un Incoming Webhook in un canale Teams (l’opzione “Post to a channel when a webhook request is received”). Il trigger parte, il messaggio arriva nel canale, ma la carta visualizzata non mostra i TextBlock
con releaseTitle
e description
: sembra una card “vuota”.
Diagnosi in un colpo d’occhio
- Il payload al webhook non è una card “nuda”: deve essere incapsulato in un messaggio di tipo
message
conattachments
. - Ciascun attachment deve avere
contentType: "application/vnd.microsoft.card.adaptive"
e la card reale incontent
. - La proprietà di schema è
"$schema"
, non$$schema
. - I placeholder
${...}
non vengono risolti dal webhook: sostituiscili lato server prima dell’invio.
Soluzione rapida (payload minimale corretto)
Invia questo formato al webhook. Nota il wrapper con type: "message"
e l’array attachments
:
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{ "type": "TextBlock", "text": "Titolo rilascio", "weight": "bolder", "size": "large" },
{ "type": "TextBlock", "text": "Descrizione", "wrap": true }
]
}
}
]
}
Passaggi chiave
Passaggio | Dettaglio |
---|---|
Usa il wrapper corretto | Il payload per l’Incoming Webhook deve essere un oggetto message con attachments . Ogni attachment ha contentType impostato a application/vnd.microsoft.card.adaptive e il JSON dell’Adaptive Card nel campo content . { "type": "message", "attachments": [ { "contentType": "application/vnd.microsoft.card.adaptive", "content": { "$schema": "http://adaptivecards.io/schemas/adaptive-card.json", "type": "AdaptiveCard", "version": "1.5", "body": [ { "type": "TextBlock", "text": "Titolo rilascio", "weight": "bolder", "size": "large" }, { "type": "TextBlock", "text": "Descrizione", "wrap": true } ] } } ] } |
Nessun doppio $ e nomi corretti | Usa la proprietà "$schema" . Evita $$schema e placeholder ${...} nel JSON finale: risolvi le variabili in Python prima del POST. |
Conferma che il webhook supporti Adaptive Cards | Assicurati di aver creato un Incoming Webhook moderno nel canale (non un connettore legacy). I nuovi Incoming Webhook e i Workflows supportano Adaptive Cards. |
Valida la card | Incolla la sezione content in un designer di Adaptive Card per verificarne sintassi e versione (1.5 o 1.6 ) supportata dal tuo client Teams. |
Limiti e alternative | Gli Incoming Webhook accettano payload fino a circa 28 KB e ~4 richieste/secondo. Per menzioni utenti, azioni interattive (Action.Submit ) o personalizzazioni avanzate, preferisci un Bot o Microsoft Graph (/chats/{id}/messages ). |
Esempio completo in Python (pronto all’uso)
Questo esempio illustra: costruzione della card, sostituzione delle variabili, invio al webhook, gestione degli errori, e una funzione di validazione minima.
import os
import json
import requests
from datetime import datetime
WEBHOOKURL = os.getenv("TEAMSWEBHOOK_URL")
def buildreleasecard(releasetitle: str, description: str, urldetail: str | None = None) -> dict:
"""
Restituisce il payload completo (wrapper + attachment) per l'Incoming Webhook di Teams.
"""
card_content = {
"$schema": "[http://adaptivecards.io/schemas/adaptive-card.json](http://adaptivecards.io/schemas/adaptive-card.json)",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{
"type": "TextBlock",
"text": release_title,
"weight": "bolder",
"size": "large"
},
{
"type": "TextBlock",
"text": description,
"wrap": True
},
{
"type": "TextBlock",
"text": f"Pubblicato: {datetime.utcnow().strftime('%Y-%m-%d %H:%M UTC')}",
"isSubtle": True,
"spacing": "small"
}
]
}
```
# Le azioni interattive Action.Submit non sono gestite dal webhook.
# Action.OpenUrl invece è renderizzata dal client e può aprire un link.
if url_detail:
card_content["actions"] = [
{
"type": "Action.OpenUrl",
"title": "Dettagli della release",
"url": url_detail
}
]
# Wrapper obbligatorio per Incoming Webhook
payload = {
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": card_content
}
]
}
return payload
```
def posttoteams(payload: dict) -> None:
if not WEBHOOK_URL:
raise RuntimeError("Variabile d'ambiente TEAMSWEBHOOKURL mancante.")
Invio JSON; requests imposta automaticamente l'header Content-Type: application/json
resp = requests.post(WEBHOOK_URL, json=payload, timeout=10)
try:
resp.raiseforstatus()
except requests.HTTPError as e:
Mostra risposta raw per debug (molto utile)
raise RuntimeError(f"Errore HTTP {resp.status_code}: {resp.text}") from e
if name == "main":
release_title = "Release 2.3.0 – Notifiche migliorate"
description = (
"- Nuovi endpoint per le notifiche\n"
"- Miglioramenti performance\n"
"- Fix per errori di timeout"
)
payload = buildreleasecard(releasetitle, description, urldetail=None)
posttoteams(payload)
print("Adaptive Card inviata con successo.")
Note importanti sul codice
- Variabili:
release_title
edescription
sono sostituite prima dell’invio (nessun${...}
nel JSON finale). - Wrap: imposta
"wrap": true
neiTextBlock
per evitare troncamenti su righe lunghe. - Versione: usa una versione di card (
1.5
o1.6
) compatibile con i client Teams della tua organizzazione. - Timeout: definisci un
timeout
nella chiamata HTTP per evitare blocchi del processo CI/CD. - Errori: stampa
resp.text
quando ottieni errori 4xx/5xx: spesso contiene un messaggio utile di convalida.
Perché la carta appariva vuota
- Il wrapper superiore aveva
type: "AdaptiveCard"
invece di"message"
, quindi Teams ignorava il contenuto non riconoscendo la struttura del messaggio. contentType
non era"application/vnd.microsoft.card.adaptive"
, per cui l’attachment non veniva interpretato come Adaptive Card.- I placeholder
${...}
sono stati inviati alla lettera: il webhook non effettua il rendering di template.
Correggendo questi tre punti, i TextBlock
con releaseTitle
e description
vengono renderizzati correttamente.
Validazione, test e debug
Convalidare la sezione content
- Copia la sezione
content
(cioè l’oggetto AdaptiveCard senza il wrapper) in un designer di Adaptive Card. - Verifica che la versione scelta sia supportata.
- Controlla che tutte le proprietà siano corrette (
type
,body
,actions
, ecc.).
Test rapido con curl
Ottimo per isolare problemi di codice:
# payload.json contiene l'oggetto completo con "type": "message"
curl -X POST \
-H "Content-Type: application/json" \
-d @payload.json \
"$TEAMSWEBHOOKURL"
Test con Postman
- Metodo: POST, URL: il tuo Incoming Webhook.
- Body: raw – JSON, incolla il payload completo.
- Invia e osserva: se ricevi
200
con testo breve, il messaggio è stato accettato; se la carta non si vede, rivedi struttura e versioni.
Struttura dell’Adaptive Card: campi essenziali
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{ "type": "TextBlock", "text": "Titolo", "weight": "bolder", "size": "large" },
{ "type": "TextBlock", "text": "Descrizione", "wrap": true }
]
}
body
accetta elementi comeTextBlock
,FactSet
,ColumnSet
,Image
, ecc.actions
può contenereAction.OpenUrl
(renderizzata), mentre azioni comeAction.Submit
richiedono un canale interattivo (bot/Graph) per gestire la risposta.
Modello riutilizzabile per release notes
Ecco un template di card orientato a note di rilascio, con campi extra utili:
{
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{ "type": "TextBlock", "text": "{{title}}", "weight": "bolder", "size": "large" },
{ "type": "TextBlock", "text": "{{description}}", "wrap": true, "spacing": "medium" },
{
"type": "FactSet",
"facts": [
{ "title": "Versione:", "value": "{{version}}" },
{ "title": "Ambiente:", "value": "{{environment}}" },
{ "title": "Autore:", "value": "{{author}}" }
]
},
{ "type": "TextBlock", "text": "Pubblicato: {{timestamp}}", "isSubtle": true, "spacing": "small" }
],
"actions": [
{ "type": "Action.OpenUrl", "title": "Note di rilascio", "url": "{{releaseUrl}}" }
]
}
Nota: i {{placeholder}}
vanno risolti nel tuo codice (ad es. con Jinja2 in Python) prima dell’invio al webhook.
Rendering del testo: suggerimenti
- Attiva
"wrap": true
per mostrare testo su più righe. - I
TextBlock
supportano Markdown di base (grassetto, corsivo, elenchi, link). Se usi Markdown, verifica sempre l’anteprima nel designer. - Per grandi blocchi di testo, valuta
maxLines
espacing
per una resa più ordinata.
Limiti, quote e quando usare alternative
- Dimensione payload: circa 28 KB per messaggio. Mantieni il JSON compatto.
- Frequenza: ~4 richieste/sec per webhook. Implementa un semplice retry con backoff.
- Interattività: per menzioni utenti, pulsanti che inviano dati (
Action.Submit
), flussi approvativi o thread complessi, usa un Bot o le API Microsoft Graph.
Checklist di troubleshooting
- Wrapper: primo livello
{ "type": "message", "attachments": [...] }
. - contentType: esattamente
application/vnd.microsoft.card.adaptive
. - Content: oggetto AdaptiveCard valido con
"$schema"
,"type"
,"version"
,"body"
. - Versione: prova
1.5
se1.6
non viene renderizzata. - Placeholder: nessun
${...}
nel payload finale. - JSON: invia con
json=payload
inrequests
(nondata=
con stringhe malformate). - Header: non forzare manualmente
Content-Type
errati (text/plain
o simili). - Encoding: usa UTF‑8; verifica che caratteri speciali non siano doppio-escapati.
- Card vuota: se compare un riquadro senza testo, ricontrolla wrapper e
contentType
.
Esempio esteso: payload con FactSet e link
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{ "type": "TextBlock", "text": "Release 2.3.0 – Notifiche migliorate", "weight": "bolder", "size": "large" },
{ "type": "TextBlock", "text": "- Nuovi endpoint\n- Performance\n- Fix timeout", "wrap": true },
{
"type": "FactSet",
"facts": [
{ "title": "Ambiente:", "value": "Prod" },
{ "title": "Build:", "value": "2025.10.05.1432" }
]
}
],
"actions": [
{ "type": "Action.OpenUrl", "title": "Apri dettagli", "url": "https://example.internal/releases/2.3.0" }
]
}
}
]
}
Integrazione in CI/CD
Integra la notifica nel processo di rilascio: al termine del deploy, uno script invia l’Adaptive Card con le informazioni della build.
# GitHub Actions (estratto)
Aggiungi TEAMSWEBHOOKURL ai Secrets del repository
name: Notify Teams on release
on:
push:
tags:
- 'v..*'
jobs:
notify:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: actions/setup-python@v5
with:
python-version: '3.11'
- run: pip install requests
- env:
TEAMSWEBHOOKURL: ${{ secrets.TEAMSWEBHOOKURL }}
RELEASETITLE: ${{ github.refname }}
RELEASEDESC: "Tag ${{ github.refname }} pubblicato"
run: |
python - <<'PY'
import os, requests
payload = {
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "[http://adaptivecards.io/schemas/adaptive-card.json](http://adaptivecards.io/schemas/adaptive-card.json)",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{ "type": "TextBlock", "text": os.getenv("RELEASE_TITLE"), "weight": "bolder", "size": "large" },
{ "type": "TextBlock", "text": os.getenv("RELEASE_DESC"), "wrap": True }
]
}
}
]
}
r = requests.post(os.getenv("TEAMSWEBHOOKURL"), json=payload, timeout=10)
r.raiseforstatus()
print("Notifica inviata")
PY
Sicurezza e governance
- Tratta l’URL del webhook come una credenziale: non committarlo in repository pubblici o log.
- Usa un secret manager (variabili d’ambiente, vault aziendali) per l’iniezione sicura.
- Prevedi una rotazione periodica dell’URL del webhook.
- Isola i webhook per progetto/ambiente; evita di riusarli trasversalmente.
FAQ pratiche
Il testo supporta Markdown?
Sì, i TextBlock
supportano Markdown di base. Verifica sempre con un designer di Adaptive Card e abilita wrap
per i paragrafi lunghi.
Posso menzionare utenti con @?
Con i soli Incoming Webhook no. Le menzioni richiedono un bot o l’API Graph per generare le entità specifiche supportate dal canale.
Perché Action.Submit
non funziona?
Il webhook non gestisce la callback dei dati inviati. Le card possono essere visualizzate, ma l’interazione server‑side richiede bot/Graph.
Versione 1.6 o 1.5?
Se il client non supporta 1.6
, prova 1.5
. Mantieni la card semplice per massima compatibilità.
Checklist finale
- Wrapper
message
conattachments
: ✅ contentType
corretto: ✅- Card valida (
"$schema"
,type
,version
,body
): ✅ - Niente placeholder
${...}
nel JSON finale: ✅ wrap: true
dove serve: ✅- Test con curl/Postman eseguiti: ✅
- Limiti e retry considerati: ✅
Riepilogo
Se la tua Adaptive Card in Teams appare “vuota”, cerca innanzitutto errori nella struttura del payload: wrapper message
, contentType
dell’attachment e assenza di placeholder non risolti. Con il formato corretto, Teams renderizza immediatamente i TextBlock
e puoi distribuite notifiche di rilascio, avvisi di build e report con un’esperienza pulita e coerente. In caso di esigenze interattive, valuta il passaggio a bot o Graph.
Appendice: snippet veloci
Python ultra‑compatto
import os, requests
title = "Release 2.3.0"
desc = "Migliorie e fix"
payload = {
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{ "type": "TextBlock", "text": title, "weight": "bolder", "size": "large" },
{ "type": "TextBlock", "text": desc, "wrap": True }
]
}
}
]
}
requests.post(os.getenv("TEAMSWEBHOOKURL"), json=payload).raiseforstatus()
Struttura JSON da copiare
{
"type": "message",
"attachments": [
{
"contentType": "application/vnd.microsoft.card.adaptive",
"content": {
"$schema": "http://adaptivecards.io/schemas/adaptive-card.json",
"type": "AdaptiveCard",
"version": "1.5",
"body": [
{ "type": "TextBlock", "text": "Titolo rilascio", "weight": "bolder", "size": "large" },
{ "type": "TextBlock", "text": "Descrizione", "wrap": true }
]
}
}
]
}
Con queste modifiche, l’Adaptive Card verrà visualizzata correttamente nel canale Teams e potrai scalare in modo sicuro e manutenibile.