> For the complete documentation index, see [llms.txt](https://docs.teranode.group/tng-identity-documentation/llms.txt). Markdown versions of documentation pages are available by appending `.md` to page URLs; this page is available as [Markdown](https://docs.teranode.group/tng-identity-documentation/introduction/portal-api/guides/manage-webhooks.md).

# Manage Webhooks

### 1. Context

Webhooks allow you to receive real-time notifications when events occur in the Issuer or Verifier services. When an event happens (e.g. a credential offer is created or a verifiable presentation is updated), Identity Portal sends an HTTP `POST` request to the URL you configure.

Each webhook configuration is scoped to a single service — **Issuer** or **Verifier** — and can subscribe to one or more event types within that service.

### 2. Authentication

All webhook management endpoints require:

* **Bearer token** — a valid Cognito JWT
* **API key** — passed via the `x-api-key` header

Required roles: `IssuerAdmin`, `VerifierAdmin`, `IssuerDev`, `VerifierDev`, or `SuperDev`.

{% hint style="info" %}
`SuperDev` users can view and manage all webhook configurations in the environment. Other roles can only manage webhooks they created.
{% endhint %}

### 3. Webhook constraints

* **URL** must start with `https://` (max 512 characters)
* **Name** must be between 3 and 60 characters
* **Subscribed events** must belong to a single service group — you cannot mix Issuer and Verifier events in the same webhook
* There is a per-environment limit on the number of webhook configurations. Use the Get usage endpoint to check your current usage.

### 4. API endpoints

Base URL:

```
https://identity.products.teranode.group/products/web/{envHash}/portal/backend/
```

{% hint style="info" %}
Please note the \`envHash\` variable needs to be replaced by what the system generated on environment creation. You can find it in the URL of the browser when you are accessing the Web Portals UI
{% endhint %}

#### 4.1. Create a webhook

```
POST /api/v1/webhooks
```

Creates a new webhook configuration and returns the signing secret.

**Request:**

```bash
curl -X POST https://identity.products.teranode.group/products/web/{envHash}/portal/backend/api/v1/webhooks \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Issuer prod webhook",
    "url": "https://your-server.com/webhooks",
    "subscribedEvents": [
      "credentialOffers.created",
      "credentialOffers.updated"
    ]
  }'
```

**Body parameters:**

* **name** `string` *required* — a user-friendly name for the webhook (3–60 characters)
* **url** `string` *required* — the HTTPS URL that will receive webhook POST requests (max 512 characters)
* **subscribedEvents** `string[]` *required* — list of event types to subscribe to (see Supported event types)

**Response:**

```
201 - created
```

```json
{
  "id": "3f389702-8d44-4c53-92cf-2b14f7f00f62",
  "name": "Issuer prod webhook",
  "url": "https://your-server.com/webhooks",
  "partialSecret": "a1b...j0",
  "subscribedEvents": [
    "credentialOffers.created",
    "credentialOffers.updated"
  ],
  "createdAt": "2026-02-18T09:30:00.000Z",
  "plainSecret": "a1b2c3d4e5f6g7h8i9j0"
}
```

{% hint style="warning" %}
The `plainSecret` field is **only returned in this response**. Store it securely immediately as it cannot be retrieved again. You will need it to verify webhook signatures.
{% endhint %}

#### 4.2. List webhooks

```
GET /api/v1/webhooks
```

Returns all webhook configurations visible to the authenticated user.

**Request:**

```bash
curl https://identity.products.teranode.group/products/web/{envHash}/portal/backend/api/v1/webhooks \
  -H "x-api-key: YOUR_API_KEY"
