> ## Documentation Index
> Fetch the complete documentation index at: https://docs.riseworks.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Document Events

> Webhook event sent when a PSA is fully signed by every required party

Document events tell you when an agreement reaches a final state, so you no longer need to poll for it. Today this covers one event: `document.signed`, sent when a Professional Services Agreement (PSA) has **all** of its required signatures.

> **[document.signed](#document-signed)**
>
> A PSA, PSA Lite, or AOR PSA version is fully signed by every required party

***

## document.signed

Rise sends `document.signed` once, after the **last required signature** on a PSA version is completed. It is not sent for each individual signature, and it never announces a partially signed agreement.

> **Payload version**
>
> `2.0`, the first version of this event on v2 webhooks. See [Event versions](https://docs.riseworks.io/webhooks/implementation/event-types.md#event-versions).

> **Scope**
>
> Organization (company) endpoints only. Team-scoped endpoints and legacy v1 endpoints do not receive it.

### Supported agreement types

| `document.type` | Agreement |
| - | - |
| `psa` | Professional Services Agreement |
| `psa_lite` | PSA Lite |
| `aor_psa` | Agent of Record (AOR) PSA |

Other document types do not send `document.signed`.

### When it fires

* **All required signatures.** The event fires only when every required signature on the version is complete. That holds whether the last signature was manual or automatic, or came from Rise finalizing the version.
* **One event per version.** Rise creates one `document.signed` event per document version. Re-finalizing a version that is already complete does not create another one. The same event can still be delivered more than once (see [Delivery guarantees](#delivery-guarantees)).
* **Renewals are new versions.** When a renewed or regenerated agreement is fully signed, you get a new event with a new `version_nanoid` and the same document `nanoid`.
* **No backfill.** Agreements completed before you subscribed, or before this event was available, are not sent retroactively. To handle them, reconcile once from your own records.

### Field reference

**object** `string` *required*

The type of object this webhook represents (always `"event"`)

**created** `number` *required*

The Unix timestamp when the event was created

**event\_type** `string` *required*

Always `document.signed`

**event\_version** `string` *required*

The version of the event schema (`2.0`)

**request\_id** `string`

A request tracking identifier. Real completions usually have none (`null`). Test deliveries always set it to a value starting with `[TEST]`.

**idempotency\_key** `string` *required*

A unique key for this delivered event, also sent in the `X-Rise-Event-Key` header. Treat it as an opaque string. A redelivered completion can have a different key, so deduplicate on `document.version_nanoid` instead.

**document** `object` *required*

The fully signed agreement version

**document**

**nanoid** `string` *required*

Identifier of the document (`do-` prefix). It stays the same across versions and renewals.

**version\_nanoid** `string` *required*

Identifier of the fully signed document version (`dv-` prefix). Use it as your deduplication key.

**company\_nanoid** `string` *required*

Identifier of the company the agreement belongs to (`co-` prefix)

**service\_provider\_nanoid** `string` *required*

Identifier of the contractor on the agreement (`us-` prefix), including when a contractor business representative signed. This is **not necessarily the last signer**.

**type** `string` *required*

Agreement type: `psa`, `psa_lite`, or `aor_psa`

**status** `string` *required*

Always `complete` for this event

**signed\_at** `string` *required*

ISO-8601 timestamp of when this version was finalized with all required signatures

> [!NOTE]
>
> The payload identifies the agreement but does not contain it: no document contents, signature hashes, or download URLs. Use the identifiers to look up the agreement in your own records.

```json document.signed (v2) theme={null}
{
  "object": "event",
  "created": 1790668706,
  "request_id": null,
  "event_type": "document.signed",
  "event_version": "2.0",
  "idempotency_key": "3f6c2a9e-8b41-4d7a-9c15-6e2b8f0d4a73_v2",
  "document": {
    "nanoid": "do-7Hq2mX9pLk4R",
    "version_nanoid": "dv-Qm8Zt3Vw5Nc1",
    "company_nanoid": "co-Ab12Cd34Ef56",
    "service_provider_nanoid": "us-Rt5Yu7Io9Pa2",
    "type": "psa",
    "status": "complete",
    "signed_at": "2026-09-29T08:58:26.000Z"
  }
}
```

All identifiers above are synthetic examples.

***

## Subscribing

Subscribe an **organization endpoint** (a v2 webhook registered for your company with no `team_nanoid`) to `document.signed`. You can add other organization-wide events to the same endpoint.

> [!WARNING]
>
> Do not pass `team_nanoid`. Rise sends `document.signed` only to company-level endpoints, so a team-scoped endpoint that lists this event receives nothing.

### In the Rise app

**1. Open organization webhooks**

Go to **B2B API → Webhooks** and select **Organization webhooks**.

**2. Add an endpoint**

Click **Add endpoint**, then enter the endpoint URL and a secret of at least 16 characters.

**3. Select the event**

Under **PSA completion**, select **PSA fully signed (document.signed)**. Add any other organization-wide events you need.

**4. Save**

Keep **Active** on and save. The endpoint appears under **Your webhook endpoints** with its subscribed events.

### With the API

Register a new organization endpoint with [`POST /v2/webhooks/register`](https://docs.riseworks.io/api-reference/b2b-webhooks/register-webhook):

```bash Register theme={null}
curl -X POST "https://b2b-api.riseworks.io/v2/webhooks/register" \
  -H "Authorization: Bearer $RISE_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "company_nanoid": "co-Ab12Cd34Ef56",
    "url": "https://example.com/webhooks/rise",
    "webhook_version": "v2",
    "secret": "'"$RISE_WEBHOOK_SECRET"'",
    "events": ["document.signed"],
    "name": "PSA completion"
  }'
```

To add the event to an existing organization endpoint, use [`PUT /v2/webhooks/{webhook_nanoid}`](https://docs.riseworks.io/api-reference/b2b-webhooks/update-webhook-v1v2-cannot-be-changed;-register-a-new-endpoint-to-switch-version). `events` **replaces** the subscribed list, so send the complete list you want, including the events already subscribed:

```bash Update theme={null}
curl -X PUT "https://b2b-api.riseworks.io/v2/webhooks/wh-Kp4Lm6Nb8Vc0" \
  -H "Authorization: Bearer $RISE_JWT" \
  -H "Content-Type: application/json" \
  -d '{
    "events": ["payment.sent", "deposit.received", "document.signed"]
  }'
```

> [!NOTE]
>
> Use your environment's base URL. See [Environments](https://docs.riseworks.io/concepts/environments.md).

***

## Verifying and handling deliveries

`document.signed` is signed exactly like every other v2 event. Each delivery has these headers:

| Header | Value |
| - | - |
| `X-Rise-Signature` | `t=<unix timestamp>,v1=<hex HMAC-SHA256>`: the HMAC of `<timestamp>.<raw body>` using your endpoint secret |
| `X-Rise-Event-Type` | `document.signed` |
| `X-Rise-Event-Key` | The event's `idempotency_key` |

Always verify against the **raw** request body. See [Webhook security](https://docs.riseworks.io/webhooks/getting-started/security.md) for the full procedure.

> [!WARNING]
>
> **SDK support.** The `WebhookValidator` in `@riseworks/sdk` rejects event types it does not know, and returns `UNKNOWN_EVENT_TYPE` even when the signature is valid. Use `WebhookValidator` for `document.signed` only with an SDK release whose changelog lists `DocumentSignedV2`. Until you have one, verify the signature yourself as shown below.

```javascript Node.js (manual verification) theme={null}
import crypto from 'node:crypto';
import express from 'express';

const app = express();
const TOLERANCE_SECONDS = 600;

app.post('/webhooks/rise', express.raw({ type: 'application/json' }), async (req, res) => {
  const raw = req.body.toString('utf8');
  const header = req.header('X-Rise-Signature') ?? '';
  const parts = Object.fromEntries(header.split(',').map((p) => p.split('=')));
  const timestamp = Number(parts.t);

  if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) {
    return res.status(400).send('Stale or missing timestamp');
  }

  const expected = crypto
    .createHmac('sha256', process.env.RISE_WEBHOOK_SECRET)
    .update(`${timestamp}.${raw}`)
    .digest('hex');
  const received = Buffer.from(parts.v1 ?? '', 'hex');
  if (received.length !== 32 || !crypto.timingSafeEqual(received, Buffer.from(expected, 'hex'))) {
    return res.status(401).send('Invalid signature');
  }

  const event = JSON.parse(raw);
  if (event.event_type === 'document.signed' && event.event_version.startsWith('2.')) {
    const { version_nanoid, service_provider_nanoid } = event.document;

    // Test deliveries carry synthetic IDs; acknowledge them without side effects.
    if (event.request_id?.startsWith('[TEST]')) {
      return res.sendStatus(200);
    }

    // Deduplicate on the document version: at-least-once delivery can repeat it.
    const isNew = await markProcessedOnce(`document.signed:${version_nanoid}`);
    if (isNew) {
      await queue.add('psa-signed', { version_nanoid, service_provider_nanoid });
    }
  }

  res.sendStatus(200);
});
```

### Delivery guarantees

* **At-least-once.** The same completion can arrive more than once, for example after a retry or a redelivery. Deduplicate on `document.version_nanoid` and make your side effects idempotent.
* **Retries.** A delivery that fails or gets a non-2xx response is retried automatically. See [Delivery and retries](https://docs.riseworks.io/webhooks/operations/delivery.md). You can also retry a failed delivery from the endpoint's **Delivery history** or with [`POST /v2/webhooks/retry/{delivery_nanoid}`](https://docs.riseworks.io/api-reference/b2b-webhooks/retry-delivery).
* **Respond quickly.** Return a 2xx right away and do slow work (fetching the agreement, updating records) in the background.
* **Inactive endpoints miss events.** Completions that happen while an endpoint is deactivated are not queued, and there is no automatic backfill.

***

## Testing

You can send a test `document.signed` to an active organization endpoint that is subscribed to it:

* **In the Rise app:** on the endpoint card, pick **PSA fully signed (document.signed)** from the dropdown and click **Test**. Sending a test does not change the endpoint's subscribed events. The delivery then shows in **Delivery history**.
* **With the API:** call [`POST /v2/webhooks/test/{webhook_nanoid}`](https://docs.riseworks.io/api-reference/b2b-webhooks/test-webhook) with `{ "event_type": "document.signed" }`. The response includes the generated `test_payload`.

### Telling test events from real completions

A test delivery has a real signature and the real envelope, but its data is synthetic:

| Field | Test event | Real completion |
| - | - | - |
| `request_id` | Starts with `[TEST]` | Usually `null` |
| `document.nanoid` | `do-test12345678` | A real document identifier |
| `document.version_nanoid` | `dv-test12345678` | A real version identifier |
| `document.service_provider_nanoid` | `us-test12345678` | The contractor's user identifier |
| `document.company_nanoid` | Your company | Your company |

> [!WARNING]
>
> Check for these markers before you act on a test delivery, and never activate a contractor or release work because of one. Every test uses the same `version_nanoid`, so your deduplication can drop repeat tests. That is expected.

To test end to end with a real completion, sign a PSA in a non-production environment and confirm the delivery arrives with real identifiers.

***

## Troubleshooting missing deliveries

> **A PSA was signed but no event arrived**
>
> * Make sure **every** required party has signed. A partially signed PSA does not send the event.
> * Check that the agreement is a `psa`, `psa_lite`, or `aor_psa`.
> * Check that the endpoint is an **organization** endpoint (no `team_nanoid`), is a **v2** endpoint, is **active**, and lists `document.signed`.
> * Check that the agreement belongs to the company that registered the endpoint.
> * Agreements completed before the subscription existed are not backfilled.

> **The delivery shows as failed**
>
> Open the endpoint's **Delivery history** to see the response code and the event. Fix the endpoint, then click **Retry** or call the retry endpoint. See [Troubleshooting webhooks](https://docs.riseworks.io/webhooks/operations/troubleshooting.md) for common HTTP and signature errors.

> **The delivery arrived but validation fails**
>
> If signature checks pass but your SDK returns `UNKNOWN_EVENT_TYPE`, your SDK version does not support `document.signed`. Verify the signature manually as shown above, or upgrade to an SDK release that includes `DocumentSignedV2`.

> **I received the same completion twice**
>
> This is expected with at-least-once delivery. Deduplicate on `document.version_nanoid`.

> **Still missing**
>
> Contact Rise support with the endpoint's `webhook_nanoid`, the document `nanoid` (if you have it), and roughly when the last signature happened.


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.