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

# Webhooks

Receive trade push notifications using webhooks. A webhook subscription specifies an event type you wish to listen to, optional filters, and the URL that receives matching callbacks.

The management API is available at `https://data.gravia.trade/v1/webhooks`. The current live feed covers confirmed `OrderFilled` events from chain (A single trade might be split into multiple different fills).

<Info>
  This is made to be closely compatible with Struct to function as a drop in replacement with minimal changes required.
</Info>

## Available Events

| Event | Use it to |
| - | - |
| `trader_new_trade` | Receive trades filtered by wallets, markets, trade amount, or a price range. |
| `close_to_bond` | Receive trades at or beyond a high or low outcome-token price threshold. You can choose to accept every qualifying trade or specify a latch-style threshhold mechanism where one event is sent per threshold crossing. |

All the events support ongoing subscriptions and `one_shot` subscriptions, which are deleted after their first successful delivery.

## Quickstart

You need a Gravia API key and a public HTTPS endpoint that accepts JSON POST requests. Localhost and private network destinations are not supported, and redirects are not followed.

<Note>
  You can obtain a Gravia API key by letting us know, check out [Quickstart](/quickstart) to get up and running with us!
</Note>

### 1. Set your API key

Send your API key in the `Authorization` header, without a `Bearer` prefix. Use this header for all webhook requests.

```sh theme={"dark"}
export GRAVIA_API_KEY='replace-with-your-api-key'
```

Subscriptions are owned and grouped by the API key that was used to create them. A webhook ID is not a credential and is only availed to manage it, you still need to transmit the `Authorization` header on all requests.

### 2. Create a subscription

Subscriptions optionally support HMAC signing on the webhook notification that's being forwarded to you, to avail this, you can provide the signing secret when initially creating the webhook.

Replace the receiver URL and signing secret below. Use a separate, randomly generated signing secret for callback verification; it isn't your Gravia API key.

```sh theme={"dark"}
curl --request POST 'https://data.gravia.trade/v1/webhooks' \
  --header "Authorization: $GRAVIA_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{
    "url": "https://receiver.example/webhooks/trades",
    "event": "trader_new_trade",
    "secret": "replace-with-a-random-signing-secret",
    "description": "Trades worth at least $100",
    "filters": {
      "min_usd_value": 100
    }
  }'
```

A successful creation returns HTTP `201`. Save the subscription ID from `data.id` and keep your signing secret in your receiver's secret storage. Subscriptions are active immediately after a success response.

| Field | Required | Description |
| - | - | - |
| `url` | Yes | Public HTTPS callback destination. |
| `event` | Yes | `trader_new_trade`, `close_to_bond`, etc... |
| `filters` | For `close_to_bond` | Event-specific settings. A close-to-bond subscription would require at least one price threshold be given at creation. |
| `secret` | No | Nonempty secret enables HMAC signatures on callbacks. Recommended for verifying callbacks. Additionally, integrate some sort of random string in the callback url to act as API key which can be simpler to setup.<br /><br />Do not in any case expose a generic / enumerable endpoint without HMAC signing, if someone with malicious intent gets hold of the callback url without HMAC signing setup, they can impersonate our notification format. |
| `description` | No | A label for your subscription. |

Management success responses wrap the result in `data`:

```json theme={"dark"}
{
  "success": true,
  "message": null,
  "data": { ... },
  "info": {"version": "1.0.0", "credits_consumed": 0} 
  // Credits consumed is only provided to maintain compatibility with struct.
}
```

Here, `data: {}` illustrates the response wrapper. Create and read operations return the subscription object, including `id`, `url`, `event`, `filters` when present, `status`, `description`, `has_secret`, `created_at`, `updated_at`, and `credits_used_24h`. Ordinary reads do not return the signing secret.

<Note>
  Credits consumed field doesn't imply that we bill you for the usage, as of writing this, the Gravia API is free to use upon obtaining an API Key.
</Note>

### 3. Send a test callback

Set the ID returned by the create request:

```sh theme={"dark"}
export WEBHOOK_ID='replace-with-data.id'

curl --request POST "https://data.gravia.trade/v1/webhooks/$WEBHOOK_ID/test" \
  --header "Authorization: $GRAVIA_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{}'
```

