Integrations
Make
Make receives UpVoiced events through a custom webhook. A filter right after it recomputes the signature with Make’s own sha256 function, so later modules only see deliveries UpVoiced really sent.
Receive events
- In a new scenario, add Webhooks › Custom webhook and click Add. Under Advanced settings, set Get request headers to Yes and JSON pass-through to Yes: the signature covers the raw body, so the scenario needs the JSON as text, exactly as sent, plus the headers. Copy the webhook address.
- In UpVoiced, add a webhook endpoint with that address (owners and admins: Settings → Integrations → Webhooks, or
POST /v1/webhooks) for the events this scenario handles. Copy the signing secret (whsec_…): it is shown once. - Click Run once in Make, then make UpVoiced send something: Send test on the endpoint (or
POST /v1/webhooks/{id}/test), or create a test post. Make learns the data structure from that request. - Add Tools › Set variables after the webhook (module 2 below) with two variables, mapping from the webhook (module 1):
signature:{{get(map(1.headers; "value"; "name"; "x-upvoiced-signature"); 1)}}timestamp:{{replace(first(split(get(map(1.headers; "value"; "name"; "x-upvoiced-signature"); 1); ",")); "t="; "")}}
mappicks the value namedx-upvoiced-signature, and the timestamp is its first part aftert=. - Add JSON › Parse JSON (module 3) with the webhook’s body text as the JSON string, then the modules you want.
- Click the link between modules 2 and 3 and add a filter named “Signed by UpVoiced” with the two conditions below (both must pass). Then turn the scenario on.
The signature filter
| Condition | Operator | Value |
|---|---|---|
{{2.signature}} | Text operators: Contains (case sensitive) | v1= followed by the sha256 function described below |
{{abs(formatDate(now; "X") - 2.timestamp)}} | Numeric operators: Less than or equal to | 300 |
Fill the three arguments of sha256(text; encoding; key):
| Argument | Put there |
|---|---|
| text | The timestamp variable, a dot (.) typed right after it, then the webhook’s body text (the JSON pass-through value), with no spaces in between. |
| encoding | hex |
| key | Your whsec_… signing secret, the whole string. Anyone who can edit the scenario can read it. |
With a key, Make’s sha256 returns the HMAC-SHA256 that UpVoiced puts after v1=. During a secret rotation the header carries one v1= per valid secret, so contains (not equals) is the right operator. Bundles that fail the filter stop there; Make has already answered UpVoiced with 200 Accepted, so they are not retried.
Check your formula
Before real deliveries arrive, evaluate your sha256 function once with these fictional values (for example in a temporary Set variables module). It must return the expected value exactly.
| key | whsec_FictionalDocsTestVector00000000000000000000 |
|---|---|
| timestamp | 1792663200 |
| body | {"id":"evt_0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c21","type":"post.status_changed","occurred_at":"2026-10-22T12:00:00.000Z","workspace":{"id":"0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c0d","slug":"acme"},"data":{"post":{"id":"0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c13","slug":"export-to-csv-9c13","title":"Export to CSV","body_md":"Fictional request: export the whole board to CSV.","url":"https://acme.upvoiced.app/p/export-to-csv-9c13","board":{"id":"0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c10","slug":"feedback","name":"Feedback"},"status":{"id":"0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c12","name":"Shipped","category":"done"},"author":{"kind":"end_user","id":"0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c14"},"vote_count":12,"comment_count":3,"merged_into_post_id":null,"deleted":false,"created_at":"2026-10-20T09:30:00.000Z"},"from_status":{"id":"0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c11","name":"Planned","category":"active"},"to_status":{"id":"0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c12","name":"Shipped","category":"done"}},"actor":{"kind":"operator","id":"0190f5e2-4c1d-7a3b-8c2d-5e6f7a8b9c15"}} |
| expected | fdc1624bf4399ef9ea41aea9e76ef542105d9da4b6b15dce437d1f41feb24c56 |
Recipes
After Parse JSON, map type, id (the event id, the same on every redelivery: use it as the unique key of what you create) and the fields under data. To handle several events in one scenario, add a Router with a filter on type per route.
New post: post.created
A post is created and is public at once (a post held for approval is not sent as post.created; its approval arrives as post.updated).
Useful items: data.post.title, data.post.url, data.post.board.name, data.post.status.name, data.post.vote_count. Add a row to a spreadsheet, create a card in your planning tool, notify a channel.
Status change: post.status_changed
A post moves to another status, for example from Planned to Shipped.
Useful items: data.post.title, data.post.url, data.from_status.name, data.to_status.name, data.post.vote_count. Update the linked record in your CRM, post a release note draft, alert the team.
Survey response: survey.response.created
A customer answers a Pulse survey (NPS, CSAT, CES and others) on a routing rule that sends responses to your endpoint.
Not subscribable yet. It arrives with Pulse survey routing rules, which send each matching response to a chosen endpoint.
Useful items: data.survey.slug, data.survey.type, data.response.score, data.response.band, data.response.text, data.response.followup.text. Open a follow-up task for detractors, log scores in a sheet, thank promoters.
Create posts from a scenario
Any module’s output (a form answer, a CRM record, a help-desk conversation) can become a post through POST /v1/posts. Add HTTP › Make a request:
- Authentication type: API key. Create a keychain with Key
Bearer uv_live_…(an API key with thewritescope), API key placement in the header and API key parameter nameAuthorization. - URL
https://api.upvoiced.com/v1/posts, Method POST. - Headers:
X-Workspace= your workspace id, andIdempotency-Key= a value built from the source record’s id (8 to 128 characters from[A-Za-z0-9_.:-]), so a re-run creates one post. - Body content type application/JSON with the Data structure input method, which escapes quotes and line breaks in mapped values for you. Fields below.
- Parse response: Yes, to map the created post’s
idlater.
| Field | Value |
|---|---|
board_id | Required. The board’s id (list boards with GET /v1/boards). |
title | Required. Up to 140 characters. |
body_md | Optional description in Markdown. |
on_behalf_of_email | Optional. The customer becomes the author and a follower, and gets a “we logged your request” email. |