Hooks
Create Hook
Create a risk monitor, a pre-liquidation check or a post-liquidation action
POST
/
lending-external-securities-v2
/
api
/
hooks
/
{marketId}
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"checkFrequency": "6h"
}'
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
hookType: 'afterLiquidation',
name: 'Desk notification',
actionType: 'inform',
webhookUrl: 'https://ops.example.com/hooks/liquidation',
triggerHookIds: [monitorHookId]
})
}
);
const { data: hook } = await res.json();
{
"success": true,
"data": {
"id": "hook_003",
"marketId": "clx_secmarket_001",
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"active": true,
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"riskThreshold": null,
"checkFrequency": "6h",
"parameterChecks": [],
"triggerHookIds": [],
"actionType": null,
"maxBuyPrice": null,
"webhookUrl": null,
"webhookPayload": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"signerFunded": false,
"lastExecutedAt": null,
"lastResult": null,
"executionCount": 0,
"createdAt": "2026-09-25T12:00:00.000Z",
"updatedAt": "2026-09-25T12:00:00.000Z",
"signed": true,
"webhookSecret": "98771eb4b90512c3cc4337cfd86f5081acc73f58934438ab9840d405a504aa2d"
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
{
"success": false,
"data": null,
"error": {
"code": "MISSING_RISK_FIELDS",
"message": "At least one risk field is required for monitor hooks"
}
}
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{ "field": "riskUri", "message": "riskUri must be a public http(s) URL" }
]
}
}
Creates a hook on a lending market and returns
A liquidation runs the active
The body carries
201 with the stored record. A hook is a rule Trusset evaluates off-chain. Creating one writes nothing on-chain, so there is nothing to sign or confirm.
When each hook type runs
hookType | Runs | What a run does |
|---|---|---|
monitorRisk | On its checkFrequency, and on Execute Hook | Fetches riskUri and evaluates riskFields against the JSON response. An evaluation of false runs every active afterLiquidation hook that lists this monitor in triggerHookIds. A fetch error does not |
beforeLiquidation | When a loan of the market is liquidated, and on Execute Hook | Evaluates parameterChecks against the liquidation’s data, or the loanData sent with that call. The result blocks nothing |
afterLiquidation | When a loan of the market is liquidated, on Execute Hook, and when a monitor it lists in triggerHookIds evaluates to false | Runs its actionType: inform queues a signed delivery to webhookUrl, buy returns unsigned bid steps |
beforeLiquidation and afterLiquidation hooks of every instance that holds the market, once per liquidation, whichever confirm or sync records it first. A liquidation whose atomic sale has not been read back yet runs them once it is.
A failing check records its result and does not stop the liquidation. A buy action runs only when the liquidation opened an auction. A monitor’s breach runs only inform actions, because a breach carries no auction to act on.
Signed deliveries
Aninform action does not post inline. It queues a delivery that a worker sends and retries. A liquidation is announced to a hook at most once, and every attempt of a delivery carries the same id, so a retry is never a second event.
Each attempt waits up to 15 seconds for an answer, and a 2xx answer delivers it. A 5xx, 408, 425 or 429 answer, a timeout or a connection failure is retried, up to eight attempts in all, after pauses of 10 seconds, 30 seconds, 1, 5, 15 and 30 minutes, then an hour. Any other answer, a redirect included, fails the delivery for good, as do a non-public host and a response body over 64 KB.
Each delivery is a POST with these headers:
| Header | Value |
|---|---|
x-webhook-signature | HMAC-SHA256, hex, keyed with the hook’s webhookSecret string exactly as returned, over the x-webhook-timestamp value, a dot and the raw body. Absent when the hook holds no secret, which its signed: false shows |
x-webhook-timestamp | Unix seconds when the attempt is sent. Each retry carries a fresh one, and a fresh signature |
x-trusset-delivery-id | The delivery id, the same on every retry |
x-hook-id, x-hook-type | The hook and the event: afterLiquidation or riskBreach |
user-agent | Trusset-Lending-Hook/1.0 |
id, event, hookId, marketId, emittedAt and timestamp, the last two both the time the event was queued. A liquidation adds loanId, marketLiquidationId, auctionId, collateralAmount, debtOwed, borrower, txHash and marketAddress. It also adds the outcome: realization (ATOMIC, ROUTER or VENUE), the atomic sale’s buyer, proceeds, surplus and reserve as Liquidate Loan returns them, and payee, the loan’s pay-to address. A field with no value reads null. A breach adds monitorHookId, monitorName, riskUri, checkedAt, breached with the failing fields and their values, and fields.
The signing secret is returned once, as webhookSecret, when the hook is created. Store it then. Rotate Hook Secret issues a new one, and List Hook Deliveries lists the deliveries with their status, attempts and last error.
A monitor is due once checkFrequency has passed since its last run, and a new monitor is due at once. The scheduler picks a new monitor up at its next wake-up, not at the moment of this call. Run it once with Execute Hook to see it evaluate.
riskUri and webhookUrl must be http or https URLs on a public host. A localhost, .local, .internal or .localhost hostname, or a private or reserved IP address, is refused at validation. Every outbound call resolves the hostname again and refuses it when any address it resolves to is not public. A monitor’s fetch follows at most four redirects, and each hop is checked the same way. A delivery follows none.Path Parameters
string
required
Body Parameters
string
required
One of
monitorRisk, beforeLiquidation or afterLiquidation.string
Display name, 1 to 128 characters. Defaults to
Risk Monitor, Pre-Liquidation Check or Post-Liquidation Action by hook type.string
URL a monitor fetches on each run, at most 2048 characters. Required for
monitorRisk.array
Checks applied to the JSON body
riskUri returns, at most 20. Required and non-empty for monitorRisk. Evaluation stops at the first field that fails.Show child attributes
Show child attributes
string
required
Dot-notation path into the response body, 1 to 256 characters. A field fails when the path resolves to nothing.
string
One of
>, >=, <, <=, ==, !=. Compared only when threshold is also set. A field with neither passes whenever its path resolves.number
Value the resolved field is compared against. The field is used as the JSON returns it, and
== and != compare strictly, so a numeric string never equals a number.number
Stored with the hook and returned on reads. No evaluation reads it.
string
Polling interval for
monitorRisk. One of 1m, 5m, 15m, 30m, 1h, 6h, 12h, 24h. A hook without a checkFrequency is never scheduled and only runs when triggered manually.array
Checks for
beforeLiquidation, at most 10. They are evaluated against the liquidation’s data, or the loanData sent to Execute Hook, not against chain state. A check fails when its parameter is missing from that data. A liquidation’s data carries debtValue, the same figure as debtOwed in a delivery, and none of the other four parameters, so a check on any other parameter fails on every liquidation.string
Required for
afterLiquidation. buy prices a bid for the remaining collateral of the auction named in the run’s liquidationData, and returns the unsigned approval and bid steps; nothing is bid automatically. inform queues a signed delivery of the liquidation data to webhookUrl, as Signed deliveries describes.string
Ceiling for a
buy action, as a decimal string in the borrow asset per whole collateral token. When the auction’s current price is above it, the run returns no steps.string
Destination of an
inform action, at most 2048 characters. Required whenever actionType is inform.object
Extra fields merged into the
inform body. The fixed fields listed under Signed deliveries overwrite a key of the same name.array
IDs of other hooks on the same market, at most 10. An
afterLiquidation hook that lists any acts on a liquidation or an Execute Hook call only when every listed hook is active and its last counted run returned false. Otherwise the run records Trigger condition not met and does nothing. A breach of one listed monitor sends its riskBreach delivery without checking the others.Response Fields
object
The new hook record, returned with HTTP
201. See List Hooks for each field. This call issues every hook a signing secret, so signed is true, and webhookSecret carries the secret, 64 hex characters, in this response only.curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"checkFrequency": "6h"
}'
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
hookType: 'afterLiquidation',
name: 'Desk notification',
actionType: 'inform',
webhookUrl: 'https://ops.example.com/hooks/liquidation',
triggerHookIds: [monitorHookId]
})
}
);
const { data: hook } = await res.json();
{
"success": true,
"data": {
"id": "hook_003",
"marketId": "clx_secmarket_001",
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"active": true,
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"riskThreshold": null,
"checkFrequency": "6h",
"parameterChecks": [],
"triggerHookIds": [],
"actionType": null,
"maxBuyPrice": null,
"webhookUrl": null,
"webhookPayload": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"signerFunded": false,
"lastExecutedAt": null,
"lastResult": null,
"executionCount": 0,
"createdAt": "2026-09-25T12:00:00.000Z",
"updatedAt": "2026-09-25T12:00:00.000Z",
"signed": true,
"webhookSecret": "98771eb4b90512c3cc4337cfd86f5081acc73f58934438ab9840d405a504aa2d"
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
{
"success": false,
"data": null,
"error": {
"code": "MISSING_RISK_FIELDS",
"message": "At least one risk field is required for monitor hooks"
}
}
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{ "field": "riskUri", "message": "riskUri must be a public http(s) URL" }
]
}
}
Error Codes
| Code | HTTP | Cause |
|---|---|---|
VALIDATION_ERROR | 400 | A field fails the schema, including a URL on a private or reserved host. error.details lists each { field, message } |
MISSING_RISK_URI | 400 | hookType is monitorRisk and riskUri is absent |
MISSING_RISK_FIELDS | 400 | hookType is monitorRisk and riskFields is absent or empty |
MISSING_ACTION_TYPE | 400 | hookType is afterLiquidation and actionType is absent |
MISSING_WEBHOOK_URL | 400 | actionType is inform and webhookUrl is absent, on any hook type |
INVALID_TRIGGER_HOOKS | 400 | An ID in triggerHookIds is not a hook on this market |
KEY_MATERIAL_REJECTED | 400 | A key in the body, including inside webhookPayload, is named for key material, such as privateKey or mnemonic |
MISSING_MARKET_ID | 400 | marketId is longer than 100 characters |
MARKET_NOT_FOUND | 404 | No market with this ID belongs to your instance |
⌘I
curl -X POST "https://api.trusset.org/lending-external-securities-v2/api/hooks/{marketId}" \
-H "X-API-Key: trusset_your_key_here" \
-H "Content-Type: application/json" \
-d '{
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"checkFrequency": "6h"
}'
const res = await fetch(
`https://api.trusset.org/lending-external-securities-v2/api/hooks/${marketId}`,
{
method: 'POST',
headers: {
'X-API-Key': 'trusset_your_key_here',
'Content-Type': 'application/json'
},
body: JSON.stringify({
hookType: 'afterLiquidation',
name: 'Desk notification',
actionType: 'inform',
webhookUrl: 'https://ops.example.com/hooks/liquidation',
triggerHookIds: [monitorHookId]
})
}
);
const { data: hook } = await res.json();
{
"success": true,
"data": {
"id": "hook_003",
"marketId": "clx_secmarket_001",
"hookType": "monitorRisk",
"name": "NAV Publication Monitor",
"active": true,
"riskUri": "https://nav.example.com/funds/NYF1",
"riskFields": [{ "path": "ageHours", "operator": "<", "threshold": 36 }],
"riskThreshold": null,
"checkFrequency": "6h",
"parameterChecks": [],
"triggerHookIds": [],
"actionType": null,
"maxBuyPrice": null,
"webhookUrl": null,
"webhookPayload": null,
"signerAddress": "0x1234f9a07c6b53d81e2a4f70c9b385d6014a7e52",
"signerFunded": false,
"lastExecutedAt": null,
"lastResult": null,
"executionCount": 0,
"createdAt": "2026-09-25T12:00:00.000Z",
"updatedAt": "2026-09-25T12:00:00.000Z",
"signed": true,
"webhookSecret": "98771eb4b90512c3cc4337cfd86f5081acc73f58934438ab9840d405a504aa2d"
},
"error": null,
"metadata": {
"timestamp": "2026-09-25T12:00:00.000Z",
"requestId": "550e8400-e29b-41d4-a716-446655440000",
"instanceId": "inst_abc123"
}
}
{
"success": false,
"data": null,
"error": {
"code": "MISSING_RISK_FIELDS",
"message": "At least one risk field is required for monitor hooks"
}
}
{
"success": false,
"data": null,
"error": {
"code": "VALIDATION_ERROR",
"message": "Invalid request parameters",
"details": [
{ "field": "riskUri", "message": "riskUri must be a public http(s) URL" }
]
}
}