The test sends a synthetic callback to your configured URL using your signing secret, if set. Its event is `trader_new_trade_test` or `close_to_bond_test`. Inspect `data.success`, `data.status_code`, `data.error`, and `data.duration_ms` in the management response to see the delivery result.

Tests send out one event and work regardless of subscription's active status, doesn't consume a one-shot subscription or change latch/threshold states for `close_to_bond` events. These can be used to verify receiver connectivity, the data provided is dummy data. Essentially, test events don't in any capacity effect your ongoing subscriptions and can be triggered as needed.

## Configure new-trade notifications

A `trader_new_trade` subscription receives each trade satisfying its filters. For example, this create-request body tracks one wallet's trades worth at least \$100, with prices from 20 to 80 cents:

```json theme={"dark"}
{
  "url": "https://receiver.example/webhooks/trades",
  "event": "trader_new_trade",
  "secret": "replace-with-a-random-signing-secret",
  "filters": {
    "wallet_addresses": ["0x1111111111111111111111111111111111111111"],
    "min_usd_value": 100,
    "min_price": 0.20,
    "max_price": 0.80
  }
}
```

Replace the example wallet with the address you want to follow.

| Filter | Type | Behavior |
| - | - | - |
| `wallet_addresses` | array\[String] | Match the trade's `trader` address. Each address is a 20-byte `0x` hexadecimal address. |
| `condition_ids` | array\[String] | Match market condition IDs, each a 32-byte `0x` hexadecimal value. |
| `event_slugs` | array\[String] | Match event slugs exactly. |
| `min_usd_value` | Float | Minimum `amount_usd`, inclusive; must be nonnegative. |
| `min_price` | Float (0.0 → 1.0) | Minimum outcome-token price, inclusive. |
| `max_price` | Float (0.0 → 1.0) | Maximum outcome-token price, inclusive. |
| `trade_types` | array\[String] | Match `OrderFilled`, `OrdersMatched`, or `ComboExecution`. Current live coverage is `OrderFilled`. |
| `exclude_shortterm_markets` | Boolean | Exclude trades classified as short-term markets. Requires classification metadata. |
| `one_shot` | Boolean | Delete the subscription after its first successful delivery. Defaults to false. |

Prices range from 0 to 1, so `0.80` means 80 cents. When both price bounds are present, `min_price` must be less than or equal to `max_price`. Omitting filters or supplying `{}` subscribes to all supported new trades.

## Configure price-threshold notifications

For `close_to_bond`, price filters define alternative high and low trigger zones. They do not define a price range.

| Setting | Behavior |
| - | - |
| `min_price` | High threshold: match when `price >= min_price`. |
| `max_price` | Low threshold: match when `price <= max_price`. |
| `trigger_mode` | `every_trade` (default) or `crossing`. |
| `reset_distance` | Absolute price distance from a threshold required to rearm crossing notifications. Only accepted in crossing mode; defaults to `0.10`. |

Set at least one threshold. Thresholds must be between 0 and 1 inclusive. If both are set, `min_price` must be greater than `max_price`. Actual trades priced exactly 0 or 1 neither trigger notifications nor rearm crossing state.

### Every qualifying trade

The following create-request body sends a notification for every trade at or above 90 cents or at or below 10 cents:

```json theme={"dark"}
{
  "url": "https://receiver.example/webhooks/prices",
  "event": "close_to_bond",
  "secret": "replace-with-a-random-signing-secret",
  "filters": {
    "min_price": 0.90,
    "max_price": 0.10,
    "trigger_mode": "every_trade"
  }
}
```

Omitting `trigger_mode` also selects this behavior. Repeated trades inside a trigger zone each qualify.

### One notification per crossing

Use crossing mode to suppress repeated notifications until the price moves far enough away from the threshold:

```json theme={"dark"}
{
  "url": "https://receiver.example/webhooks/prices",
  "event": "close_to_bond",
  "secret": "replace-with-a-random-signing-secret",
  "filters": {
    "min_price": 0.90,
    "max_price": 0.10,
    "trigger_mode": "crossing",
    "reset_distance": 0.10
  }
}
```

