# Upgrade to v0.16

This guide covers breaking changes and migration steps when upgrading from v0.15 to v0.16.

## Breaking Changes Overview

| Change | Impact | Action Required |
| --- | --- | --- |
| [Webhook signing secret prefix changed](#webhook-signing-secret-prefix-changed) | New webhook destination signing secrets | Set `DESTINATIONS_WEBHOOK_SIGNING_SECRET_TEMPLATE` to preserve previous behavior |
| [Event `destination_id` replaced with `matched_destination_ids`](#event-destination_id-replaced-with-matched_destination_ids) | Event API responses and queries | Update code that reads `destination_id` from events to use `matched_destination_ids` |
| [Delivery metadata timestamp format](#delivery-metadata-timestamp-format) | Consumers of delivery metadata `timestamp` field | Update timestamp parsing from Unix seconds to ISO 8601 |
| [Response data body stored as raw string](#response-data-body-stored-as-raw-string) | Consumers of `response_data.body` | Update code that treats `response_data.body` as a parsed JSON object |
| [Alert callbacks replaced by operator events](#alert-callbacks-replaced-by-operator-events) | Users of `ALERT_CALLBACK_URL` | Migrate to `OPERATION_EVENTS_*` config |

## Webhook Signing Secret Prefix Changed

The default signing secret template for new webhook destinations changed from `{{.RandomHex}}` to `whsec_{{.RandomHex}}`. New destinations will now have signing secrets prefixed with `whsec_` (e.g., `whsec_a1b2c3...`).

Existing destinations keep their current signing secrets — this only affects newly created destinations.

### Preserving previous behavior

To keep generating unprefixed secrets, set this config var:

```
DESTINATIONS_WEBHOOK_SIGNING_SECRET_TEMPLATE={{.RandomHex}}

```

## Event `destination_id` Replaced with `matched_destination_ids`

The `destination_id` field has been removed from events and replaced with `matched_destination_ids`, an array of destination IDs that the event was actually routed to.

### API response change

v0.15:

```json
{
  "id": "evt_123",
  "destination_id": "des_456",
  "topic": "order.created"
}

```

v0.16:

```json
{
  "id": "evt_123",
  "matched_destination_ids": ["des_456", "des_789"],
  "topic": "order.created"
}

```

### Filtering

You can now filter events by destination: `GET /events?destination_id=des_456`. This filters using the new `matched_destination_ids` field.

### No data backfill

Existing events created before the upgrade will have an empty `matched_destination_ids` array. Only events published after the upgrade will have this field populated.

Action: Update any code that reads `event.destination_id` to use `event.matched_destination_ids`. If you need a single destination ID, use the first element of the array.

## Delivery Metadata Timestamp Format

The `timestamp` field in delivery metadata has changed from Unix seconds to ISO 8601 format.

v0.15:

```
timestamp: 1609459200

```

v0.16:

```
timestamp: 2021-01-01T00:00:00Z

```

Action: Update any code that parses the `timestamp` metadata field to handle ISO 8601 (RFC 3339) format instead of Unix seconds.

## Response Data Body Stored as Raw String

The `response_data.body` field in delivery attempts is now always stored as a raw string, regardless of the destination's response content type. Previously, JSON response bodies were parsed into objects.

### API response change

v0.15 — JSON response body was a parsed object:

```json
{
  "status": 200,
  "body": {
    "id": "usr_123",
    "status": "created"
  }
}

```

v0.16 — body is the raw response string:

```json
{
  "status": 200,
  "body": "{\"id\":\"usr_123\",\"status\":\"created\"}"
}

```

Non-JSON responses are unchanged (already stored as strings).

### Database migration

The `attempts.response_data` column is migrated from JSONB to TEXT (migration `000009`). This migration runs automatically on startup. Any direct SQL queries that use JSON operators on `response_data` (e.g., `response_data->'body'->>'key'`) will need to be updated.

Action: Update any code that reads `response_data.body` as a structured object. If you need the parsed object, parse the string with `JSON.parse()` (or equivalent) on the client side.

## Alert Callbacks Replaced by Operator Events

The `ALERT_CALLBACK_URL` config has been removed. Alert notifications are now handled by the new [operator events](/docs/outpost/features/operator-events) system, which supports multiple sink types and additional event topics.

### Migration

If you were using `ALERT_CALLBACK_URL` to receive alert callbacks via HTTP, migrate to the operator events HTTP sink:

v0.15:

```
ALERT_CALLBACK_URL=https://example.com/alerts

```

v0.16:

```
OPERATION_EVENTS_TOPICS=*
OPERATION_EVENTS_HTTP_URL=https://example.com/alerts
OPERATION_EVENTS_HTTP_SIGNING_SECRET=your-secret

```

The HTTP sink signs payloads with HMAC-SHA256 and sends the signature in the `X-Outpost-Signature` header (format: `v0=<hex>`).

### New sink options

In addition to HTTP, operator events can be sent to:

* AWS SQS — `OPERATION_EVENTS_AWS_SQS_QUEUE_URL`, `OPERATION_EVENTS_AWS_SQS_ACCESS_KEY_ID`, `OPERATION_EVENTS_AWS_SQS_SECRET_ACCESS_KEY`, `OPERATION_EVENTS_AWS_SQS_REGION`
* GCP Pub/Sub — `OPERATION_EVENTS_GCP_PUBSUB_PROJECT_ID`, `OPERATION_EVENTS_GCP_PUBSUB_TOPIC_ID`, `OPERATION_EVENTS_GCP_PUBSUB_CREDENTIALS`
* RabbitMQ — `OPERATION_EVENTS_RABBITMQ_SERVER_URL`, `OPERATION_EVENTS_RABBITMQ_EXCHANGE`

### Event topics

Use `OPERATION_EVENTS_TOPICS=*` for all topics, or specify a comma-separated list:

* `alert.destination.consecutive_failure` — emitted at 50%, 70%, 90%, and 100% failure thresholds
* `alert.destination.disabled` — emitted when a destination is auto-disabled
* `alert.attempt.exhausted_retries` — emitted when delivery exhausts all retry attempts
* `tenant.subscription.updated` — emitted on destination create/update/delete/disable/enable

### Alert default changes

The alert thresholds have also changed:

| Setting | v0.15 Default | v0.16 Default |
| --- | --- | --- |
| Consecutive failure count | 20 | 100 |
| Auto-disable destination | `true` | `false` |

If you relied on the previous defaults, set them explicitly:

```
ALERT_CONSECUTIVE_FAILURE_COUNT=20
ALERT_AUTO_DISABLE_DESTINATION=true

```

## Other Notable Changes

These changes are not breaking but may be useful to know about.

### DELETE /destinations response

`DELETE /tenants/{tid}/destinations/{did}` now returns `{ "success": true }` instead of the full destination object, matching the behavior of `DELETE /tenants/{tid}`.

### Empty webhook header prefix

You can now disable the webhook header prefix by setting it to whitespace (e.g., `DESTINATIONS_WEBHOOK_HEADER_PREFIX=" "`). This results in headers like `signature` instead of `x-outpost-signature`.

## Upgrade Checklist

1. Before upgrading:
  
  * [ ] Decide whether to adopt `whsec_` prefixed signing secrets or preserve previous format (set `DESTINATIONS_WEBHOOK_SIGNING_SECRET_TEMPLATE` if preserving)
  * [ ] Update code that reads `event.destination_id` to use `event.matched_destination_ids`
  * [ ] Update any timestamp metadata parsing from Unix seconds to ISO 8601
  * [ ] Update code that reads `response_data.body` as a parsed object — it is now a raw string
  * [ ] If using `ALERT_CALLBACK_URL`, migrate to `OPERATION_EVENTS_*` config
  * [ ] Review alert default changes (consecutive failure count: 20→100, auto-disable: true→false) and set explicitly if needed
  * [ ] Update SDK dependencies to the latest version
2. Upgrade:
  
  * [ ] Update Outpost to v0.16 and restart — database migrations run automatically on startup
3. After upgrading:
  
  * [ ] Verify event queries and filters work with `matched_destination_ids`