Before you begin
- A destination app or an HTTPS endpoint you control
- Permission to configure that system
- A test call to your Penny number with sample details
Step 1
Choose the path that fits the receiving system
Use Zapier when your app already has a suitable action. Use a signed webhook when your team owns the receiving endpoint. Business adds a reports API: a summary for a date range, as JSON or CSV. It can’t change settings or create bookings.
Step 2
Connect the destination and keep credentials private
For Zapier, create an API key in Settings → Integrations, open the invite link shown there, and connect the Penny app in Zapier with the key. Or use Webhooks by Zapier’s Catch Hook (paid Zapier plans) and add its URL as a webhook. For your own receiver, add the HTTPS URL as a webhook in Settings → Integrations and store the signing secret when it is shown; you won’t see it again. To post to Slack or Microsoft Teams, add that app’s “Send Channel Message” action to a Zap with a Penny trigger.
Keep the API key and the signing secret out of browser code, screenshots, and support messages. A webhook receiver checks the signature against the timestamp and the raw request body, and refuses a timestamp more than five minutes old. The signature reference below shows how.
Step 3
Map fields to the action you want
Map the caller’s details, the answers Penny took down, and her summary to the destination. When a delivery fails, Penny sends the same event again with the same id, so store each event once by its id. One conversation can send several events: the completed conversation, plus one for each booking, request, lead, or message. Choose the ones your workflow needs.
For HubSpot, Salesforce, or QuickBooks, the Zap or receiver defines the destination object. A call event should create the task or intake record your team has chosen; it is not proof of a paid invoice or a scheduled job.
Step 4
Inspect one delivery from call to destination
Press Test in Settings → Integrations to send a sample event, then make one test call and compare the Penny record with the receiving app. Check missing fields and the destination status. For a custom receiver, check rejected signatures and repeated events as part of your integration test.
On Business, the same API key reads your reports: request GET /export/reports with a date range, as JSON or CSV. Verify one read before building a reporting workflow.
The events Penny sends
Calls and website chats send the same events. conversation.completed arrives once for every call or chat that ends. booking.created arrives for each appointment Penny booked, with its time; request.created for each appointment request Penny takes for your office to confirm; lead.created for each new customer’s details; and message.created for each message, a voicemail included.
A webhook can take any of these events, chosen when you add it. A Zap takes the one its trigger names. A booking that was cancelled, or that Penny couldn’t confirm, sends no booking event; the completed conversation still arrives. Try Penny chats in your dashboard are marked Test and send nothing. Pressing Test sends one event of type test with sample details.
What an event contains
Each event is one JSON object. id stays the same on every delivery attempt, so it is what you deduplicate on. type names the event; there is no version field. conversation holds the channel (voice or web), the language (en or es), the start and end times, the caller’s phone and name when Penny has them, her summary, and a link to the conversation in your Inbox. It names the location only when your account had more than one running during the conversation.
details lists the answers Penny took down, each under your label. source says what produced the event: conversation, capture, voicemail, or booking. A booking.created event adds appointment: its start, end, time zone, and the appointment type as you named it. Times are ISO 8601 timestamps. Penny removes the card, bank account, and ID numbers she recognizes before an event is sent; a number given without saying what it is can still come through.
{
"id": "evt_5c1f8e0a9b2d4c7e8f6a1b3d5e7f9a0c",
"type": "booking.created",
"occurredAt": "2026-11-03T22:14:09.000Z",
"business": { "id": "biz-1234", "name": "Example Plumbing" },
"conversation": {
"id": "CA0123456789abcdef0123456789abcdef",
"channel": "voice",
"language": "en",
"startedAt": "2026-11-03T22:09:41.000Z",
"endedAt": "2026-11-03T22:13:55.000Z",
"caller": { "phone": "+13125550100", "name": "Sample Caller" },
"summary": "Sample Caller booked a water heater inspection for Tuesday morning.",
"url": "https://pennyassistant.com/dashboard/inbox/CA0123456789abcdef0123456789abcdef",
"location": { "id": "loc-north", "name": "North Side" }
},
"source": "booking",
"details": [
{ "label": "Name", "value": "Sample Caller" },
{ "label": "Phone", "value": "+13125550100" },
{ "label": "How can we help?", "value": "Water heater inspection" }
],
"appointment": {
"start": "2026-11-10T15:00:00.000Z",
"end": "2026-11-10T16:00:00.000Z",
"timezone": "America/Chicago",
"type": "Inspection"
}
}The Penny app in Zapier
The Penny app is invite-only: open it from the invite link in Settings → Integrations and connect it with your API key. It has five triggers: Conversation Completed, New Booking, New Request, New Lead, and New Message. Zapier’s trigger test loads a sample event from Penny, so you can map fields before a real call arrives.
Turning a Zap on adds it to the list in Settings → Integrations, and turning it off in Zapier removes it. Its Details field holds every answer as one block of text, ready for a Slack or Microsoft Teams message. A Catch Hook added as a webhook receives the same events, signed like any webhook.
Verify a webhook’s signature
Penny signs every webhook post with the signing secret shown when you added the webhook; it starts whsec_. The X-Penny-Signature header reads t=<unix seconds>,v1=<hex>. The hex is the HMAC-SHA256, keyed with the secret, of the timestamp, a period, and the raw request body.
Recompute it over the bytes you received, before parsing them, and compare in constant time. Refuse a timestamp more than 300 seconds from your clock, so a captured request can’t be replayed later. Answer with a 2xx once the event is stored, then do the work. A Zap connected through the Penny app isn’t signed: the hook address Zapier made for it is the credential.
import { createHmac, timingSafeEqual } from "node:crypto";
// rawBody: the request body as received, as a Buffer, before any parsing.
// header: the X-Penny-Signature header. secret: the webhook's whsec_ secret.
export function isFromPenny(rawBody, header, secret, nowSeconds = Date.now() / 1000) {
const parts = /^t=(\d+),v1=([a-f0-9]{64})$/.exec(header ?? "");
if (!parts || Math.abs(nowSeconds - Number(parts[1])) > 300) return false;
const expected = createHmac("sha256", secret).update(parts[1] + ".").update(rawBody).digest();
return timingSafeEqual(expected, Buffer.from(parts[2], "hex"));
}Delivery, retries, and pausing
A new webhook stays off until it answers Test with a 2xx. Penny posts over HTTPS to a public address, waits up to 8 seconds, and doesn’t follow redirects. Any 2xx counts as delivered.
When a post fails, Penny tries that destination again after 1 minute, 5 minutes, 30 minutes, 2 hours, and 6 hours, with the same event and id: six attempts in about nine hours. Fifteen failed posts in a row pause a destination and email you; fix the receiver, then press Test to turn it back on. An account can have up to 25 destinations.
The API key
Each account has one API key, from Starter. Create it in Settings → Integrations; Penny shows it once, so copy it then. It starts penny_ and goes in the Authorization: Bearer header. It connects the Penny app in Zapier, and on Business it reads your reports. It can’t change settings or create bookings.
Rotating or revoking the key stops the old one immediately and turns off the Zaps it connected. Connect the Penny app in Zapier again with the new key, then turn those Zaps back on.
The reports API
On Business, GET https://api.pennyassistant.com/export/reports?from=2026-11-01&to=2026-11-30 with your API key returns the report your dashboard shows, as JSON. Add &format=csv for a CSV file, with one row per location when you have more than one. from and to are your business’s own dates, both included, at most a year apart. An account below Business gets a 402 answer.
You are ready when
The receiving system contains one correctly mapped test record, linked to the call that produced it.
If the test does not pass
- If a Zap receives no sample, press Test in Settings → Integrations and check the selected trigger.
- If signature verification fails, check that you verify the unmodified body with the current secret. To change the secret, add the webhook again and update your receiver.
- If the destination rejects a field, inspect its required properties and map the missing intake detail.
Questions about this setup
Which route should I use?
Choose Zapier for an app with an action you need, a signed webhook for a receiver your team controls, or the Business reports API for reporting on your own schedule. Zapier and webhooks are included from Starter.
How do I keep a retry from creating a second record?
Store each event once by its id: a retry carries the same id, and every new event has a new one. Test that the receiving system handles the same event twice without creating another record.
Does Penny post to Slack or Microsoft Teams directly?
No. Pair a Penny trigger in Zapier with the Slack or Microsoft Teams “Send Channel Message” action. The trigger’s Details field puts every answer in the message.