---
title: "Track signature status on the activity feed"
url: "https://docs.signnow.com/docs/hubspot-activity-feed-sync"
type: "page"
section: "Integrations"
slug: "hubspot-activity-feed-sync"
---

# Track signature status on the activity feed

# Track signature status on the HubSpot activity feed

Log every SignNow signing event as a note on a Deal, Contact or Company timeline using webhooks.

## Use case overview

The SignNow integration for HubSpot writes to a record's activity feed when a document is fully signed and attached back to the record. This use case adds the steps in between — sent, signed, declined, signer complete — so a sales team can follow signing progress without leaving the record.

It works by subscribing to SignNow document group events and routing them into a HubSpot workflow, which writes a note to the record. No middleware is involved: SignNow posts directly to HubSpot.

Every document sent from a HubSpot record passes through the integration's `/prepare` endpoint, which writes the originating record's type and ID onto the SignNow document group as `context_table` and `context_id`. The webhook payload carries those values back, and the workflow uses `context_id` to find the record. Nothing needs tagging by hand.

```mermaid
flowchart TD
    A[HubSpot user sends a document for signature] --> B[SignNow document group created<br/>context_id = HubSpot Record ID]
    B --> C[SignNow event fires<br/>sent / signed / declined / signer complete / complete]
    C --> D[HubSpot workflow<br/>Received a webhook from an external app]
    D --> E[Custom Code action<br/>calls the HubSpot Notes API]
    E --> F[Note appears on the record's activity feed]
```

## Prerequisites

- HubSpot Data Hub Professional or Enterprise, for the webhook trigger and the Custom Code action.
- Permission to create private apps and workflows in the HubSpot account.
- A SignNow account with API access and permission to create event subscriptions.
- The SignNow account connected inside HubSpot must be the same account you create the event subscription under. The subscription is scoped to a SignNow user ID, so events fire for that user's document groups. Check the connected account in the SignNow card on a record before you begin.

## Configuration guide

### Step 1. Get a SignNow API access token

Retrieve the Basic authorization token from the SignNow API dashboard: **Apps and Keys** → select your application → **OAuth 2.0** tab.

![b1-01-basic-authorization-token.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/b1-01-basic-authorization-token.png)

Request an access token with [`POST /oauth2/token`](/docs/oauth2/operations/post-oauth2-token):

```bash
curl --request POST \
  --url https://api.signnow.com/oauth2/token \
  --header 'Authorization: Basic {{basic_authorization_token}}' \
  --header 'Content-Type: application/x-www-form-urlencoded' \
  --data-urlencode 'username={{account_email}}' \
  --data-urlencode 'password={{account_password}}' \
  --data-urlencode 'grant_type=password' \
  --data-urlencode 'scope=*'
```

The response contains `access_token`, `refresh_token` and `expires_in` (30 days by default). Use the access token as a Bearer token in the following steps, and the refresh token to renew it without re-sending credentials.

### Step 2. Find your SignNow user ID

The event subscription is created at user level, so it needs the SignNow user's ID. Call [`GET /user`](/docs/user/operations/get-user-info):

```bash
curl --request GET \
  --url https://api.signnow.com/user \
  --header 'Authorization: Bearer {{access_token}}'
```

Take `id` from the response — a 40-character hex string.

### Step 3. Create a HubSpot private app

The HubSpot Notes API needs an authenticated token. A private app token does not expire.

1. In HubSpot, go to **Development → Legacy Apps** and click **Create legacy app**.

![a1-01-legacy-apps-create-app.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/a1-01-legacy-apps-create-app.png)

2. Choose **Private** ("For one account").
3. Tick the acknowledgement checkbox and click **Continue with legacy private app**.
4. Name the app something identifiable, for example `SignNow Activity Sync`. This name appears on every note the integration creates.
5. On the **Scopes** tab, add the read and write scopes for the object this workflow will track. For a Deal-based workflow, add `crm.objects.deals.read` and `crm.objects.deals.write`. For Contacts use `crm.objects.contacts.read` and `crm.objects.contacts.write`; for Companies, `crm.objects.companies.read` and `crm.objects.companies.write`.

