Revup API, as shipped in the download
Revup REST API, Webhooks & MCP
Revup ships a JSON REST API and signed webhooks so your booking system, POS or CRM can queue review requests the moment a job finishes — and so a private message from an unhappy customer reaches your helpdesk quickly. Every customer who rates is shown your public review link, whatever the rating.
Authentication
Create a key in Settings → API & Webhooks. Send it on every call:
Authorization: Bearer apk_...
Shown once at creation, stored as a SHA-256 hash. Revoke any time — revoking the key is the off switch.
Every write checks the key holder's role, exactly as the screen does: a viewer's key cannot add a contact or create a review request (403 forbidden).
Endpoints
| Method & path | What it does |
|---|---|
GET /api/ping | Verify a key |
GET /api/contacts | Newest 500 contacts |
POST /api/contacts | Create a contact (name + email or phone; optional served_by, the team member their asks are credited to) |
GET /api/requests | Newest 200 requests; ?status= filters |
POST /api/requests | Queue (and by default send) review requests for a campaign; optional staff credits the team member who did the job (3.2.0). Each request carries staff and nps |
GET /api/feedback | Private messages from the rating page; ?handled=false for the open ones. Each carries reply_draft, reply_by and reply_sent_at (3.2.0) |
POST /api/feedback/{id}/draft | Save a reply draft on a private message: {"body": "..."}. Nothing is sent; a person sends it from the inbox (3.2.0). 409 once the reply was sent |
GET /api/testimonials | Consented testimonials with ready-made share links; ?status= pending, approved or hidden (3.2.0) |
GET /api/openapi.json | OpenAPI 3.0 description |
Queue requests after a job completes
curl -X POST https://reviews.example.com/api/requests \
-H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
-d '{"campaign_id":1,"contact_ids":[12,13,14]}'
Add "send": false to queue without sending — useful if you want a human to review the batch first.
Duplicate-suppressed. This reuses the same core the UI uses, so a contact who already has an open request for that campaign is not queued a second time. The response reports queued, sent, pending and failed counts so you can reconcile.
pending means the channel is not configured (no SMTP / no Twilio) — the request still exists and carries a gate_url you can send yourself.
Read the private messages
curl -H "Authorization: Bearer apk_..." \
"https://reviews.example.com/api/feedback?handled=false"
{ "feedback": [ { "id": 4, "request_id": 19, "contact_id": 7,
"rating": 2, "message": "Waited 40 minutes.",
"handled": false, "created_at": "2026-08-06 11:44:02" } ], "count": 1 }
These are the private messages. A customer who rates below your threshold is shown your public review link like everyone else, and is also offered a private message to you — it lands here. Pull them into your helpdesk and close the loop.
The payload deliberately carries no gate token: a token is a capability to submit a rating, and replaying it is not something an integration should be able to do.
MCP — the agent endpoint (new in 3.0)
Revup speaks MCP (Model Context Protocol) on one route, so an assistant can watch your review pipeline and queue follow-ups without anyone building a bridge first.
POST /mcp
Authorization: Bearer apk_... ← the SAME revocable key the REST API uses
Content-Type: application/json
Stateless streamable-HTTP: one JSON-RPC message per request, no session, no SSE. A GET gets 405 with Allow: POST, as the transport requires — and that answer is unauthenticated, so a client probing for MCP support learns you speak it before it has a key.
Connect it to an assistant
Mint the key in the app first: Settings → API keys. Choose Read only when the assistant should answer questions but never change anything — the endpoint then lists only the read tools and refuses the rest by name, so a careless prompt cannot write. Full access behaves as before.
Every client needs the same three facts, and nothing in the handshake is vendor-specific:
| The address | https://reviews.example.com/mcp |
| The key | header Authorization: Bearer apk_xxxx |
| The transport | MCP over streamable HTTP, stateless |
Claude — one command, or the same URL and header as a custom connector in the desktop and web apps:
claude mcp add --transport http revup https://reviews.example.com/mcp \
--header "Authorization: Bearer apk_xxxx"
ChatGPT and the OpenAI API — one entry in the Responses API's tools array (in ChatGPT itself, the same URL and key go in as a connector):
{
"type": "mcp",
"server_label": "revup",
"server_url": "https://reviews.example.com/mcp",
"authorization": "apk_xxxx",
"require_approval": "never"
}
**Every other client spells the same three facts differently — copy the shape from its own documentation, not from here.** VS Code is the clearest example of why: its configuration reference (read 6 September 2026) puts servers in .vscode/mcp.json under a "servers" object — *"an object that maps server names to their configurations"*. Pasted as-is, a block written for another client will not load there. The id, the URL and the token are what travel; the JSON around them belongs to whichever client you are configuring.
A local model, n8n, or your own code — n8n's MCP Client node takes the URL and the same Authorization: Bearer header; a model running on your own machine reaches it through any MCP client, so nothing leaves your network at all. Writing it yourself is one POST of JSON-RPC 2.0:
curl -X POST https://reviews.example.com/mcp \
-H "Authorization: Bearer apk_xxxx" -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/list"}'
Any client that speaks MCP's streamable-HTTP transport works, including ones that do not exist yet: Revup implements the protocol, not an integration with a particular vendor.
The tools
| Tool | Writes? | What it does |
|---|---|---|
list_requests | no | Review requests with status, rating and the customer they belong to. Filter by status, rated, limit. |
request_detail | no | One request, plus any private feedback left against it. |
create_request | YES | Queues a review request for a customer and mints its one-time rating link. |
response_stats | no | Response rate, average rating, how many raters opened your review page and how many left a private message — the dashboard's own figures — and the NPS summary (3.2.0). |
list_feedback | no | The private messages, each with the rating, the customer's first name, any reply draft and your reply templates for that rating (3.2.0). |
draft_reply | YES (a draft) | Saves a reply draft on one private message. It is never sent from here: a person reads, edits and sends it from the inbox (3.2.0). |
status is queued (scheduled inside a send window), pending (waiting to be sent or copied), sent, or failed — the same four words the Requests screen uses.
What the endpoint refuses, and why
- It cannot change the rating page. The threshold and the review URL are not writable over MCP. An agent may chase reviews; the page every customer sees stays with the owner.
- No figure is recomputed.
response_statsreturnsRevup::stats()— the same call the dashboard makes — so an agent can never quote a response rate Revup disagrees with. create_requestqueues, it does not send. Sending goes out through your own SMTP, your own Twilio, or as a link you copy; that is the owner's action and the owner's cost.- It writes requests through the product's own creator, so the one-open-request-per-customer rule and token minting apply exactly as they do in the browser. A second request for a customer who already has one open is refused.
- Arguments must be single values, unknown arguments are refused by name, and out-of-enum values are refused with the valid options listed.
- Roles apply exactly as they do in the browser. A viewer's key is refused
create_request.
Every MCP write lands in the audit trail beside the browser and API writes, naming the key's user as the actor and recording via: mcp.
Example
curl -X POST https://your-install/mcp \
-H "Authorization: Bearer apk_..." -H "Content-Type: application/json" \
-d '{"jsonrpc":"2.0","id":1,"method":"tools/call",
"params":{"name":"response_stats","arguments":{}}}'
Webhooks
Add a receiver in Settings → API & Webhooks and choose events:
| Event | Fires when |
|---|---|
request.sent | A review request is dispatched |
feedback.received | A customer sends you a private message from the rating page — wire this to your helpdesk |
rating.public | A customer opens your public review page from the rating page (any rating; once per request) |
testimonial.approved | You approve a testimonial a customer agreed to. Carries the quote, the name and share links for X, Facebook and LinkedIn, so your own automation can post it (3.2.0) |
Every delivery is signed:
X-Revup-Event: feedback.received
X-Revup-Signature: sha256=<hmac_sha256(raw_body, your_webhook_secret)>
Compare with hash_hmac('sha256', $rawBody, $secret) and reject on mismatch.
Inbound: automatic asks (3.2.0)
Settings → Automatic asks gives each trigger a private address, POST /hooks/in/{key}. Another app posts to it when a job, booking or sale ends; Revup finds or adds the customer and asks them through the trigger's campaign — at once, or in the trigger's own send window.
- Signed. With a signing secret set, a delivery carries `X-<App>-Signature: sha256=<HMAC-SHA256 of the raw body>
(any header ending in-Signature); a wrong one answers401` and creates nothing. Without a secret, the address itself is the key. - Generic JSON.
emailorphone(andname,staff,id) at the top level or insidecustomer,contactordata. Form-encoded bodies work too. - Jobora.
job.completed(andjob.signed_off). The customer's email is read from Jobora's ownGET /api/customerswith the key you give the trigger; the assigned technician is credited. - Slotly.
appointment.createdandappointment.rescheduledqueue the ask for the morning after the appointment ends,appointment.cancelledwithdraws it,payment.recordedasks if nothing was queued. The customer is read from Slotly'sGET /api/v1/appointments/{id}. - Once per customer. A customer asked in the last N days (30 by default), by any app or person, is not asked again; the delivery answers
200with"outcome":"duplicate"so the sender does not retry. Every delivery is logged on the Automatic asks page.
curl -X POST https://reviews.example.com/hooks/in/<key> \
-H 'Content-Type: application/json' \
-H "X-Signature: sha256=$(printf '%s' "$BODY" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)" \
-d "$BODY"
Public widget (3.2.0)
When you switch the widget on (Testimonials), three public addresses answer; each sets no cookie and is cacheable for five minutes. Switched off, they answer 404.
| Address | What it is |
|---|---|
GET /widget.js | The two-line embed's script: frames /widget and sizes it |
GET /widget | The widget page: approved comments and, if chosen, the average of every rating |
GET /widget.json | The same data as JSON, open to any site (CORS), for drawing it yourself |
Email notifications
Revup already had its own SMTP settings and mailer, so 2.0 adds no new mail configuration — just a notify address and one toggle in Settings: email me the moment a customer sends a private message. That is the one alert where minutes matter.
Errors
| Code | Meaning |
|---|---|
401 | Missing, unknown or revoked key |
422 | Validation — detail names the offending field |
Connect it to Zapier, Make or n8n
RevUp talks to other tools without a plugin: it sends a webhook (an HTTPS message) the moment something happens, and it accepts calls to the API above. Zapier, Make and n8n can use both.
Before you start
- In RevUp, open Settings → API & Webhooks. Mint an API key (choose Read only if the other tool only reads), and add a webhook when you have its URL (below).
- Use your install's full address, for example
https://revup.yourcompany.com.
When something happens in RevUp: send it to another tool
- In the other tool, create the trigger that gives you a URL to receive webhooks: - Zapier: the "Webhooks by Zapier" app, trigger Catch Hook (Webhooks by Zapier needs a paid Zapier plan). - Make: module Webhooks → Custom webhook. - n8n: the Webhook node.
- Copy that URL into RevUp under Settings → API & Webhooks, and choose the event or events below.
- Make the event happen once in RevUp, so the other tool receives a sample and you can map its fields.
| Event | Fires when |
|---|---|
request.sent | A review request is dispatched |
feedback.received | A customer sends you a private message from the rating page — wire this to your helpdesk |
rating.public | A customer opens your public review page from the rating page (any rating; once per request) |
testimonial.approved | You approve a testimonial a customer agreed to. Carries the quote, the name and share links for X, Facebook and LinkedIn, so your own automation can post it (3.2.0) |
Every delivery carries X-Revup-Signature, an HMAC-SHA256 signature over the raw body. Zapier and Make accept the delivery without checking it, so keep the webhook URL private. In n8n you can check it in a Code node before using the data.
When something happens elsewhere: act in RevUp
- Zapier: "Webhooks by Zapier", action POST (or Custom Request).
- Make: module HTTP → Make a request.
- n8n: the HTTP Request node.
Send the request to your install's address plus the path from the Endpoints table above, with the header Authorization: Bearer <your API key> and a JSON body. A read-only key cannot write.
The endpoints that change something:
POST /api/contacts: Create a contact (name+emailorphone; optionalserved_by, the team member their asks are credited to)POST /api/requests: Queue (and by default send) review requests for a campaign; optionalstaffcredits the team member who did the job (3.2.0). Each request carriesstaffandnpsPOST /api/feedback/{id}/draft: Save a reply draft on a private message:{"body": "..."}. Nothing is sent; a person sends it from the inbox (3.2.0).409once the reply was sent
Example recipes
- request.sent → a row in Google Sheets: webhook trigger on
request.sent(A review request is dispatched), then the Google Sheets "Create row" action with the fields you want. - feedback.received → a message in Slack or Teams: webhook trigger on
feedback.received(A customer sends you a private message from the rating page — wire this to your helpdesk), then the Slack or Microsoft Teams "Send message" action. - A new form entry or row elsewhere → RevUp: the other tool's trigger (for example a new Google Sheets row or a form submission), then an HTTP action calling
POST /api/contacts(Create a contact (name+emailorphone; optionalserved_by, the team member their asks are credited to)).
These recipes use only the events and endpoints documented above. We have not run them inside Zapier, Make or n8n ourselves; RevUp's own automated tests check its API.