# OneSpan Webhooks

OneSpan Sign sends JSON callback notifications (webhooks) to a URL you register whenever
something happens to a package (a "transaction" in the new experience): created, sent, signed,
completed, declined and so on. Payloads are **not signed**. The only authentication is the
callback key you register, sent back in the `Authorization` header.

## When to Use This Skill

* How do I receive OneSpan Sign callback notifications?
* How do I verify the OneSpan callback key in the `Authorization: Basic` header?
* Does OneSpan sign webhook payloads? (No: shared callback key only)
* Why does my OneSpan callback endpoint return 401?
* How do I handle `PACKAGE_COMPLETE`, `DOCUMENT_SIGNED` or `SIGNER_COMPLETE` events?

## Verification (core)

The callback key is passed as `Authorization: Basic {callbackKey}`. That is the **raw key**,
NOT base64(user:pass), so don't decode it as HTTP Basic. Store the whole header value
(including the `Basic ` prefix) as `ONESPAN_AUTHORIZATION` and compare the whole
`Authorization` header to it:

```javascript
const crypto = require('crypto');

// Hash both sides: equal-length buffers, so timingSafeEqual never throws.
function safeEqual(a, b) {
  const ha = crypto.createHash('sha256').update(String(a)).digest();
  const hb = crypto.createHash('sha256').update(String(b)).digest();
  return crypto.timingSafeEqual(ha, hb);
}

function verifyOneSpanRequest(headers, expected = process.env.ONESPAN_AUTHORIZATION) {
  if (!expected) return false;                  // never accept when unconfigured
  const received = headers['authorization'];    // full value, e.g. "Basic myCallbackKey"
  return Boolean(received) && safeEqual(received, expected);
}

```

Return `401` when this fails (missing header or mismatch), `400` for a body that isn't a JSON
object, and `2xx` once handled. Read the body raw (`express.raw({ type: '*/*' })`) so any
content type works, and parse JSON only after the credential check. Python and Next.js
equivalents are in [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/onespan-webhooks/references/verification.md).

**What this does and doesn't give you:** a static shared credential authenticates the sender,
not the body, and gives no replay protection (there is no signature and no timestamp). Use
HTTPS, treat the callback key like a password, and rotate it if it leaks (register a new key
with `POST /api/callback` or in the UI).

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/onespan-webhooks/examples/express/),
> [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/onespan-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/onespan-webhooks/examples/fastapi/).

## Event Types

The event type is the `name` field in the JSON body. Documented events:

| Event | Meaning |
| --- | --- |
| `PACKAGE_CREATE` | Package created |
| `PACKAGE_ACTIVATE` | Package moved from DRAFT or DELETED to SENT |
| `DOCUMENT_SIGNED` | A document was signed, or consent/disclosure accepted (`documentId` set) |
| `SIGNER_COMPLETE` | A signer finished all documents |
| `PACKAGE_COMPLETE` | Package completed signing (`sessionUser` is the last signer) |
| `PACKAGE_DECLINE` | A recipient declined; `message` holds the reason |
| `EMAIL_BOUNCE` | Email bounced; `message` holds the bounce type |

Others: `COBROWSE_REQUEST`, `DOCUMENT_VIEWED`, `KBA_FAILURE`, `PACKAGE_ARCHIVE`,
`PACKAGE_ATTACHMENT`, `PACKAGE_DEACTIVATE`, `PACKAGE_DELETE`, `PACKAGE_DISPUTE`,
`PACKAGE_EXPIRE`, `PACKAGE_READY_FOR_COMPLETE`, `PACKAGE_RESTORE`, `PACKAGE_TRASH`,
`RECORDINGS_READY`, `ROLE_REASSIGN`, `SIGNER_LOCKED`, `TEMPLATE_CREATE` and the eOriginal
events `EDEPOSIT_SUCCESS`, `EDEPOSIT_FAILURE`, `EDEPOSIT_FAILURE_ATTEMPT`. See
[references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/onespan-webhooks/references/overview.md).

Body fields: `name`, `sessionUser`, `packageId`, `documentId`, `message`, `createdDate`
(the docs show ISO strings and, for eDeposit events, epoch milliseconds).

## Environment Variables

```bash
ONESPAN_AUTHORIZATION=Basic myCallbackKey   # full header value: "Basic " + your callback key

```

## Local Development

```bash
npx hookdeck-cli listen 3000 onespan --path /webhooks/onespan

```

No account required. Use the printed URL as the callback URL in OneSpan Sign.

## Resources

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/onespan-webhooks/references/overview.md) - Callback payloads and the full event list
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/onespan-webhooks/references/setup.md) - Registering the callback URL and key (UI and `POST /api/callback`)
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/onespan-webhooks/references/verification.md) - Header check in Node, Python and Next.js, gotchas
* [examples/](https://github.com/hookdeck/webhook-skills/tree/main/skills/onespan-webhooks/examples/) - Runnable Express, Next.js and FastAPI handlers with tests