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

<CardGroup cols={1}>
  <Card title="document.signed" icon="file-signature" href="#document-signed">
    A PSA, PSA Lite, or AOR PSA version is fully signed by every required party
  </Card>
</CardGroup>

***

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

<CardGroup cols={2}>
  <Card title="Payload version" icon="code-branch">
    `2.0`, the first version of this event on v2 webhooks. See [Event versions](/webhooks/implementation/event-types#event-versions).
  </Card>

  <Card title="Scope" icon="building">
    Organization (company) endpoints only. Team-scoped endpoints and legacy v1 endpoints do not receive it.
  </Card>
</CardGroup>

### 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

<ParamField path="object" type="string" required>
  The type of object this webhook represents (always `"event"`)
</ParamField>

<ParamField path="created" type="number" required>
  The Unix timestamp when the event was created
</ParamField>

<ParamField path="event_type" type="string" required>
  Always `document.signed`
</ParamField>

<ParamField path="event_version" type="string" required>
  The version of the event schema (`2.0`)
</ParamField>

<ParamField path="request_id" type="string">
  A request tracking identifier. Real completions usually have none (`null`). Test deliveries always set it to a value starting with `[TEST]`.
</ParamField>

<ParamField path="idempotency_key" type="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.
</ParamField>

<ParamField path="document" type="object" required>
  The fully signed agreement version
</ParamField>

<Expandable title="document" defaultOpen>
  <ParamField path="nanoid" type="string" required>
    Identifier of the document (`do-` prefix). It stays the same across versions and renewals.
  </ParamField>

  <ParamField path="version_nanoid" type="string" required>
    Identifier of the fully signed document version (`dv-` prefix). Use it as your deduplication key.
  </ParamField>

  <ParamField path="company_nanoid" type="string" required>
    Identifier of the company the agreement belongs to (`co-` prefix)
  </ParamField>

  <ParamField path="service_provider_nanoid" type="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**.
  </ParamField>

  <ParamField path="type" type="string" required>
    Agreement type: `psa`, `psa_lite`, or `aor_psa`
  </ParamField>

  <ParamField path="status" type="string" required>
    Always `complete` for this event
  </ParamField>

  <ParamField path="signed_at" type="string" required>
    ISO-8601 timestamp of when this version was finalized with all required signatures
  </ParamField>
</Expandable>

<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.
</Note>

<RequestExample>
  ```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"
    }
  }
  ```
</RequestExample>

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.
</Warning>

### In the Rise app

<Steps>
  <Step title="Open organization webhooks">
    Go to **B2B API → Webhooks** and select **Organization webhooks**.
  </Step>

  <Step title="Add an endpoint">
    Click **Add endpoint**, then enter the endpoint URL and a secret of at least 16 characters.
  </Step>

  <Step title="Select the event">
    Under **PSA completion**, select **PSA fully signed (document.signed)**. Add any other organization-wide events you need.
  </Step>

  <Step title="Save">
    Keep **Active** on and save. The endpoint appears under **Your webhook endpoints** with its subscribed events.
  </Step>
</Steps>

### With the API

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

<CodeGroup>
  ```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"
    }'
  ```
</CodeGroup>

To add the event to an existing organization endpoint, use [`PUT /v2/webhooks/{webhook_nanoid}`](/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:

<CodeGroup>
  ```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"]
    }'
  ```
</CodeGroup>

<Info>
  Use your environment's base URL. See [Environments](/concepts/environments).
</Info>

***

## 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](/webhooks/getting-started/security) 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.
</Warning>

<CodeGroup>
  ```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);
  });
  ```
</CodeGroup>

### 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](/webhooks/operations/delivery). You can also retry a failed delivery from the endpoint's **Delivery history** or with [`POST /v2/webhooks/retry/{delivery_nanoid}`](/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}`](/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.
</Warning>

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

<AccordionGroup>
  <Accordion title="A PSA was signed but no event arrived" icon="triangle-exclamation">
    * 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.
  </Accordion>

  <Accordion title="The delivery shows as failed" icon="circle-xmark">
    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](/webhooks/operations/troubleshooting) for common HTTP and signature errors.
  </Accordion>

  <Accordion title="The delivery arrived but validation fails" icon="key">
    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`.
  </Accordion>

  <Accordion title="I received the same completion twice" icon="clone">
    This is expected with at-least-once delivery. Deduplicate on `document.version_nanoid`.
  </Accordion>

  <Accordion title="Still missing" icon="life-ring">
    Contact Rise support with the endpoint's `webhook_nanoid`, the document `nanoid` (if you have it), and roughly when the last signature happened.
  </Accordion>
</AccordionGroup>


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