![a1-02-scopes-deals-read-write.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/a1-02-scopes-deals-read-write.png)

6. Click **Create**, confirm, then copy the access token. Store it in a password manager — it goes into a workflow secret in Step 7, never into the code.

### Step 4. Create the webhook event and copy its URL

1. Go to **Automation → Workflows**, click **Create workflow** and choose **From scratch**.

![a2-01-create-workflow-from-scratch.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/a2-01-create-workflow-from-scratch.png)

2. Search the trigger list for `webhook` and select **Received a webhook from an external app**.

![a2-02-trigger-received-webhook.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/a2-02-trigger-received-webhook.png)

3. Click **Add a webhook**.

![a2-03-add-a-webhook.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/a2-03-add-a-webhook.png)

4. Name the webhook event, for example `signnow-document-group-events`.
5. On the **Connect** step, HubSpot generates an inbound webhook URL. Click **Copy** and keep the wizard open — the next step needs this URL.

![a2-04-connect-copy-webhook-url.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/a2-04-connect-copy-webhook-url.png)

### Step 5. Subscribe to SignNow events

Create a subscription with [`POST /v2/event-subscriptions`](/docs/manage-event-subscriptions/operations/create-event-subscription-2), using the webhook URL from Step 4 as the callback:

```bash
curl --request POST \
  --url https://api.signnow.com/v2/event-subscriptions \
  --header 'Authorization: Bearer {{access_token}}' \
  --header 'Content-Type: application/json' \
  --data '{
    "event": "user.document_group.invite.sent",
    "entity_id": "{{signnow_user_id}}",
    "attributes": {
      "callback": "{{hubspot_webhook_url}}",
      "include_metadata": true,
      "use_tls_12": true,
      "retry_count": 3,
      "delay": 0
    }
  }'
```

`include_metadata: true` is what carries `context_id` in the payload. Without it the workflow has no value to match on.

Repeat the request for each event you want on the activity feed. These five make up a signature progress trail:

- `user.document_group.invite.sent` — envelope sent
- `user.document_group.invite.signed` — one document in the group was signed
- `user.document_group.invite.signer.complete` — a signer finished everything asked of them
- `user.document_group.invite.declined` — a signer declined
- `user.document_group.complete` — the whole group is finished

