Prisma Reviews / Guides / Reviews to Airtable
Send customer reviews to Airtable automatically: upsert by review id
Airtable is where many small teams already track locations, clients and tasks, and a reviews table next to them is useful: filter by rating, group by platform, build an interface for the people who answer. Airtable has no reviews integration, so rows come from its REST API, and the one feature that makes this clean is upsert: one call that creates the row the first time and updates it later. This guide does it in Python with a Prisma webhook (one event per new review on 84 platforms), then shows the backfill and the n8n version.
pip install flask requestsYou need a Prisma API key and webhook secret, an Airtable personal access token with data.records:read and data.records:write on the base, the base id (app…) and the table name or id.
1. The table
Fields, exactly named: Review ID (single line text), Title (single line text), Rating (number), Platform (single select), Author (single line text), Date (date, with time), Location (single line text or link), Text (long text), URL (URL), Replied (checkbox). Airtable creates select options on the fly when typecast is true, so new platforms don’t break the insert.
2. Receive the webhook and upsert the row
import hashlib, hmac, os, time, 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']}"}
SECRET = os.environ["PRISMA_WEBHOOK_SECRET"]
AT = {"Authorization": f"Bearer {os.environ['AIRTABLE_TOKEN']}", "Content-Type": "application/json"}
TABLE = f"https://api.airtable.com/v0/{os.environ['AIRTABLE_BASE']}/{os.environ.get('AIRTABLE_TABLE', 'Reviews')}"
def verify(raw: bytes, header: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(","))
expected = hmac.new(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 fields(r: dict) -> dict:
return {"Review ID": r["id"], "Title": r.get("title") or "", "Rating": r.get("rating"), "Platform": r["platform"],
"Author": r.get("author") or "", "Date": r["created_at"], "Location": r["location_id"],
"Text": r.get("text") or "", "URL": r.get("url") or "", "Replied": bool(r.get("reply"))}
def upsert(reviews: list[dict]):
for i in range(0, len(reviews), 10): # 10 records per request
body = {"performUpsert": {"fieldsToMergeOn": ["Review ID"]}, "typecast": True,
"records": [{"fields": fields(r)} for r in reviews[i:i + 10]]}
res = requests.patch(TABLE, headers=AT, json=body, timeout=30)
if res.status_code == 429:
time.sleep(30); res = requests.patch(TABLE, headers=AT, json=body, timeout=30)
res.raise_for_status()
@app.post("/webhooks/prisma")
def prisma_webhook():
if not verify(request.get_data(), request.headers.get("Prisma-Signature", "")):
abort(400)
event = request.get_json()
if event["type"] in ("review.created", "review.updated", "reply.published"):
review = requests.get(f"{PRISMA}/reviews/{event['data']['id']}", headers=PH, timeout=30).json()
upsert([review])
return {"ok": True}Because the merge key is Review ID, the same handler serves three events: review.created inserts the row, review.updated refreshes an edited review, and reply.published flips Replied to true. Redeliveries after a failure (same event, same review id) just update the same row. Airtable allows 5 requests a second per base.
3. Register the webhook
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", "review.updated", "reply.published"]}'4. Backfill the history
params, batch = {"limit": 100}, []
while True:
page = requests.get(f"{PRISMA}/reviews", headers=PH, params=params, timeout=30).json()
batch.extend(page["data"])
if not page.get("next_cursor"):
break
params["cursor"] = page["next_cursor"]
upsert(batch) # 10 per request, paced on 4295. The same with n8n, no code
Prisma Reviews trigger (new review) → Airtable node, operation Upsert (Update or create), matching column Review ID, with the fields above mapped. The Prisma node is verified on n8n Cloud (n8n setup). Zapier and Make: catch hook → Prisma “get review” → Airtable “find or create record” (webhooks in Zapier and Make).
Variants
- Per-location tables: link Location to a Locations table with the Prisma
location_idin a field, and usetypecastto resolve the link by that value. - Interface for the answering team: an Airtable Interface filtered on Rating ≤ 2 and Replied unchecked, with the URL button to answer on the platform; Prisma flips the checkbox when the reply is published.
- Rating over time: a rollup on the Locations table (average of Rating, count of records) gives the dashboard without a BI tool; for reply rate and response time per location, GET /v1/stats returns them ready-made.
The same webhook, other tools: HubSpot · Notion · Zendesk · Intercom · Slack (approve replies) · Slack (alerts) · Microsoft Teams · Discord · Google Sheets.
FAQ
Does Airtable have a Google reviews integration?
No. Airtable’s integrations don’t cover review platforms. Rows are created through Airtable’s REST API (PATCH or POST /v0/{baseId}/{table}) by your script, n8n, Zapier or Make, fed by a reviews webhook.
How do I keep Airtable rows from duplicating?
Use Airtable’s upsert: PATCH /v0/{baseId}/{table} with performUpsert and fieldsToMergeOn set to a Review ID field. The same call creates the row the first time and updates it later, for example when the reply is published.
What are Airtable’s API limits?
5 requests per second per base and 10 records per create or update request; a 429 asks you to wait 30 seconds. Batch the backfill in groups of 10 and pace it.
Every review, one Airtable base, no duplicates.
2 locations free. Test keys return sample reviews, so the table fills today.