Rated 5.0 out of 5 on G2
API Reference
HummingDeck exposes a REST API for integration partners and automation platforms. Endpoints authenticate with a Bearer token and return JSON responses.
https://app.hummingdeck.com/api/v1Authentication
Every API request carries a Bearer token in the Authorization header. Two kinds of credential are accepted, and they behave differently.
Method
Bearer token
Header format
Authorization: Bearer {access_token}
Credential types
Workspace API token
Authorization: Bearer hd_api_...
REST API access is available by request on Business and is enabled per workspace after review. Workspace owners and admins then create separately named API keys in Workspace settings, Integrations, HummingDeck API. Choose only the permissions each integration needs. A key is shown once when it is created and cannot be retrieved afterwards. It expires one year after creation and stays permanently bound to its workspace, so a request cannot select or override the workspace.
A workspace can have up to 20 active API keys. Replacing one key immediately invalidates only its previous secret; other keys keep working. Owners and admins can switch off one key or every key at any time. Revocation is permanent for that secret.
A workspace API key can call only the operations allowed by its selected permissions. Webhook subscription endpoints are not available to workspace API keys.
Zapier OAuth
Authorization: Bearer {access_token}
Issued through the OAuth authorization flow when a workspace connects the Zapier integration. Access tokens expire after 30 days. Use the refresh token, which lasts 90 days, to obtain a new access token without re-authorizing.
This is the only credential that can create or delete webhook subscriptions.
Permissions
Choose at least one permission. Write permissions also include matching read access. You can change permissions when you replace the key.
rooms:readView rooms, tabs, items, links, and labels.
rooms:writeCreate and manage rooms, tabs, items, links, and labels.
plan:readView Mutual Action Plan phases and tasks.
plan:writeCreate and manage Mutual Action Plan phases and tasks.
analytics:readView engagement analytics, activity, and captured emails.
crm:readFind workspace companies and contacts.
crm:writeCreate or update companies, contacts, and link audiences.
documents:readFind documents and read their metadata.
documents:writeUpload documents and attach documents or URLs to rooms.
Permission labels on endpoint rows apply to workspace API keys. Required permissions always apply, also-required permissions are needed together, and conditional permissions apply only when the request uses the related filters or fields. GET /me needs no permission. Zapier OAuth uses its fixed integration access.
When a request returns 401
A request returns 401 when the key is unknown or malformed, has expired, has been switched off, belongs to a workspace whose API access was turned off, or was issued by someone who is no longer an owner or admin of that workspace.
Test your connection
Verify your token is valid and see the authenticated user's profile.
/meReturns the current user's name, email, and team information.
No API-key permission required
Documents
Upload, search, and manage documents (PDFs, slide decks, proposals, and other files).
/decksUpload a new document. Send as multipart/form-data with a file field (PDF, PPTX, DOCX, XLSX, XLS, HTML) and a title field. The API upload limit is 30 MB. Processing continues after the upload; the response includes processingStatus.
documents:write/decksList up to 20 documents, newest first. Use the optional title query parameter for a case-insensitive partial-title filter.
documents:readGET /decks Response fields
| Field | Type | Description |
|---|---|---|
| id | string | Document ID |
| title | string | Document title |
| fileType | string | Document MIME type |
| pageCount | integer | null | Number of pages |
| thumbnailUrl | string | null | Thumbnail image URL |
| processingStatus | string | pending, processing, completed, or failed. A document can go into a room while it is processing; send a link to it once it reads completed. |
| processingErrorCode | string | null | Why processing failed, when it did |
| createdAt | string | ISO 8601 timestamp |
POST /decks Response fields
| Field | Type | Description |
|---|---|---|
| id | string | Document ID |
| title | string | Document title |
| fileType | string | Document MIME type |
| processingStatus | string | pending, processing, completed, or failed. A document can go into a room while it is processing; send a link to it once it reads completed. |
| processingErrorCode | string | null | Why processing failed, when it did |
Rooms
Create deal rooms with their documents and audience link in one call, find rooms, change their settings, archive and restore them, and arrange their tabs and items. Available only to workspace API tokens; Zapier OAuth credentials are rejected.
/roomsList rooms, newest first. Filter with search, status (active, archived, or all), and companyId. Pages hold 25 rooms (up to 100 with limit); pass a page's nextCursor as cursor for the next one.
rooms:readcrm:read(Required when the companyId filter is present.)/roomsCreate a room with its documents and first audience link in one call.
rooms:writedocuments:write(Required when documentIds contains one or more document IDs.)crm:write(Required when the request supplies contactId, recipientName, recipientEmail, companyId, companyName, or when either primaryLink.allowedEmails or primaryLink.allowedDomains is non-empty.)/rooms/{roomId}Return the room's settings, its tabs and items in display order, and its link counts.
rooms:read/rooms/{roomId}Change the name, welcome message, point of contact, company, or contact.
rooms:writecrm:write(Required when companyId or contactId is present, including null to detach the association.)/rooms/{roomId}/archiveArchive the room. Its links stop working.
rooms:write/rooms/{roomId}/restoreRestore an archived room. Its links work again.
rooms:write/rooms/{roomId}/tabsAdd a tab, at a position or last.
rooms:write/rooms/{roomId}/tabs/{tabId}Rename a tab.
rooms:write/rooms/{roomId}/tabs/orderPut every tab in a new order.
rooms:write/rooms/{roomId}/tabs/{tabId}Remove a tab that shows no items.
rooms:write/rooms/{roomId}/itemsAdd a document, URL, embed, or section divider to a tab.
rooms:writedocuments:write(Required when type is document or url.)/rooms/{roomId}/items/{itemId}/moveMove an item to the end of another tab.
rooms:write/rooms/{roomId}/items/orderPut one tab's items in a new order.
rooms:write/rooms/{roomId}/items/{itemId}Take an item out of the room. It stays in your library.
rooms:write/rooms/{roomId}/linksList the room's audience links, newest first, with each Restricted link's active invitees.
rooms:read/rooms/{roomId}/linksCreate another attributed audience link, in any access mode, for an active room.
rooms:writecrm:write/rooms/{roomId}/links/{linkId}Turn a link on or off, set or clear its expiry, or replace its allowlist.
rooms:writecrm:write(Required when allowedEmails or allowedDomains is present, including an empty array that clears the audience.)/rooms/{roomId}/action-planReturn the room's action plan: its settings, phases, tasks (internal ones included), dependencies, and progress.
plan:read/rooms/{roomId}/action-planChange the plan's settings, including whether the people who open the room may tick their own tasks off.
plan:write/rooms/{roomId}/action-plan/phasesAdd a milestone. Leave color out and phases rotate teal, peach, blue by order.
plan:write/rooms/{roomId}/action-plan/phases/{phaseId}Rename a phase, move it, retarget its date, or set its colour. Sending color null restores the rotation.
plan:write/rooms/{roomId}/action-plan/phases/{phaseId}Remove a phase. mode is required: delete_tasks or move_to_unphased, so tasks are never removed by accident.
plan:write/rooms/{roomId}/action-plan/tasksAdd a task. assignee is null, a side on its own for the company that owns it, or a side with an email for a named person.
plan:write/rooms/{roomId}/action-plan/tasks/{taskId}Update a task. Omitting assignee leaves ownership alone; sending null clears it.
plan:write/rooms/{roomId}/action-plan/tasks/{taskId}Remove a task. Its subtasks go with it.
plan:write/rooms/{roomId}/action-plan/tasks/{taskId}/statusComplete or reopen a task on the workspace's behalf. A task behind an unfinished dependency returns 409 TASK_BLOCKED.
plan:write/rooms/{roomId}/analyticsRoom visits, unique viewers, average time, documents opened of total, and average completion. Bots excluded.
analytics:read/rooms/{roomId}/activityWhat happened in the room, newest first. Discussion entries name the sender and never carry the message. Narrow with since.
analytics:read/rooms/{roomId}/captured-emailsAddresses the room collected. source is verify when the visitor confirmed it with a one-time link, ask when they only typed it.
analytics:read/room-viewsRoom entries across the workspace, newest first. Someone entering a room has no other home; /views covers document views only.
analytics:read/room-labelsList the workspace's room labels with how many rooms use each. Find label IDs here before tagging a room.
rooms:read/room-labelsCreate a label. Names are unique per workspace, ignoring case; color is a #RRGGBB hex value.
rooms:write/room-labels/{labelId}Rename a label, change its colour, or edit its description.
rooms:write/room-labels/{labelId}Delete a label and its assignments. The rooms that carried it are untouched; the response says how many lost it.
rooms:writeCreate a room in one call
Upload each file with POST /decks, then create the room for the recipient's company with a Restricted link for the people who should see it. The company, contacts, room, documents, and link are created together: if the call is refused, none of them is. Documents can go into the room while they are still processing. The room's items report processingStatus, so send the link once every document reads completed.
{
"name": "Acme renewal",
"companyName": "Acme Inc",
"recipientName": "Pat Buyer",
"recipientEmail": "pat@acme.example",
"documentIds": [
"{documentId}",
"{documentId}"
],
"primaryLink": {
"accessMode": "verified-allowlist",
"allowedEmails": [
"pat@acme.example",
{
"email": "cfo@acme.example",
"name": "Sam Rivera"
}
],
"allowedDomains": [
"acme.example"
]
}
}accessMode is open (anyone with the URL), verify-any (visitors confirm their email address with a one-time link), or verified-allowlist (only the addresses in allowedEmails and anyone at the domains in allowedDomains). The API adds nobody to a Restricted link on its own, so include your own address if you want to preview the room. An option your plan does not include returns 403 FEATURE_NOT_AVAILABLE, and an unknown field returns 400, so a room never opens to a different audience than the one you asked for.
Arrange tabs and items
Start from the room as it is now: reading a room returns its tabs and items in display order, and each item reports its tab and its position in it, counting from 0. Add tabs and items at a position, move items between tabs, and send a tab's complete new order. An order must list every item in the tab exactly once, so read the room again if another change landed in between. A tab can be removed once it shows no items.
{
"type": "section",
"label": "Commercials",
"tabId": "{tabId}",
"position": 0
}Supported embed providers
Embeds accept a share link or an embed link and normalize it to the provider's embed form. Anything outside this set returns 400 EMBED_PROVIDER_NOT_SUPPORTED.
| Field | Type | Description |
|---|---|---|
| Video | Loom, YouTube, Vimeo, Wistia, Vidyard | |
| Scheduling | Calendly, Cal.com, SavvyCal, Google Calendar | |
| Forms | Typeform, Tally, Google Forms, Jotform, Fillout | |
| Design | Figma, Miro, Canva, Whimsical | |
| Docs and tables | Google Docs, Google Sheets, Notion, Coda, Airtable | |
| Presentations | Google Slides, Pitch, Gamma, Guideflow, Flipsnack, Prezi | |
| Audio | Spotify, SoundCloud |
Add another audience link
Every room already has one link from POST /rooms; add more for audiences that need different attribution or access. Provide at least one of recipientName, recipientEmail, contactId, companyId, or companyName. accessMode takes the same open, verify-any, or verified-allowlist values as primaryLink, with the same fields (requireEmail, allowedEmails, allowedDomains, label, expiresAt, allowDownloads). A refused call, including a plan gate, leaves no link, company, or contact behind.
{
"companyName": "Analytical Engines",
"accessMode": "verified-allowlist",
"allowedEmails": [
{
"email": "cfo@analytical.example",
"name": "Sam Rivera"
}
]
}Update a link
Four fields: isActive, expiresAt, allowedEmails, allowedDomains (the last two on verified-allowlist links only). accessMode and the slug never change; create a new link instead. Turning a link back on rechecks the plan's active-link limit.
{
"isActive": false
}Build the action plan
Every room owns exactly one plan, so it mounts under the room without an ID of its own. Most tasks belong to a company rather than a person: send a side on its own and the plan reads it as the company, which is what you want when you do not know who on the other side will do the work. Add an email only when you know the individual. An internal task is never shown in the room, so it cannot belong to the recipient side.
{
"title": "Sign the NDA",
"assignee": {
"side": "buyer"
},
"dueDate": "2026-10-02"
}recipientCompletionEnabled on the plan decides whether the people who open the room may tick their own side's tasks off. It defaults to true, and it is the only gate: the API never asks a caller to supply a recipient's address in order to complete a task. Who ticked each one is recorded at whatever confidence the room's access mode gives.
Tag rooms with labels
Labels are workspace-wide, so create them once and reuse them. Pass labelIds on POST /rooms to tag a room as you create it, or on PATCH /rooms/{roomId} to replace the whole set; an empty array clears every label, and omitting the field leaves them alone. A room carries at most five, which is structural rather than a setting. Reading a room returns its labels.
{
"labelIds": [
"{labelId}"
]
}Learn what happened
Poll /room-views for entries across the workspace, then read one room's analytics, activity, and captured addresses. Pass a page's nextCursor as cursor to continue; a cursor this API did not issue returns 400 rather than restarting from the top, so a poller never repeats work. Use since to narrow the activity window and cursor to page through it. /room-views is a window rather than an archive: without since you get the last 30 days, and more than 90 days back is refused. The window used comes back as since; send it alongside cursor to keep paging one set.
Companies & Contacts
Find existing account records or create them with deterministic matching. Company names and contact emails are matched case-insensitively.
/companies?name={name}&domain={domain}Find up to 10 companies by exact name, domain, or both.
crm:read/companiesFind a company by case-insensitive name, or create it. An explicit domain only enriches the record. Returns created to identify the outcome.
crm:write/contacts?email={query}Search contacts by email address. Returns matching contacts with their associated company.
crm:read/contactsFind a contact by email or create it, with an optional existing or new company.
crm:writePOST /companies request
| Field | Type | Description | |
|---|---|---|---|
| name | string | required | Company name |
| domain | string | optional | Company domain used for enrichment. Never used to match an existing company |
POST /contacts request
| Field | Type | Description | |
|---|---|---|---|
| name | string | conditional | Full name. Use this or firstName and lastName |
| firstName | string | conditional | First name when name is not supplied |
| lastName | string | optional | Last name when using firstName |
| string | required | Email used for case-insensitive matching | |
| title | string | optional | Job title |
| companyId | UUID | optional | Existing company in the authenticated workspace |
| companyName | string | optional | Company to find or create when companyId is not supplied |
| companyDomain | string | optional | Optional enrichment domain used with companyName. Not a company match key |
Company response
| Field | Type | Description |
|---|---|---|
| company.id | UUID | Company ID |
| company.name | string | Company name |
| company.domain | string | null | Normalized company domain |
| created | boolean | Whether this request created the company |
Contact response
| Field | Type | Description |
|---|---|---|
| contact.id | UUID | Contact ID |
| contact.firstName | string | First name |
| contact.lastName | string | Last name |
| contact.email | string | Email address |
| contact.title | string | null | Job title |
| contact.companyId | UUID | null | Associated company ID |
| contact.companyName | string | null | Associated company name |
| created | boolean | Whether this request created the contact |
| company | object | null | Resolved company, when available |
| companyCreated | boolean | Whether this request created the company |
Webhooks
Subscribe to real-time events via REST Hooks. When an event occurs, HummingDeck sends a POST request to your registered HTTPS URL with the event payload. Failed deliveries are retried up to 3 times (at 1s, 5s, and 30s intervals). Webhook subscriptions are managed by the Zapier integration and are not available to workspace API tokens.
/hooksSubscribe to an event. Requires a target HTTPS URL and an event type. Returns a subscription ID.
Zapier OAuth only
/hooks/{id}Unsubscribe from an event by subscription ID.
Zapier OAuth only
Event types
| Event | Description |
|---|---|
| view.created | A real person viewed a shared document. Bot traffic (email security scanners, crawlers) is filtered automatically. |
| decision.made | A prospect responded to a proposal: accepted, declined, or requested changes. |
| email_captured | A viewer entered their email address to access gated content. |
Example payloads
view.created
{
"event": "view.created",
"data": {
"id": "view_abc123",
"deck_id": "deck_xyz789",
"deck_title": "Q4 Enterprise Proposal",
"viewer_email": "sarah@acme.com",
"viewer_name": "Sarah Wood",
"viewer_company": "Acme Corp",
"location": "San Francisco, CA",
"device": "Desktop",
"browser": "Chrome",
"pages_viewed": 8,
"total_pages": 12,
"duration_seconds": 272,
"completion_percent": 67,
"created_at": "2026-03-29T14:32:00Z"
}
}decision.made
{
"event": "decision.made",
"data": {
"share_slug": "proposal-2024",
"decision": "accepted",
"deck_title": "Q4 Enterprise Proposal",
"viewer_email": "sarah@acme.com",
"viewer_name": "Sarah Wood",
"decision_note": "Approved pending final review",
"decided_at": "2026-03-29T15:30:00Z"
}
}email_captured
{
"event": "email_captured",
"data": {
"email": "prospect@company.com",
"share_slug": "proposal-2024",
"deck_title": "Q4 Enterprise Proposal",
"view_id": "view_xyz789",
"captured_at": "2026-03-29T14:35:00Z"
}
}Views & Events
Polling endpoints for retrieving recent engagement data. These return the same data that webhooks deliver in real time. Use them for backfilling, testing, or as a fallback.
/viewsList the most recent 100 document views. Bot sessions are excluded.
analytics:read/decisionsList recent proposal decisions (accepted, declined, changes requested).
analytics:read/emailsList recent email captures from gated content.
analytics:readError handling
Every error returns a JSON object with an error field describing what went wrong. Most responses also include a code field for programmatic handling, such as PLAN_LIMIT_REACHED, FEATURE_NOT_AVAILABLE, ROOM_NOT_ACTIVE, TAB_NOT_EMPTY, INVALID_FORMAT, or FILE_TOO_LARGE. HTTP status codes follow standard conventions.
| Status | Meaning |
|---|---|
| 400 | Bad request: missing or invalid parameters |
| 401 | Unauthorized: invalid or expired Bearer token |
| 403 | Forbidden: the credential lacks a required scope, a plan limit is reached, the plan does not include a required option, or this credential type is not allowed on this endpoint |
| 404 | Not found: resource does not exist or is not owned by your team |
| 409 | Conflict: the supplied identifiers do not agree, the room is archived, or the room's tabs do not allow the change |
| 413 | Payload too large: the request body or uploaded file exceeds this endpoint’s limit |
| 429 | Too many requests: the key or client IP exceeded its current rate limit; retry after the response’s Retry-After delay |
| 500 | Server error: retry the request |
Rate limits
Manual workspace keys and Zapier OAuth connections are limited per credential: 600 reads per 5 minutes, 120 writes per minute, 60 /room-views requests per minute, and 20 uploads per hour. Across all credentials, each workspace is limited to 1,200 reads per 5 minutes, 240 writes per minute, 120 /room-views requests per minute, and 40 uploads per hour. Failed bearer authentication and invalid OAuth client authentication are each limited per client IP to 60 attempts per 5 minutes. Maximum 50 active webhook subscriptions per team.
This API is currently used by our Zapier integration. Additional integration platforms may be supported in the future.