```

**Query parameters:**

* **service** `string` *optional* — filter by service: `issuer`, `verifier`, or `issuer,verifier`

**Example filters:**

```
GET /api/v1/webhooks?service=issuer
GET /api/v1/webhooks?service=verifier
GET /api/v1/webhooks?service=issuer,verifier
```

**Response:**

```
200 - ok
```

```json
[
  {
    "id": "3f389702-8d44-4c53-92cf-2b14f7f00f62",
    "name": "Issuer prod webhook",
    "url": "https://your-server.com/webhooks",
    "partialSecret": "a1b...j0",
    "subscribedEvents": ["credentialOffers.created"],
    "createdAt": "2026-02-18T09:30:00.000Z"
  }
]
```

#### 4.3. Get a webhook

```
GET /api/v1/webhooks/:id
```

Returns a single webhook configuration by ID.

**Request:**

```bash
curl https://identity.products.teranode.group/products/web/{envHash}/portal/backend/api/v1/webhooks/3f389702-8d44-4c53-92cf-2b14f7f00f62 \
  -H "x-api-key: YOUR_API_KEY"
```

**Response:**

```
200 - ok
```

```json
{
  "id": "3f389702-8d44-4c53-92cf-2b14f7f00f62",
  "name": "Issuer prod webhook",
  "url": "https://your-server.com/webhooks",
  "partialSecret": "a1b...j0",
  "subscribedEvents": ["credentialOffers.created"],
  "createdAt": "2026-02-18T09:30:00.000Z"
}
```

#### 4.4. Update a webhook

```
PATCH /api/v1/webhooks/:id
```

Updates a webhook configuration. All body fields are optional.

**Request:**

```bash
curl -X PATCH https://identity.products.teranode.group/products/web/{envHash}/portal/backend/api/v1/webhooks/3f389702-8d44-4c53-92cf-2b14f7f00f62 \
  -H "x-api-key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Issuer primary webhook",
    "url": "https://your-server.com/webhooks/v2",
    "subscribedEvents": [
      "credentialOffers.created",
      "credentialOffers.updated"
    ]
  }'
```

**Body parameters:**

* **name** `string` *optional* — updated name (3–60 characters)
* **url** `string` *optional* — updated HTTPS URL (max 512 characters)
* **subscribedEvents** `string[]` *optional* — updated list of event types

**Response:**

```
200 - ok
```

```json
{
  "id": "3f389702-8d44-4c53-92cf-2b14f7f00f62",
  "name": "Issuer primary webhook",
  "url": "https://your-server.com/webhooks/v2",
  "partialSecret": "a1b...j0",
  "subscribedEvents": [
    "credentialOffers.created",
    "credentialOffers.updated"
  ],
  "createdAt": "2026-02-18T09:30:00.000Z"
}
```

#### 4.5. Delete a webhook

```
DELETE /api/v1/webhooks/:id
```

Deletes a webhook configuration. Events will no longer be delivered to this URL.

**Request:**

```bash
curl -X DELETE https://identity.products.teranode.group/products/web/{envHash}/portal/backend/api/v1/webhooks/3f389702-8d44-4c53-92cf-2b14f7f00f62 \
  -H "x-api-key: YOUR_API_KEY"
```

**Response:**

```
200 - ok
```

```json
{
  "message": "Webhook configuration deleted successfully."
}
```

#### 4.6. Get webhook usage

```
GET /api/v1/webhooks/usage
```

Returns how many webhook configurations exist in the current environment and the maximum allowed.

**Request:**

```bash
curl https://identity.products.teranode.group/products/web/{envHash}/portal/backend/api/v1/webhooks/usage \
  -H "x-api-key: YOUR_API_KEY"
