diff --git a/app.py b/app.py
index d0d4d1bc37976b0e525dcd499f3fd380497fddc4..c5eff58bf897466c21269a0efaebde4ed3ea8dec 100644
--- a/app.py
+++ b/app.py
@@ -1,8 +1,12 @@
 import requests
-from flask import Flask, request, make_response
-from config import url
+from flask import Flask, request, make_response, render_template
+
+from config import url, hookshot_params, flask_params, msg_templates, msg_template_default
+from validate import grafana_validate_incoming
+
+
+app = Flask(__name__, **flask_params)
 
-app = Flask(__name__)
 
 @app.route("/webhook/slack/<hook>", methods=['POST'])
 def slack(hook):
@@ -65,35 +69,43 @@ def slack(hook):
 
 @app.route("/webhook/grafana/<hook>", methods=['POST', 'PUT'])
 def grafana(hook):
-    plain = ''
-    html = ''
+    """
+    see https://grafana.com/docs/grafana/latest/alerting/alerting-rules/manage-contact-points/integrations/webhook-notifier/
+    todo:
+    - handle query params to select different templates
+    - handle empty -> default
+    """
+
+    args = request.args
+    if "template" in args.keys():
+        template_type = args.get("template")
+        if template_type not in msg_templates:
+            template_type = msg_template_default
+    else:
+        template_type = msg_template_default
+
+    if "version" in args.keys():
+        version = args.get("version")
+    else:
+        version = "v2"
+
+    print(args)
+    print(f"template: {template_type}, version: {version}")
+
     incoming = request.json
     print('Got incoming /grafana hook: ' + str(incoming))
 
-    title = str(incoming.get('title', ''))
-    rule_url = str(incoming.get('ruleUrl', ''))
-    rule_name = str(incoming.get('ruleName', ''))
-    message = str(incoming.get('message', ''))
-    state = str(incoming.get('state', ''))
-    eval_matches = incoming.get('evalMatches', [])
-
-    if title and rule_url and rule_name:
-        plain += title + ' ' + rule_url + ': ' + rule_name + ' (' + state + ')\n'
-        html += '<b><a href="' + rule_url + '">' + title + '</a></b>: ' + rule_name + ' (' + state + ')<br/>\n'
+    if not isinstance(incoming, dict):
+        incoming = dict()
 
-    if message:
-        plain += message + '\n'
-        html += message + '<br/>\n'
+    grafana_validate_incoming(incoming)
 
-    for eval_match in eval_matches:
-        metric = str(eval_match.get('metric', ''))
-        value = str(eval_match.get('value', ''))
-        if metric and value:
-            plain += metric + ': ' + value + '\n'
-            html += '<b>' + metric + '</b>: ' + value + '<br/>\n'
+    t = lambda fmt: f"{template_type}.{fmt}.jinja"
+    plain = render_template(t("txt"), **incoming)
+    html = render_template(t("html"), **incoming)
 
     if plain and html:
-        json = {'text':plain,'html':html}
+        json = {'text':plain,'html':html, **hookshot_params}
         print('Sending hookshot: ' + str(json))
         r = requests.post(url + hook, json=json)
     else:
@@ -102,5 +114,6 @@ def grafana(hook):
 
     return {"ok":True}
 
+
 if __name__ == "__main__":
-    app.run(port=9080, debug=True)
+    app.run(host="0.0.0.0", port=9080, debug=True)
diff --git a/config.py b/config.py
index 3b638abf23e1969d363ec44c5afb0c72705cdf67..000562e4b7a57913ebfd65c3e1d87fc397aadb80 100644
--- a/config.py
+++ b/config.py
@@ -1 +1,6 @@
 url = 'https://hookshot.mbot.ethz.ch/webhook/'