With these settings, a high-side notification fires at 90 cents or above. That side can fire again after a trade at 80 cents or below rearms it. A low-side notification fires at 10 cents or below and rearms at 20 cents or above. Rearming alone does not send a notification. Reaching the opposite configured trigger zone fires immediately, even without an intermediate reset trade.

| Observed trade price | Crossing mode | Every-trade mode |
| - | - | - |
| 91 cents | High notification | High notification |
| 95 cents | No notification | High notification |
| 85 cents | No notification | No notification |
| 80 cents | Rearm high side; no notification | No notification |
| 90 cents | High notification | High notification |
| 9 cents | Low notification | Low notification |
| 5 cents | No notification | Low notification |
| 20 cents | Rearm low side; no notification | No notification |
| 10 cents | Low notification | Low notification |

Crossing state is independent for each subscription and `position_id`. The first observed qualifying trade fires even if the price is already inside a trigger zone. Price always refers to the traded outcome token: a Down token at 95 cents matches the high side.

`reset_distance` is measured from the configured threshold, not the latest peak or trough. `0.10` means 10 cents, not a relative 10% change. It must be greater than 0 and less than 1. Configured reset boundaries must also stay strictly inside the price range:

* High side: `min_price - reset_distance > 0`.
* Low side: `max_price + reset_distance < 1`.

Changing the event or filters, or resuming a paused subscription, starts a newly armed state. Changing only the URL, description, or secret preserves crossing state. State normally survives service restarts; by default, it expires 30 days after the last firing or side-switch event. Expiration or loss of stored state allows the next qualifying trade to fire again without an intervening reset.

### Restrict the markets or outcomes

Add any of these filters alongside the price settings:

| Filter | Type | Behavior |
| - | - | - |
| `position_ids` | array\[String] | Match outcome-token IDs as decimal strings, not JSON numbers. |
| `condition_ids` | array\[String] | Match market condition IDs. |
| `outcomes` | array\[String] | Match outcome names exactly, including case. |
| `position_outcome_indices` | array\[Int] | Match nonnegative outcome indices. |
| `event_slugs` | array\[String] | Match event slugs exactly. |
| `exclude_shortterm_markets` | Boolean | Exclude classified short-term markets. |
| `one_shot` | Boolean | Delete the subscription after its first successful delivery across all positions. |

For example, add `"position_ids": ["12345678901234567890"]` to track a particular outcome token, replacing the example ID with the token you want to follow. Without scope filters, the thresholds apply across the supported feed.

<Warning>
  Exclude Short-term markets setting has no effect as of writing, it exists purely to maintain compatibility.
</Warning>

## How filters combine

Different filters use AND: a trade must satisfy every configured filter. Values inside an array use OR: matching any listed value is sufficient. Empty arrays impose no restriction. Each array can be at most 500 entries beyond which an error response is returned.

Wallet addresses and condition IDs match case-insensitively. Event slugs, outcome names, and trade types match exactly. Missing metadata required by a filter prevents the trade from matching until that metadata is available. The current feed does not supply short-term classification, so enabling `exclude_shortterm_markets` can prevent notifications from that feed.

In crossing mode, scope filters apply both to triggering and rearming. State follows observed trade processing order; delayed trades can affect later crossings. It does not reconstruct a complete price history.

## Receive and verify callbacks

Callbacks are HTTPS POST requests with a JSON body. The following abbreviated example shows the envelope and selected fields of a close-to-bond notification; actual `data` contains additional trade fields:

```json theme={"dark"}
{
  "id": "d29dcff2-9a3b-5db6-857a-6cf7c20a4368",
  "event": "close_to_bond",
  "data": {
    "trade_id": "example-trade-id",
    "position_id": "12345678901234567890",
    "price": 0.91,
    "bond_side": "high",
    "threshold": 0.90
  },
  "timestamp": 1790467200000,
  "webhook_id": "32b27c4c-6304-4b1e-87ad-3e352839be42",
  "attempt": 1
}
```

