Built for trustTLS encryptionGDPR-readyGoogle CloudSecure paymentsSecurity overview
G2

Rated 5.0 out of 5 on G2

Read the reviews on G2
The page-level analytics are the best part because they show real engagement instead of just basic opens.
Verified User in Computer Software
What I like most about the product is how easy it is to use, especially when it comes to listing all my links and embedding demos in one place for leads and prospects.
Jerome K.Founder
Responsiveness, configurability and development velocity.
Suman K.Co-Founder & CEO

API Reference

HummingDeck exposes a REST API for integration partners and automation platforms. Endpoints authenticate with a Bearer token and return JSON responses.

Base URLhttps://app.hummingdeck.com/api/v1
OpenAPI Spec

Authentication

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:read

View rooms, tabs, items, links, and labels.

rooms:write

Create and manage rooms, tabs, items, links, and labels.

plan:read

View Mutual Action Plan phases and tasks.

plan:write

Create and manage Mutual Action Plan phases and tasks.

analytics:read

View engagement analytics, activity, and captured emails.

crm:read

Find workspace companies and contacts.

crm:write

Create or update companies, contacts, and link audiences.

documents:read

Find documents and read their metadata.

documents:write

Upload 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.

GET/me

Returns 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).

POST/decks

Upload 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.

Required:documents:write
GET/decks

List up to 20 documents, newest first. Use the optional title query parameter for a case-insensitive partial-title filter.

Required:documents:read

GET /decks Response fields

FieldTypeDescription
idstringDocument ID
titlestringDocument title
fileTypestringDocument MIME type
pageCountinteger | nullNumber of pages
thumbnailUrlstring | nullThumbnail image URL
processingStatusstringpending, processing, completed, or failed. A document can go into a room while it is processing; send a link to it once it reads completed.
processingErrorCodestring | nullWhy processing failed, when it did
createdAtstringISO 8601 timestamp

POST /decks Response fields

FieldTypeDescription
idstringDocument ID
titlestringDocument title
fileTypestringDocument MIME type
processingStatusstringpending, processing, completed, or failed. A document can go into a room while it is processing; send a link to it once it reads completed.
processingErrorCodestring | nullWhy processing failed, when it did

Share Links

Create trackable document links. A personal link can resolve or create its contact and company in the same request.

POST/shares

Create a personal or anonymous link. Personal links can find or create account records automatically.

Required:documents:write
Conditional:crm:write(Required for a non-anonymous share when the request supplies contactId, companyId, companyName, recipientEmail, or recipientName.)

Request fields

FieldTypeDescription
deckIdstringrequiredID of the document to share
recipientNamestringoptionalRecipient's name (for personal links)
recipientEmailstringoptionalRecipient's email (for personal links)
contactIdUUIDoptionalExisting contact in the authenticated workspace
companyIdUUIDoptionalExisting company. Cannot be combined with companyName
companyNamestringoptionalCompany to find by name or create
companyDomainstringoptionalDomain stored for enrichment when companyName is supplied. Never used to select a company
typestringoptionalDefaults to personal when recipient or account fields are present, otherwise anonymous

Create the link and account records together

Send recipient and company details directly to /shares. HummingDeck finds matching records, creates any that are missing, attaches them to the link, and reports what was created. Set type to anonymous explicitly to skip account creation.

{
  "deckId": "8f3d41de-2bb8-4d8e-80de-6cd2072ffab1",
  "recipientName": "Ada Lovelace",
  "recipientEmail": "ada@analytical.example",
  "companyName": "Analytical Engines",
  "companyDomain": "analytical.example"
}

Response fields

FieldTypeDescription
idstringShare ID
slugstringShare slug (used in the URL)
shareUrlstringFull trackable URL
typestring"personal" or "anonymous"
recipientNamestringRecipient name (if personal)
recipientEmailstringRecipient email (if personal)
contactobject | nullResolved contact attached to the link
contactCreatedbooleanWhether this request created the contact
companyobject | nullResolved company attached to the link
companyCreatedbooleanWhether this request created the company
createdAtstringISO 8601 timestamp

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.

