Prisma Reviews / Guides / Reviews to Zendesk

Turn customer reviews into Zendesk tickets, and reply from the ticket

A 1-star review is a support case that happens to be public. Treating it like one means a ticket in the queue, the same SLAs, the agent who knows the customer, and an answer on the platform when the case is solved. This guide builds both directions with Zendesk’s APIs: a Prisma webhook creates a ticket for each review under 4 stars, and a Zendesk trigger sends the agent’s answer back, which Prisma publishes on the platforms that allow replies. Python with Flask, about 70 lines.

Shell
pip install flask requests

You need a Prisma API key and webhook secret, a Zendesk API token (Admin Center → Apps and integrations → Zendesk API) with the agent email it belongs to, and your subdomain.

1. One custom field in Zendesk

Admin Center → Objects and rules → Tickets → Fields → Add field → Multi-line, title Review reply, visible to agents only. Note its field id (360…). This is where the agent writes the public answer; ticket comments stay internal.

2. Create a ticket for each low-rated review

Python
import base64, hashlib, hmac, os, time, uuid, requests
from flask import Flask, request, abort

app = Flask(__name__)
PRISMA = "https://api.prismareviews.com/v1"
PH = {"Authorization": f"Bearer {os.environ['PRISMA_API_KEY']}"}
ZD = f"https://{os.environ['ZENDESK_SUBDOMAIN']}.zendesk.com/api/v2"
ZAUTH = (f"{os.environ['ZENDESK_EMAIL']}/token", os.environ["ZENDESK_API_TOKEN"])
REPLY_FIELD = int(os.environ["ZENDESK_REVIEW_REPLY_FIELD_ID"])

def verify_prisma(raw: bytes, header: str) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(","))
    expected = hmac.new(os.environ["PRISMA_WEBHOOK_SECRET"].encode(), f"{parts['t']}.".encode() + raw, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts["v1"]) and abs(time.time() - int(parts["t"])) < 300

def ticket_exists(review_id: str) -> bool:
    r = requests.get(f"{ZD}/tickets.json", auth=ZAUTH, params={"external_id": review_id}, timeout=30)
    r.raise_for_status()
    return bool(r.json().get("tickets"))

@app.post("/webhooks/prisma")
def prisma_webhook():
    if not verify_prisma(request.get_data(), request.headers.get("Prisma-Signature", "")):
        abort(400)
    e = request.get_json()
    if e["type"] != "review.created" or ticket_exists(e["data"]["id"]):
        return {"ok": True}
    r = requests.get(f"{PRISMA}/reviews/{e['data']['id']}", headers=PH, timeout=30).json()
    if (r.get("rating") or 5) >= 4:
        return {"ok": True}
    body = (f"{r['rating']}★ review on {r['platform']} by {r.get('author') or 'a customer'} ({r['created_at'][:10]})\n\n"
            f"{r.get('title') or ''}\n{r.get('text') or ''}\n\nOpen: {r.get('url') or '-'}\n\n"
            "Write the public answer in the 'Review reply' field and solve the ticket to publish it.")
    ticket = {"subject": f"{r['rating']}★ {r['platform']} review: {(r.get('title') or r.get('text') or '')[:60]}",
              "comment": {"body": body, "public": False}, "external_id": r["id"], "priority": "high" if r["rating"] <= 2 else "normal",
              "tags": ["review", f"review_{r['platform']}", f"stars_{r['rating']}"]}
    requests.post(f"{ZD}/tickets.json", auth=ZAUTH, json={"ticket": ticket}, timeout=30).raise_for_status()
    return {"ok": True}

The review id goes in external_id, so a redelivered webhook finds the existing ticket and stops. The comment is internal (public: False) and the requester defaults to the API user, so nobody outside the team gets an email. Zendesk’s API allows 200 to 700 requests a minute depending on the plan.

3. Send the answer back when the ticket is solved