| Field | Meaning |
| - | - |
| `id` | Logical delivery ID. Stable across retries; use it for deduplication. |
| `event` | Event type, with a `_test` suffix for synthetic tests. |
| `data` | Event-specific trade payload. |
| `timestamp` | Callback timestamp in Unix milliseconds. |
| `webhook_id` | Subscription that produced this callback. |
| `attempt` | Delivery attempt number, starting at 1. |

Both events include trade identity and execution fields such as `trader`, `taker`, `position_id`, `trade_id`, `hash`, `block`, `confirmed_at`, `amount_usd`, `shares_amount`, `fee`, `side`, and `price`, plus market metadata when available. `confirmed_at` uses Unix **seconds**, unlike the envelope timestamp. Token IDs are strings and should remain strings in your application. Unavailable market metadata may be null.

New-trade payloads additionally include `trader_info`, `exchange`, `trade_type`, `image_url`, and `probability`, with optional builder fields. Close-to-bond payloads instead include `bond_side` (`high` or `low`) and `threshold`. These identify the matched price threshold; `side` separately describes the trade as `Buy` or `Sell`. Both threshold modes use the same payload shape.

### Signature verification

Callback headers include `X-Webhook-ID`, `X-Delivery-ID`, `X-Event-Type`, and `X-Attempt`. If a signing secret is configured, `X-Webhook-Signature` contains `sha256=<hex-digest>`.

Compute HMAC-SHA256 over the exact raw request body using the signing secret as UTF-8 bytes, then compare the complete signature in constant time. Use the secret exactly as supplied or returned; do not decode it as base64. Verify before parsing or processing the callback. Re-serializing parsed JSON changes the signed bytes.

This Python helper verifies a callback using the standard library:

```python theme={"dark"}
import hashlib
import hmac


def verify_signature(raw_body: bytes, signature: str, secret: str) -> bool:
    if not secret or not signature or not signature.isascii():
        return False
    expected = "sha256=" + hmac.new(
        secret.encode("utf-8"), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)
```

Pass the unmodified request bytes, the `X-Webhook-Signature` header, and the signing secret associated with the subscription to this function. A receiver configured to require signatures should reject missing or invalid signatures.

### Acknowledgments and retries

After verification, durably record or enqueue the callback before returning a `2xx` response. Process longer-running work asynchronously. Deduplicate by `id` with a durable, atomic check so simultaneous retries do not repeat application actions; acknowledge already accepted deliveries with `2xx`.

Delivery is at least once, and callbacks can arrive out of order. Non-`2xx` responses and transport failures retry up to 10 total attempts, with a one-second retry delay and a 30-second request timeout. Retries retain the delivery ID but can change the attempt number, timestamp, and signature.

After retries are exhausted, the subscription's current configuration is disabled. Fix the receiver, then set `status` to `active` to re-enable it. One crossing produces one logical event, but retries may produce multiple HTTP requests. With `one_shot: true`, the first claimed delivery is retried as needed and a successful delivery deletes the whole subscription.

<Danger>
  Ensure that you're always checking and activating the subscription statuses when restarting your application or reconnecting after long network failure.
</Danger>

## Manage subscriptions

Every endpoint below requires the creating API key in `Authorization`. Paths are relative to `https://data.gravia.trade`.

| Method | Path | Purpose |
| - | - | - |
| `POST` | `/v1/webhooks` | Create a subscription. |
| `GET` | `/v1/webhooks` | List your subscriptions. |
| `GET` | `/v1/webhooks/events` | Discover supported events and applicable filters. |
| `GET` | `/v1/webhooks/{id}` | Read a subscription. |
| `PUT` | `/v1/webhooks/{id}` | Update, pause, or resume a subscription. |
| `DELETE` | `/v1/webhooks/{id}` | Delete a subscription. |
| `POST` | `/v1/webhooks/{id}/rotate-secret` | Generate a new signing secret. Send `{}`. |
| `POST` | `/v1/webhooks/{id}/test` | Request a test callback. Send `{}`. |
| `GET` | `/v1/webhooks/{id}/logs` | Read terminal delivery results. |

### Discover and list

```sh theme={"dark"}
curl 'https://data.gravia.trade/v1/webhooks/events' \
  --header "Authorization: $GRAVIA_API_KEY"

curl 'https://data.gravia.trade/v1/webhooks?limit=20&offset=0&status=active' \
  --header "Authorization: $GRAVIA_API_KEY"
```