Each request returns its own subscription `id`. The [Webhooks guide](/docs/guides-webhooks#entity-events) lists every available event.

### Step 6. Send a test envelope and map the payload

Send a document for signature from a HubSpot record, the normal way. SignNow delivers the event to the webhook URL and HubSpot captures it, advancing the wizard to **Map**.

On the **Map** step, every field from the payload is pre-populated. Give each a readable label and a data type. Keep at minimum:

| Payload field | Suggested label | Data type |
| --- | --- | --- |
| `meta.event` | SignNow event | Text |
| `meta.timestamp` | SignNow timestamp | Number |
| `meta.metadata.context_id` | SignNow context ID | Number |
| `content.group_id` | SignNow group ID | Text |

![a2-05-map-context-id-number.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/a2-05-map-context-id-number.png)

> [!WARNING]
> Delete the `access_token` field if it appears in the mapped list. It is a live SignNow credential and should not be stored as a HubSpot property.

On the **Match** step, set **Associated object** to the object type this workflow tracks, then match the third-party property `meta.metadata.context_id` against HubSpot's **Record ID**.

Click **Continue to trigger setup**, select your webhook event as the enrollment trigger, then step through to **Settings** and turn **Re-enroll** on. Re-enrollment is what lets a record receive a note for every event rather than only the first.

### Step 7. Add the Custom Code action

1. On the workflow canvas, add an action → **Data ops** → **Custom code**, and choose **Node.js**.
2. Under **Secrets**, add a secret named `HUBSPOT_PRIVATE_APP_TOKEN` with the token from Step 3.
3. Under **Property to include in code**, define these input fields:

- `recordId` → Record ID
- `signnowEvent` → `meta.event`
- `groupId` → `content.group_id`
- `signnowTimestamp` → `meta.timestamp`
- `signerEmail` → the signer field carried by the events you subscribed to, if any

The names must match the code exactly; a mismatch produces an undefined value rather than an error.

![a3-01-custom-code-secret-and-inputs.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/a3-01-custom-code-secret-and-inputs.png)

4. Replace the sample code in the editor with the snippet below. Set `ASSOCIATION_TYPE_ID` to match the object type this workflow runs on: `214` for Deals, `202` for Contacts, `190` for Companies.

```javascript
const hubspot = require('@hubspot/api-client');

const ASSOCIATION_TYPE_ID = 214;

const EVENT_LABELS = {
  'user.document_group.invite.sent':             'Envelope sent to %SIGNER%',
  'user.document_group.invite.signed':           'Signed by %SIGNER%',
  'user.document_group.invite.declined':         'Declined by %SIGNER%',
  'user.document_group.invite.signer.complete':  'Signer %SIGNER% completed their part',
  'user.document_group.complete':                'Envelope completed - all parties signed',
};

exports.main = async (event, callback) => {
  const {
    recordId,
    signnowEvent,
    groupId,
    signerEmail,
    signnowTimestamp,
  } = event.inputFields;

  const template = EVENT_LABELS[signnowEvent] || ('SignNow status update: ' + signnowEvent);
  const line = template.replace('%SIGNER%', signerEmail || 'the signer');
  const noteBody = 'SignNow: ' + line + (groupId ? ' (group ' + groupId + ')' : '');

  const hsTimestamp = signnowTimestamp
    ? new Date(Number(signnowTimestamp) * 1000).toISOString()
    : new Date().toISOString();

  const hubspotClient = new hubspot.Client({ accessToken: process.env.HUBSPOT_PRIVATE_APP_TOKEN });

  try {
    await hubspotClient.crm.objects.notes.basicApi.create({
      properties: {
        hs_timestamp: hsTimestamp,
        hs_note_body: noteBody,
      },
      associations: [
        {
          to: { id: recordId },
          types: [{ associationCategory: 'HUBSPOT_DEFINED', associationTypeId: ASSOCIATION_TYPE_ID }],
        },
      ],
    });
    callback({ outputFields: { noteCreated: true } });
  } catch (err) {
    throw err;
  }
};
```

![a3-02-code-editor-client-v11.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/a3-02-code-editor-client-v11.png)

5. Click **Save**.

To run the code against a real record, open the Custom code action and scroll to its own **Test action** section, choose a record, supply test values, and click **Test**. The **Test** button at the top of the workflow builder previews the path a record would take without executing the code.

![a3-03-test-action-panel.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/a3-03-test-action-panel.png)

The snippet re-throws on failure. HubSpot retries a Custom Code action that throws, with backoff, for up to a few days — which covers transient issues such as a momentary rate limit.

### Step 8. Turn on the workflow and verify

1. Name the workflow and click **Review and turn on**. A workflow that is off enrols nothing.
2. Send a test envelope from a record of the object type your workflow tracks.
3. Refresh the record after a few seconds. A note appears reading "SignNow: Envelope sent to the signer (group …)".
4. Sign the document. Three further events fire in sequence — `invite.signed`, `invite.signer.complete` and `document_group.complete` — producing three more notes.

> [!NOTE]
> Test with a real send rather than the **Enroll** button. Enrolling a record manually runs the action without a webhook payload behind it, so the inputs arrive empty.

## Result

The record's activity feed carries a note for each stage of the signing process, timestamped and attributed to the private app you created.

![c-01-record-timeline-notes.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/c-01-record-timeline-notes.png)

Notes written by this workflow are attributed to the private app's name, so they are distinguishable from the ones the SignNow integration writes itself.

## Troubleshooting

**No notes appear at all.** Check, in order: the workflow is switched on; the SignNow account connected in HubSpot is the same user the subscription was created under; `include_metadata: true` was set on the subscription; and the callback URL is complete — copy it with the **Copy** button rather than reading it from the truncated input.

**The workflow never enrols the record.** Inspect a delivered payload and confirm `meta.metadata.context_id` holds the record's Record ID. Then confirm the mapped property's data type is Number — Record ID is numeric, and a Text mapping cannot be matched against it.

**The workflow runs but no note appears.** Check the Custom Code action's execution log. The usual causes are a missing object scope on the private app token, or an `ASSOCIATION_TYPE_ID` that does not match the object type.

**Notes say "the signer" instead of a name.** The signer field is named differently across events, and some events carry none. Map the field that the events you subscribed to actually carry — see [Get callback info by ID](/docs/callbacks-info/operations/get-v2-dashboard-event-subscriptions-subscription-id-callbacks) for per-event payloads.

To inspect what a subscription has been sending, call [`GET /v2/event-subscriptions/{subscription_id}/callbacks`](/docs/callbacks-info/operations/get-v2-dashboard-event-history) for its delivery history, including the response each callback received. A failed delivery can be replayed with [`POST /v2/event-subscriptions/{subscription_id}/callbacks/{callback_id}/resend`](/docs/callbacks-info/operations/post-v2-event-subscriptions-subscription-id-callbacks-callback-id-resend).

In HubSpot, the workflow's **Performance history** menu splits the diagnosis: **Enrollment history** shows whether the record enrolled, and **Action logs** show whether the action ran and what it returned.

![c-02-performance-history-menu.png](/reference-assets/images/SignNow_Hubspot/activity_feed_sync/c-02-performance-history-menu.png)

## Reference

### Example webhook payload

A document group event delivered with `include_metadata: true`:

```json
{
  "meta": {
    "event": "user.document_group.invite.sent",
    "timestamp": 1789998905,
    "environment": "https://api.signnow.com",
    "callback_url": "https://api.hubapi.com/automation/v4/webhook-triggers/XXXXXXXX/XXXXXXXX",
    "initiator_id": "5dXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "metadata": {
      "context_id": "XXXXXXXXXXXX",
      "context_table": "XXXXXXXXXX",
      "dtg_origin_id": "57XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
      "origin_entity_type": "dgt"
    }
  },
  "content": {
    "status": "pending",
    "group_id": "47XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX",
    "step_order": 1,
    "group_invite_id": "03XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX"
  }
}
```

The `meta` object is consistent across events. The `content` object varies: different events carry different fields, and the signer is named differently depending on the event. HubSpot builds its webhook event definition from a single captured payload, so the event you capture in Step 6 determines which fields are available to map for all of them.

`context_table` holds an opaque identifier rather than a readable object type, so match on `context_id` alone.

### Association type IDs

| Note association | `associationTypeId` | Category |
| --- | --- | --- |
| Note → Deal | `214` | `HUBSPOT_DEFINED` |
| Note → Contact | `202` | `HUBSPOT_DEFINED` |
| Note → Company | `190` | `HUBSPOT_DEFINED` |

### HubSpot Custom Code environment

| Item | Value |
| --- | --- |
| Runtime | Node.js 20.x |
| Preloaded client | `@hubspot/api-client` v11 |
| Limits | 20s timeout, 128 MB memory |
| Required tier | Data Hub Professional or Enterprise |

### Tracking more than one object type

Duplicate the workflow once per object type. Each copy matches `context_id` against the Record ID of its own object, uses that object's read and write scopes on the private app, and sets `ASSOCIATION_TYPE_ID` accordingly.


---
*Full page: https://docs.signnow.com/docs/hubspot-activity-feed-sync*
