Inviare Adaptive Card a Teams via Webhook con Python: guida completa e troubleshooting

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.

Indice

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 con attachments.
  • Ciascun attachment deve avere contentType: "application/vnd.microsoft.card.adaptive" e la card reale in content.
  • 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

PassaggioDettaglio
Usa il wrapper correttoIl 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 correttiUsa la proprietà "$schema". Evita $$schema e placeholder ${...} nel JSON finale: risolvi le variabili in Python prima del POST.
Conferma che il webhook supporti Adaptive CardsAssicurati 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 cardIncolla 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 alternativeGli 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 e description sono sostituite prima dell’invio (nessun ${...} nel JSON finale).
  • Wrap: imposta "wrap": true nei TextBlock per evitare troncamenti su righe lunghe.
  • Versione: usa una versione di card (1.5 o 1.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

  1. Copia la sezione content (cioè l’oggetto AdaptiveCard senza il wrapper) in un designer di Adaptive Card.
  2. Verifica che la versione scelta sia supportata.
  3. 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

  1. Metodo: POST, URL: il tuo Incoming Webhook.
  2. Body: rawJSON, incolla il payload completo.
  3. 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 come TextBlock, FactSet, ColumnSet, Image, ecc.
  • actions può contenere Action.OpenUrl (renderizzata), mentre azioni come Action.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 e spacing 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 se 1.6 non viene renderizzata.
  • Placeholder: nessun ${...} nel payload finale.
  • JSON: invia con json=payload in requests (non data= 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 con attachments: ✅
  • 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.

Indice