Skip to navigation

News Alerts

Get each new article about a company posted to your webhook
View as Markdown

A news alert subscribes your HTTPS endpoint to one company. Each time a public news article is newly linked to that company, directly or through its primary product, aVenture posts one signed entity.news.published event to your endpoint. News alerts work on every signed-in account.

Every call below uses Authorization: Bearer <token> with an API key or OAuth token against https://api.aventure.vc.

OperationUse it forCLI
PUT /v1/news/subscriptionsSubscribe, or refresh an existing subscriptionaventure news subscriptions subscribe
GET /v1/news/subscriptionsList your subscriptions and their delivery healthaventure news subscriptions list
POST /v1/news/subscriptions/{subscriptionId}/test-eventsSend one signed sample event to your endpointaventure news subscriptions test-events send --subscription-id <subscriptionId>
DELETE /v1/news/subscriptionsUnsubscribeaventure news subscriptions unsubscribe

Subscribe

Create a signing secret: whsec_ followed by the base64 encoding of 24 to 64 random bytes.

echo "whsec_$(openssl rand -base64 32)"

Put the subscription in a file. The CLI reads the secret only from a file, never from a flag.

subscribe.json
{
"name": "entity.news.published",
"arguments": { "entityId": "4eabfc26-3ed9-4ad3-a935-9be07ae3329a" },
"delivery": {
"mode": "webhook",
"url": "https://example.com/aventure/news",
"secret": "whsec_..."
},
"cursor": null
}
curl -X PUT "https://api.aventure.vc/v1/news/subscriptions" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d @subscribe.json
Response
{ "id": "sub_...", "refreshBefore": "...", "cursor": "...", "truncated": false }

A new endpoint must first pass verification. An endpoint you verified in the last 24 hours skips it.

The same account, url, event name, and entityId always name the same subscription, so repeating the call refreshes it. A subscription expires at refreshBefore, 7 days out, unless you refresh it; send ttlMs for a shorter lifetime. Set cursor to the cursor of a response or event to resume delivery after it.

Refreshing with a new secret rotates it. The old secret keeps co-signing for 5 minutes, so each request carries two signatures until then.

Limits: 100 subscriptions per account, and an https URL on a public host of at most 2048 characters. Redirects are not followed.

Check and Test a Subscription

GET /v1/news/subscriptions returns an array of your subscriptions. The secret is never returned.

[
{
"id": "sub_...",
"subscription": {
"name": "entity.news.published",
"arguments": { "entityId": "4eabfc26-3ed9-4ad3-a935-9be07ae3329a" },
"delivery": { "url": "https://example.com/aventure/news" }
},
"refreshBefore": "...",
"cursor": "...",
"createdAt": "...",
"verifiedAt": "...",
"status": "active",
"consecutiveFailureCount": 0,
"lastError": null,
"nextAttemptAt": "...",
"updatedAt": "..."
}
]
statusMeaning
activeDelivering normally
retryingThe last delivery failed; nextAttemptAt is the next try and lastError says why
paused12 consecutive deliveries failed. Delivery stops until you refresh with PUT
expiredrefreshBefore passed without a refresh

subscription is exactly the body that unsubscribes it.

To check your endpoint end to end, send a test event:

curl -X POST "https://api.aventure.vc/v1/news/subscriptions/$SUBSCRIPTION_ID/test-events" \
-H "Authorization: Bearer $AUTH_TOKEN"

It posts one signed sample entity.news.published event, built from the company’s earliest linked article or the newest catalog article when the company has none, and does not move the cursor. The answer reports what your endpoint returned: {"eventId": "evt_test_...", "status": 200, "acknowledged": true}. acknowledged is true for any 2xx. When your endpoint gives no HTTP answer, the call returns 422 with code callback_connection_refused, callback_timeout, or callback_tls_error. An id that is not yours returns 404.

Unsubscribe

unsubscribe.json
{
"name": "entity.news.published",
"arguments": { "entityId": "4eabfc26-3ed9-4ad3-a935-9be07ae3329a" },
"delivery": { "url": "https://example.com/aventure/news" }
}
curl -X DELETE "https://api.aventure.vc/v1/news/subscriptions" \
-H "Authorization: Bearer $AUTH_TOKEN" \
-H "Content-Type: application/json" \
-d @unsubscribe.json

It returns 204, including when the subscription is already gone.

Build the Receiver

Every request is a signed JSON POST with Standard Webhooks headers webhook-id, webhook-timestamp, and webhook-signature (space-separated v1,<signature> entries), plus X-MCP-Subscription-Id. Verify the raw body bytes with a Standard Webhooks library before you parse the JSON: the standardwebhooks package on npm for Node.js, or on PyPI for Python.

receiver.mjs
import { createServer } from "node:http";
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(process.env.AVENTURE_WEBHOOK_SECRET); // whsec_...
const seenEventIds = new Set();
createServer(async (req, res) => {
const chunks = [];
for await (const chunk of req) chunks.push(chunk);
let message;
try {
message = webhook.verify(Buffer.concat(chunks), req.headers);
} catch {
res.writeHead(401).end();
return;
}
if (message.type === "verification") {
res.writeHead(200, { "Content-Type": "application/json" });
res.end(JSON.stringify({ challenge: message.challenge }));
return;
}
if (message.name === "entity.news.published" && !seenEventIds.has(message.eventId)) {
seenEventIds.add(message.eventId);
console.log(message.data.title, message.cursor);
}
res.writeHead(204).end();
}).listen(8080);

Answer Verification

A new endpoint first receives {"type": "verification", "challenge": "<nonce>"}. Answer 2xx with exactly {"challenge": "<nonce>"}. An endpoint that echoes the whole body fails verification.

Read Events

{
"eventId": "...",
"name": "entity.news.published",
"timestamp": "...",
"data": { "title": "...", "newsUrlOriginal": "..." },
"cursor": "..."
}

data is the public news article, the same object GET /v1/news returns. eventId equals the webhook-id header and stays the same across retries, so drop any event whose eventId you have already handled. Store the latest cursor to resume from it after a gap in your own processing.

Two other message types can arrive:

MessageMeaning
{"type": "gap", "cursor": "..."}Events before this cursor were skipped. An event larger than 256 KB is replaced by a gap
{"type": "terminated", "error": {"code": -32012, "message": "Forbidden", "data": {"reason": "Access revoked"}}}The subscription ended because the account’s access ended

Acknowledge and Retry

Your answerResult
Any 2xxAcknowledged
410 or 413That event is refused for good; the cursor moves past it
Anything else, or no answerRetried with exponential backoff and jitter, starting at 1 minute and doubling up to 6 hours

After 12 consecutive failures delivery pauses until you refresh the subscription with PUT. Answer within 10 seconds; the connection must open within 5. Events go out about once a minute, after an article has been linked for about 2 minutes.

Subscribe From an MCP Client

The aVenture MCP server supports MCP Events with webhook delivery: events/list, events/subscribe, and events/unsubscribe. The events/subscribe and events/unsubscribe params are the same JSON as the PUT and DELETE bodies above, and delivery follows the same receiver contract.

JSON-RPC errorMeaning
-32015Your endpoint failed verification or delivery; data.reason says why
-32013Subscription limit reached
-32602Invalid params
-32012Forbidden
-32011Not found