```

**Query parameters:**

* **service** `string` *optional* — filter by service: `issuer`, `verifier`, or `issuer,verifier`

**Response:**

```
200 - ok
```

```json
{
  "used": 3,
  "limit": 10
}
```

### 5. Supported event types

Each webhook must subscribe to events from a single service group.

#### Issuer events

| Event type                 | Description                                   |
| -------------------------- | --------------------------------------------- |
| `credentialOffers.created` | A new credential offer has been created       |
| `credentialOffers.updated` | An existing credential offer has been updated |

#### Verifier events

| Event type                       | Description                                          |
| -------------------------------- | ---------------------------------------------------- |
| `verifiablePresentation.created` | A new verifiable presentation has been created       |
| `verifiablePresentation.updated` | An existing verifiable presentation has been updated |

{% hint style="warning" %}
You cannot mix Issuer and Verifier event types in the same webhook configuration. For example, subscribing to both `credentialOffers.created` and `verifiablePresentation.created` in a single webhook is not allowed.
{% endhint %}

### 6. Webhook delivery

#### Delivery envelope

When an event occurs, Identity Portal sends an HTTP `POST` request to your configured URL with the following JSON body:

```json
{
  "id": "2b95ec1e-7f43-4bd6-bacc-c0f91e9f7f8f",
  "type": "event",
  "payload": {
    "event": "credentialOffers.created",
    "data": {
      "id": "7e7b8e48-30ae-4f7d-8f74-fb0ed8f398d8",
      "organisationProfileId": "293829c8-70de-4eb2-a424-d6d96840dfe2",
      "credentialId": "f790bd03-9780-4a5e-9289-38f12bcf3ef3",
      "status": "ready_to_claim",
      "createdAt": "2026-02-18T09:30:00.000Z"
    }
  }
}
```

* **id** `string` — unique identifier for this webhook event (use for idempotency)
* **type** `string` — always `"event"`
* **payload.event** `string` — the event type that triggered this delivery
* **payload.data** `object` — the event-specific data (see Event payloads below)

#### Headers

Each delivery includes these headers:

| Header                | Description                         |
| --------------------- | ----------------------------------- |
| `Content-Type`        | `application/json`                  |
| `X-Webhook-Signature` | HMAC-SHA256 signature (hex-encoded) |
| `X-Webhook-Timestamp` | Unix timestamp in milliseconds      |

For details on how to verify signatures, see [Verify webhook signatures](/tng-identity-documentation/introduction/portal-api/guides/verify-webhook-signatures.md).

#### Retry behavior

* Your endpoint must return HTTP **200** to acknowledge successful delivery
* Any other status code (or a network error) is treated as a failure
* Failed deliveries are retried up to **12 times** over approximately **2.8 days**
* Backoff schedule: immediate, 5 min, 30 min, 1 hour, 2 hours, 5 hours, 10 hours, 12 hours (repeating)
* Request timeout: **5 seconds**

{% hint style="info" %}
Webhook delivery is **at-least-once**. The same event may be delivered more than once. Use the `id` field in the delivery envelope for idempotency.
{% endhint %}

#### Event payloads

**Credential offer events**

Events: `credentialOffers.created`, `credentialOffers.updated`

| Field                   | Type     | Required | Description                            |
| ----------------------- | -------- | -------- | -------------------------------------- |
| `id`                    | `string` | yes      | Credential offer UUID                  |
| `organisationProfileId` | `string` | yes      | Organisation profile identifier        |
| `credentialId`          | `string` | yes      | Credential identifier                  |
| `issuer`                | `string` | no       | Issuer identifier                      |
| `subject`               | `string` | no       | Subject identifier                     |
| `status`                | `string` | yes      | Current status of the credential offer |
| `expirationDate`        | `string` | no       | ISO 8601 datetime                      |
| `createdAt`             | `string` | yes      | ISO 8601 datetime                      |
| `updatedAt`             | `string` | no       | ISO 8601 datetime                      |

**Verifiable presentation events**

Events: `verifiablePresentation.created`, `verifiablePresentation.updated`

| Field          | Type      | Required | Description                                  |
| -------------- | --------- | -------- | -------------------------------------------- |
| `id`           | `string`  | yes      | Verifiable presentation UUID                 |
| `definitionId` | `string`  | yes      | Presentation definition identifier           |
| `status`       | `string`  | yes      | Current status of the presentation           |
| `createdBySSO` | `boolean` | no       | Whether the presentation was created via SSO |
| `createdAt`    | `string`  | yes      | ISO 8601 datetime                            |
| `updatedAt`    | `string`  | no       | ISO 8601 datetime                            |
| `expiresAt`    | `string`  | yes      | ISO 8601 datetime                            |
| `error`        | `object`  | no       | Error details, if any                        |

### 7. Error responses

| Status | Description                                |
| ------ | ------------------------------------------ |
| `400`  | Invalid request body or parameters         |
| `401`  | Missing or invalid authentication          |
| `403`  | Insufficient permissions                   |
| `404`  | Webhook configuration not found            |
| `409`  | Webhook limit reached for this environment |