GET/rooms

List 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.

Required:rooms:read
Conditional:crm:read(Required when the companyId filter is present.)
POST/rooms

Create a room with its documents and first audience link in one call.

Required:rooms:write
Conditional:documents: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.)
GET/rooms/{roomId}

Return the room's settings, its tabs and items in display order, and its link counts.

Required:rooms:read
PATCH/rooms/{roomId}

Change the name, welcome message, point of contact, company, or contact.

Required:rooms:write
Conditional:crm:write(Required when companyId or contactId is present, including null to detach the association.)
POST/rooms/{roomId}/archive

Archive the room. Its links stop working.

Required:rooms:write
POST/rooms/{roomId}/restore

Restore an archived room. Its links work again.

Required:rooms:write
POST/rooms/{roomId}/tabs

Add a tab, at a position or last.

Required:rooms:write
PATCH/rooms/{roomId}/tabs/{tabId}

Rename a tab.

Required:rooms:write
PUT/rooms/{roomId}/tabs/order

Put every tab in a new order.

Required:rooms:write
DELETE/rooms/{roomId}/tabs/{tabId}

Remove a tab that shows no items.

Required:rooms:write
POST/rooms/{roomId}/items

Add a document, URL, embed, or section divider to a tab.

Required:rooms:write
Conditional:documents:write(Required when type is document or url.)
POST/rooms/{roomId}/items/{itemId}/move

Move an item to the end of another tab.

Required:rooms:write
PUT/rooms/{roomId}/items/order

Put one tab's items in a new order.

Required:rooms:write
DELETE/rooms/{roomId}/items/{itemId}

Take an item out of the room. It stays in your library.

Required:rooms:write
GET/rooms/{roomId}/links

List the room's audience links, newest first, with each Restricted link's active invitees.

Required:rooms:read
POST/rooms/{roomId}/links

Create another attributed audience link, in any access mode, for an active room.

Required:rooms:write
Also required:crm:write
PATCH/rooms/{roomId}/links/{linkId}

Turn a link on or off, set or clear its expiry, or replace its allowlist.

Required:rooms:write
Conditional:crm:write(Required when allowedEmails or allowedDomains is present, including an empty array that clears the audience.)
GET/rooms/{roomId}/action-plan

Return the room's action plan: its settings, phases, tasks (internal ones included), dependencies, and progress.

Required:plan:read
PATCH/rooms/{roomId}/action-plan

Change the plan's settings, including whether the people who open the room may tick their own tasks off.

Required:plan:write
POST/rooms/{roomId}/action-plan/phases

Add a milestone. Leave color out and phases rotate teal, peach, blue by order.

Required:plan:write
PATCH/rooms/{roomId}/action-plan/phases/{phaseId}

Rename a phase, move it, retarget its date, or set its colour. Sending color null restores the rotation.

Required:plan:write
DELETE/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.

Required:plan:write
POST/rooms/{roomId}/action-plan/tasks

Add 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.

Required:plan:write
PATCH/rooms/{roomId}/action-plan/tasks/{taskId}

Update a task. Omitting assignee leaves ownership alone; sending null clears it.

Required:plan:write
DELETE/rooms/{roomId}/action-plan/tasks/{taskId}

Remove a task. Its subtasks go with it.

Required:plan:write
POST/rooms/{roomId}/action-plan/tasks/{taskId}/status

Complete or reopen a task on the workspace's behalf. A task behind an unfinished dependency returns 409 TASK_BLOCKED.

Required:plan:write
GET/rooms/{roomId}/analytics

Room visits, unique viewers, average time, documents opened of total, and average completion. Bots excluded.

Required:analytics:read
GET/rooms/{roomId}/activity

What happened in the room, newest first. Discussion entries name the sender and never carry the message. Narrow with since.

Required:analytics:read
GET/rooms/{roomId}/captured-emails

Addresses the room collected. source is verify when the visitor confirmed it with a one-time link, ask when they only typed it.

Required:analytics:read
GET/room-views

Room entries across the workspace, newest first. Someone entering a room has no other home; /views covers document views only.