In Zendesk, Admin Center → Apps and integrations → Webhooks → Create webhook: endpoint https://yourapp.example.com/zendesk/solved, method POST, JSON, and copy its signing secret. Then Objects and rules → Triggers → Add trigger:

JSON
{"review_id": "{{ticket.external_id}}", "ticket_id": "{{ticket.id}}", "reply": "{{ticket.ticket_field_360000000000}}", "agent": "{{current_user.name}}"}

(replace the number with your field id). Then the endpoint:

Python
def verify_zendesk(req) -> bool:
    ts = req.headers.get("X-Zendesk-Webhook-Signature-Timestamp", "")
    sig = base64.b64encode(hmac.new(os.environ["ZENDESK_WEBHOOK_SECRET"].encode(), (ts + req.get_data(as_text=True)).encode(), hashlib.sha256).digest()).decode()
    return hmac.compare_digest(sig, req.headers.get("X-Zendesk-Webhook-Signature", ""))

@app.post("/zendesk/solved")
def zendesk_solved():
    if not verify_zendesk(request):
        abort(401)
    d = request.get_json()
    text = (d.get("reply") or "").strip()
    if not d.get("review_id") or not text:
        return {"ok": True}
    res = requests.post(f"{PRISMA}/reviews/{d['review_id']}/reply", headers={**PH, "Idempotency-Key": f"zd-{d['ticket_id']}"}, json={"text": text}, timeout=30)
    note = "Reply queued for publishing on the platform." if res.ok else f"Not published: {res.json().get('error', {}).get('message', res.status_code)}"
    requests.put(f"{ZD}/tickets/{d['ticket_id']}.json", auth=ZAUTH, json={"ticket": {"comment": {"body": note, "public": False}}}, timeout=30)
    return {"ok": True}

The Idempotency-Key is the ticket id, so if Zendesk retries the webhook the reply is published once. When the platform doesn’t allow replies (422 reply_not_supported) or the text is too long (422 reply_too_long with max_length), the ticket gets an internal note saying so, and the agent answers on the platform with the link from the first comment.

4. Register the Prisma webhook

cURL
curl -X POST https://api.prismareviews.com/v1/webhooks -H "Authorization: Bearer $PRISMA_API_KEY" -H "Content-Type: application/json" \
  -d '{"url": "https://yourapp.example.com/webhooks/prisma", "events": ["review.created"]}'

Optionally subscribe a second endpoint to reply.published and reply.failed and add a final internal note on the ticket (find it by external_id), so the record shows when the answer actually went live.

Variants

The same webhook, other tools: HubSpot · Notion · Airtable · Intercom · Slack (approve replies) · Slack (alerts) · Microsoft Teams · Discord · Google Sheets.

FAQ

Can Zendesk import Google or App Store reviews?

Not natively for most platforms. Reviews become tickets through Zendesk’s Tickets API (POST /api/v2/tickets), created by your integration when a reviews webhook fires. Store the review id in the ticket’s external_id to avoid duplicates.

How does the agent’s answer get back to the review platform?

The agent writes the public answer in a custom ticket field and solves the ticket. A Zendesk trigger calls a Zendesk webhook with the ticket’s external_id and that field; your endpoint verifies the signature and publishes the text with POST /v1/reviews/{id}/reply on platforms that allow replies.

Why a custom field and not the ticket comment?

Ticket comments are for the conversation inside the team and may contain notes that must never be public. A dedicated Review reply field makes the published text explicit: what is in it is exactly what the customer will read on the platform.

Bad reviews in the support queue, answers back on the platform.

Get a free API key

2 locations free. Test keys accept replies without publishing, so the loop runs today.

Next step: see the 84 review platforms Prisma supports, and compare plans on Prisma Reviews pricing.

Get your API key in a minute

Free Spark plan: 2 locations, no card. Test keys return sample data on all 84 platforms, so you can build before connecting a real one.