Webhooks

Subscribe to async JECP events. HMAC-SHA256 signed. Outbox-pattern delivery with exponential backoff.

Event types (v1)

EventFires whenSubscriber
invocation.completedSuccessful chargeAgent + Provider
invocation.refundedRefund processed (90% credited)Agent + Provider
refund.requestedAgent files a refundProvider
refund.deniedProvider denies refundAgent + Provider
wallet.low_balanceBalance ≤ $0.50 after chargeAgent
provider.kyc_status_changedStripe Connect KYC updateProvider
test.syntheticManual test from POST /test endpointCaller

1. Subscribe

const sub = await jecp.subscribe({
  endpoint_url: 'https://myapp.com/jecp/webhook',
  events: ['invocation.completed', 'wallet.low_balance'],   // empty = subscribe to all
});

console.log(sub.subscription_id);  // uuid
console.log(sub.hmac_secret);      // SHOWN ONCE — save it now
The hmac_secret is shown only on creation. If you lose it, delete the subscription and create a new one.

2. Verify inbound events

Each event POST includes X-JECP-Webhook-Signature and X-JECP-Webhook-Timestamp headers. Use the SDK's verifyWebhook() helper:

import { verifyWebhook, WebhookVerificationError } from '@jecpdev/sdk';

app.post('/jecp/webhook', async (req, res) => {
  try {
    const event = await verifyWebhook({
      body: req.rawBody,
      signature: req.headers['x-jecp-webhook-signature'],
      timestamp: req.headers['x-jecp-webhook-timestamp'],
      secret: process.env.JECP_WEBHOOK_SECRET!,
    });

    switch (event.type) {
      case 'invocation.completed':
        await trackRevenue(event.data);
        break;
      case 'wallet.low_balance':
        await alertOps(event.data.balance_after);
        break;
    }
    res.status(200).send('ok');
  } catch (e) {
    if (e instanceof WebhookVerificationError) {
      res.status(401).send('invalid signature');
    } else {
      res.status(500).send('error');
    }
  }
});

Event payload structure

{
  "type": "invocation.completed",
  "id":   "evt_abc123",
  "created_at": "2026-05-09T15:00:00Z",
  "data": {
    "transaction_id": "tx-abc",
    "agent_id": "jdb_ag_...",
    "provider_id": "uuid",
    "capability_id": "uuid",
    "action": "translate",
    "amount_usdc": 0.005,
    "balance_after": 19.995,
    "provider_share_usdc": 0.00425
  }
}

Delivery guarantees

Test your endpoint

// Trigger a synthetic event to your endpoint
await jecp.testSubscription(sub.subscription_id);

// or via CLI:
// jecp webhook test <subscription_id>

Manage subscriptions

const list = await jecp.listSubscriptions();
console.log(list.subscriptions);

// PATCH (CLI: jecp webhook update)
await fetch('https://jecp.dev/v1/subscriptions/SUB_ID', {
  method: 'PATCH',
  headers: { 'X-Agent-ID': '...', 'X-API-Key': '...', 'Content-Type': 'application/json' },
  body: JSON.stringify({ status: 'paused' }),
});

Common patterns

Agent-side: auto-topup on low balance

// On wallet.low_balance event:
if (event.type === 'wallet.low_balance' && event.data.balance_after < 1) {
  const { url } = await jecp.topup(20);
  // Notify user / open URL / etc.
}

Provider-side: track revenue in real time

if (event.type === 'invocation.completed') {
  await db.revenue.insert({
    transaction_id: event.data.transaction_id,
    amount_usdc: event.data.provider_share_usdc,
    capability: event.data.action,
  });
}