Required:analytics:read
GET/room-labels

List the workspace's room labels with how many rooms use each. Find label IDs here before tagging a room.

Required:rooms:read
POST/room-labels

Create a label. Names are unique per workspace, ignoring case; color is a #RRGGBB hex value.

Required:rooms:write
PATCH/room-labels/{labelId}

Rename a label, change its colour, or edit its description.

Required:rooms:write
DELETE/room-labels/{labelId}

Delete a label and its assignments. The rooms that carried it are untouched; the response says how many lost it.

Required:rooms:write

Create 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.

FieldTypeDescription
VideoLoom, YouTube, Vimeo, Wistia, Vidyard
SchedulingCalendly, Cal.com, SavvyCal, Google Calendar
FormsTypeform, Tally, Google Forms, Jotform, Fillout
DesignFigma, Miro, Canva, Whimsical
Docs and tablesGoogle Docs, Google Sheets, Notion, Coda, Airtable
PresentationsGoogle Slides, Pitch, Gamma, Guideflow, Flipsnack, Prezi
AudioSpotify, 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.

GET/companies?name={name}&domain={domain}

Find up to 10 companies by exact name, domain, or both.

Required:crm:read
POST/companies

Find a company by case-insensitive name, or create it. An explicit domain only enriches the record. Returns created to identify the outcome.

Required:crm:write
GET/contacts?email={query}

Search contacts by email address. Returns matching contacts with their associated company.

Required:crm:read
POST/contacts

Find a contact by email or create it, with an optional existing or new company.

Required:crm:write

POST /companies request

FieldTypeDescription
namestringrequiredCompany name
domainstringoptionalCompany domain used for enrichment. Never used to match an existing company

POST /contacts request

FieldTypeDescription
namestringconditionalFull name. Use this or firstName and lastName
firstNamestringconditionalFirst name when name is not supplied
lastNamestringoptionalLast name when using firstName
emailstringrequiredEmail used for case-insensitive matching
titlestringoptionalJob title
companyIdUUIDoptionalExisting company in the authenticated workspace
companyNamestringoptionalCompany to find or create when companyId is not supplied
companyDomainstringoptionalOptional enrichment domain used with companyName. Not a company match key

Company response

FieldTypeDescription
company.idUUIDCompany ID
company.namestringCompany name
company.domainstring | nullNormalized company domain
createdbooleanWhether this request created the company

Contact response

FieldTypeDescription
contact.idUUIDContact ID
contact.firstNamestringFirst name
contact.lastNamestringLast name
contact.emailstringEmail address
contact.titlestring | nullJob title
contact.companyIdUUID | nullAssociated company ID
contact.companyNamestring | nullAssociated company name
createdbooleanWhether this request created the contact
companyobject | nullResolved company, when available
companyCreatedbooleanWhether 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.

POST/hooks

Subscribe to an event. Requires a target HTTPS URL and an event type. Returns a subscription ID.

Zapier OAuth only

DELETE/hooks/{id}

Unsubscribe from an event by subscription ID.

Zapier OAuth only

Event types

EventDescription
view.createdA real person viewed a shared document. Bot traffic (email security scanners, crawlers) is filtered automatically.
decision.madeA prospect responded to a proposal: accepted, declined, or requested changes.
email_capturedA 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.

GET/views

List the most recent 100 document views. Bot sessions are excluded.

Required:analytics:read
GET/decisions

List recent proposal decisions (accepted, declined, changes requested).

Required:analytics:read
GET/emails

List recent email captures from gated content.

Required:analytics:read

Error 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.

StatusMeaning
400Bad request: missing or invalid parameters
401Unauthorized: invalid or expired Bearer token
403Forbidden: 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
404Not found: resource does not exist or is not owned by your team
409Conflict: the supplied identifiers do not agree, the room is archived, or the room's tabs do not allow the change
413Payload too large: the request body or uploaded file exceeds this endpoint’s limit
429Too many requests: the key or client IP exceeded its current rate limit; retry after the response’s Retry-After delay
500Server 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.