+flask_params = {"template_folder": "template"}
+hookshot_params = {"version": "v2", "msgtype": "m.notice"}
+
+msg_templates = ("default", "concise", "ultra_concise")
+msg_template_default = "ultra_concise"
diff --git a/template/concise.html.jinja b/template/concise.html.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..3c423ed8ebf967ef599cdf65846f59ae51b5beef
--- /dev/null
+++ b/template/concise.html.jinja
@@ -0,0 +1,16 @@
+{% if alerts | length %}
+{% for alert in alerts %}
+<b><a href="{{ alert.generatorURL }}">[{{ alert.status }}] {{ alert.labels.alertname }}</a>:</b>
+{% if alert.labels %}
+{% for key, value in alert.labels.items() %}
+{{ key }}={{ value }} {{ ", " if not loop.last else " " }}
+{% endfor %}
+{% endif %}
+(<a href="{{ alert.silenceURL }}">Silence</a>)<br>
+{% if alert.valuesForJinja %}
+{% for key, value in alert.valuesForJinja.items() %}
+<b>{{ key }}:</b> {{ value }}<br>
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% endif %}
diff --git a/template/concise.txt.jinja b/template/concise.txt.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..1451d1460cd1d05a12e29b13ceae29f2b5a3917a
--- /dev/null
+++ b/template/concise.txt.jinja
@@ -0,0 +1,8 @@
+{% if alerts | length %}{% for alert in alerts %}
+[{{ alert.status }}] {{ alert.labels.alertname }}:
+Alert: {{ alert.generatorURL }}
+Silence: {{ alert.silenceURL }}
+{% if alert.labels %}{% for key, value in alert.labels.items() %}{{ key }}={{ value }}, {% endfor %}{% endif %}
+{% if alert.valuesForJinja %}{% for key, value in alert.valuesForJinja.items() %}{{ key }}: {{ value }}, {% endfor %}{% endif %}
+___
+{% endfor %}{% endif %}
diff --git a/template/default.html.jinja b/template/default.html.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..0d2fe3945c74680df5479d9fb5029717fc1edeb2
--- /dev/null
+++ b/template/default.html.jinja
@@ -0,0 +1,34 @@
+<b>{{ status | upper }}:{{ noFiring }} ({{ commonLabels.values() | join(" ") }})</b>
+<p><a href="{{ externalUrl }}?orgId={{ orgId }}">Grafana Instance</a></p>
+{% if alerts | length %}{% for alert in alerts %}
+<p><b>{{ alert.labels.alertname }}</b> (Status: {{ alert.status }}):
+    <a href="{{ alert.generatorURL }}">View</a>, <a href="{{ alert.silenceURL }}">Silence</a>
+</p>
+
+{% if alert.valuesForJinja %}
+<b>Values</b>
+<table>
+    {% for key, value in alert.valuesForJinja.items() %}
+    <tr>
+        <td>{{ key }}</td>
+        <td>{{ value }}</td>
+    </tr>
+    {% endfor %}
+</table>
+{% endif %}
+
+{% if alert.labels %}
+<b>Labels</b>
+<table>
+    {% for key, value in alert.labels.items() %}
+    <tr>
+        <td>{{ key }}</td>
+        <td>{{ value }}</td>
+    </tr>
+    {% endfor %}
+</table>
+{% endif %}
+<p><i>(StartsAt: {{alert.startsAtParsed }}{% if alert.endsAtParsed %}, EndsAt: {{alert.endsAtParsed }}{% endif %})</i>
+</p>
+<br>
+{% endfor %}{% endif %}
\ No newline at end of file
diff --git a/template/default.txt.jinja b/template/default.txt.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..c5ae8c593278be08284f2ef76d33ba5698e7be5d
--- /dev/null
+++ b/template/default.txt.jinja
@@ -0,0 +1,26 @@
+{{ status | upper }}:{{ noFiring }} ({{ commonLabels.values() | join(" ") }})
+Grafana: {{ externalUrl }}?orgId={{ orgId }}
+
+{% if alerts | length %}
+{% for alert in alerts %}
+---
+Name: {{ alert.labels.alertname }}
+Status: {{ alert.status }}
+View alert: {{ alert.generatorURL }}
+Silence alert: {{ alert.silenceURL }}
+
+{% if alert.Values %}
+Values:
+{% for key, value in alert.Values.items() %}
+{{ key }}: {{ value }}
+{% endfor %}
+{% endif %}
+
+{% if alert.labels %}
+Labels
+{% for key, value in alert.labels.items() %}
+{{ key }}: {{ value }}
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% endif %}
diff --git a/template/slack.html.jinja b/template/slack.html.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..bfe10b05cb95ba0de489a1ef12d7e64cf06d003f
--- /dev/null
+++ b/template/slack.html.jinja
@@ -0,0 +1,25 @@
+{% if color %}
+<font color="{{ color}}">
+{% endif %}
+
+{% if title and title_link %}
+<b><a href="{{ title_link }}">{{ title}}</a></b><br/>
+{% elif title %}
+<b>{{ title}}</b><br/>
+{% endif %}
+
+{% if text %}
+{{ text }}<br />
+{% endif %}
+
+{% for field in fields %}
+<b>{{ field.title }}:</b> {{ value}}<br/>
+{% endfor %}
+
+{% if footer %}
+{{ footer}}<br/>
+{% endif %}
+
+{% if color%}
+</font>
+{% endif %}
\ No newline at end of file
diff --git a/template/slack.txt.jinja b/template/slack.txt.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..93a2b4bf37a96c5ff03cdf91312f8e81dab6d61f
--- /dev/null
+++ b/template/slack.txt.jinja
@@ -0,0 +1,11 @@
+{% if title and title_link %}
+{{ title }} {{ title_link }}
+{% elif title %}{{ title }}{% endif %}
+
+{% if text %}{{ text }}{% endif %}
+
+{% for field in fields %}
+{{ field.title }}: {{ value}}
+{% endfor %}
+
+{{ footer}}
diff --git a/template/ultra_concise.html.jinja b/template/ultra_concise.html.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..fa2d51d5a798dabc6432d9248a2fbe65812f556d
--- /dev/null
+++ b/template/ultra_concise.html.jinja
@@ -0,0 +1,18 @@
+{% if alerts | length %}
+{% for alert in alerts %}
+{{ "<br>" if not loop.first }}
+<b><a href="{{ alert.generatorURL }}">[{{ alert.status }}] {{ alert.labels.alertname }}</a></b> 
+(<a href="{{ alert.silenceURL }}">Silence</a>)<b>:</b>
+{% if alert.labels %}
+{% for key, value in alert.labels.items() %}
+{{ key }}={{ value }} {{- ", " if not loop.last else " " }}
+{% endfor %}
+{% endif %}
+<br>
+{% if alert.valuesForJinja %}
+{% for key, value in alert.valuesForJinja.items() %}
+<b>{{ key }}:</b> {{ value }}{{ ", " if not loop.last else " " }}
+{% endfor %}
+{% endif %}
+{% endfor %}
+{% endif %}
diff --git a/template/ultra_concise.txt.jinja b/template/ultra_concise.txt.jinja
new file mode 100644
index 0000000000000000000000000000000000000000..1451d1460cd1d05a12e29b13ceae29f2b5a3917a
--- /dev/null
+++ b/template/ultra_concise.txt.jinja
@@ -0,0 +1,8 @@
+{% if alerts | length %}{% for alert in alerts %}
+[{{ alert.status }}] {{ alert.labels.alertname }}:
+Alert: {{ alert.generatorURL }}
+Silence: {{ alert.silenceURL }}
+{% if alert.labels %}{% for key, value in alert.labels.items() %}{{ key }}={{ value }}, {% endfor %}{% endif %}
+{% if alert.valuesForJinja %}{% for key, value in alert.valuesForJinja.items() %}{{ key }}: {{ value }}, {% endfor %}{% endif %}
+___
+{% endfor %}{% endif %}
diff --git a/validate.py b/validate.py
new file mode 100644
index 0000000000000000000000000000000000000000..c7fe8f2822a08187ddcbd5f7e0ff52dd0493f109
--- /dev/null
+++ b/validate.py
@@ -0,0 +1,85 @@
+from datetime import datetime
+from typing import Dict
+
+# https://grafana.com/docs/grafana-cloud/alerting-and-irm/alerting/configure-notifications/manage-contact-points/integrations/webhook-notifier/
+
+
+def validate_str_entry(d: Dict, k: str):
+    """Empty string if not exists"""
+    d[k] = d.get(k, "")
+
+
+def validate_number_entry(d: Dict, k: str):
+    """0 if not exists"""
+    d[k] = d.get(k, 0)
+
+
+def validate_dict_entry(d: Dict, k: str):
+    """Empty dict if not exists"""
+    entry = d.get(k, None)
+    if not isinstance(entry, dict):
+        entry = dict()
+    d[k] = entry
+
+
+def grafana_validate_incoming(incoming: Dict):
+    """
+    Makes sure the necessary keys are present in the incoming dict so it can be parsed by jinja
+    """
+    string_keys = ("receiver", "status", "externalUrl", "version", "groupKey")
+    for key in string_keys:
+        validate_str_entry(incoming, key)
+
+    validate_number_entry(incoming, "orgId")
+    validate_number_entry(incoming, "truncatedAlerts")
+
+    validate_dict_entry(incoming, "groupLabels")
+    validate_dict_entry(incoming, "commonLabels")
+    validate_dict_entry(incoming, "commonAnnotations")
+
+    incoming["alerts"] = alerts = incoming.get("alerts", [])
+
+    for alert in alerts:  # Handle non dict values?
+        grafana_validate_alert(alert)
+
+    l = lambda a: a.get("status", "") == "firing"
+    incoming["no_firing"] = len(tuple(filter(l, alerts)))
+
+
+def grafana_validate_alert(alert: Dict):
+
+    string_keys = (
+        "status",
+        "startsAt",
+        "endsAt",
+        "generatorURL",
+        "fingerprint",
+        "silenceURL",
+        "imageURL",
+    )
+    for key in string_keys:
+        validate_str_entry(alert, key)
+
+    validate_dict_entry(alert, "labels")
+    validate_dict_entry(alert, "annotations")
+    validate_dict_entry(alert, "values")
+
+    # fixes "values" being shadowed in jinja
+    alert["valuesForJinja"] = alert.get("values", None)
+
+    alert["startsAtParsed"] = format_dt(alert.get("startsAt", ""))
+    alert["endsAtParsed"] = format_dt(alert.get("endsAt", ""))
+
+
+def format_dt(dt_str: str) -> str:
+    """
+    Returns empty string if error or is year 1 e.g. zero value of golang time.Time
+    see https://pkg.go.dev/time#Time
+    """
+    try:
+        dt = datetime.fromisoformat(dt_str)
+        if dt.year < 2:
+            return ""
+        return dt.strftime("%Y-%m-%d %H:%M:%S %z")
+    except ValueError:
+        return ""