Event discovery returns `data.events`. Listing returns `data.webhooks` and `data.total`. Lists support `limit` (default 20, maximum 100), `offset` (default 0), and optional `status` (`active` or `paused`) and `event` filters.

### Update, pause, or resume

```sh theme={"dark"}
curl --request PUT "https://data.gravia.trade/v1/webhooks/$WEBHOOK_ID" \
  --header "Authorization: $GRAVIA_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{"status":"paused"}'
```

Use `{"status":"active"}` to resume. Updates are partial: omitted or null fields retain their existing values. A supplied `filters` object replaces the **entire** filter set, so include all thresholds and scope filters you want to keep. For example, to change a high-only crossing subscription to a 95-cent threshold:

```json theme={"dark"}
{
  "filters": {
    "min_price": 0.95,
    "trigger_mode": "crossing",
    "reset_distance": 0.10
  }
}
```

That replacement removes any previous scope filters and low threshold. Omitting `trigger_mode` from a replacement selects every-trade mode. `filters: {}` removes filtering for `trader_new_trade`; it is invalid for `close_to_bond`, which still requires a threshold. An empty `secret` disables callback signing.

Pause and delete prevent new attempts once the configuration change is observed. Already-started requests can finish. Resuming crossing subscriptions starts newly armed state.

### Rotate the signing secret

```sh theme={"dark"}
curl --request POST "https://data.gravia.trade/v1/webhooks/$WEBHOOK_ID/rotate-secret" \
  --header "Authorization: $GRAVIA_API_KEY" \
  --header 'Content-Type: application/json' \
  --data '{}'
```

Securely save `data.secret` and update your receiver; ordinary reads cannot retrieve it. Rotation keeps the subscription ID and management API key unchanged. Subsequent attempts use the new secret once the change is observed. Allow for requests already in progress with the previous secret during the transition.

### Inspect delivery logs

```sh theme={"dark"}
curl "https://data.gravia.trade/v1/webhooks/$WEBHOOK_ID/logs?limit=10" \
  --header "Authorization: $GRAVIA_API_KEY"
```

Logs show terminal delivery outcomes, not each retry or synthetic test. Recent results may take time to appear. Use `limit` (default 10, maximum 250), optional `from` and `to` timestamps in Unix milliseconds, and `pagination_key` for subsequent pages. When `data.pagination.has_more` is true, pass `data.pagination.pagination_key` as the next `pagination_key`, URL-encoding it. The page's `total` counts entries on that page; results are newest first.

### Delete a subscription

```sh theme={"dark"}
curl --request DELETE "https://data.gravia.trade/v1/webhooks/$WEBHOOK_ID" \
  --header "Authorization: $GRAVIA_API_KEY"
```

The response contains `data: {"deleted": true}`. Deleted subscriptions and their logs are no longer accessible through management endpoints.

## Troubleshooting

| Symptom | What to check |
| - | - |
| `400` response | Confirm the HTTPS URL, event-specific filters, price thresholds, and crossing reset boundaries. Read the response `message`. |
| `401` response | Send the API key verbatim in `Authorization`; omit the `Bearer` prefix. |
| `404` response | Confirm the subscription ID and use the API key that created it. Deleted and unowned subscriptions are unavailable. |
| `429` response | Follow the response's rate-limit headers and `Retry-After` when present. |
| `503` response | The service is temporarily unavailable. An accepted test may still complete after its management request times out. |
| Test succeeds but no live callbacks | Check subscription status, live feed coverage, filter values, and required metadata. A test bypasses live matching. |
| Repeated callbacks | Compare delivery IDs. The same ID indicates a retry; different IDs may be separate trades in every-trade mode. |
| Crossing stops firing | Check whether an observed trade satisfying the scope filters has reached the reset boundary or opposite threshold. |
| Signature mismatch | Verify the raw request bytes with the correct signing secret, including any rotation transition. |
| Deliveries stop after failures | Inspect logs, correct the receiver failure, and reactivate the subscription. |

Management errors include `success: false`, a descriptive `message`, and `data: null`.
