VayuDocumentationIntegrationsAPI ReferenceChange Log
SupportGet started now
API Documentation

Webhooks

Receive real-time event notifications from Vayu

Webhooks let Vayu push notifications to your server when billing events occur. You subscribe per event type with a callback URL, and Vayu sends a signed POST request whenever that event fires.

Subscribing

Endpoint: POST /webhook

{
  "callbackUrl": "https://your-server.com/webhooks/vayu",
  "eventType": "Overage"
}
curl -X POST "https://connect.withvayu.com/webhook" \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $ACCESS_TOKEN" \
  -H "x-api-key: $VAYU_CLIENT_ID" \
  -d '{
    "callbackUrl": "https://your-server.com/webhooks/vayu",
    "eventType": "Overage"
  }'
import { Vayu } from 'vayu-ts';

const vayu = new Vayu(process.env.VAYU_API_KEY);

await vayu.webhooks.subscribe({
  callbackUrl: 'https://your-server.com/webhooks/vayu',
  eventType: 'Overage',
});
from vayu_sdk import Vayu
import os

vayu = Vayu(api_key=os.environ["VAYU_API_KEY"])

vayu.webhooks.subscribe(
    callback_url="https://your-server.com/webhooks/vayu",
    event_type="Overage",
)
import vayu "github.com/vayucode/vayu-sdks/go"

v := vayu.NewVayu(os.Getenv("VAYU_API_KEY"))

err := v.Webhooks.Subscribe(vayu.WebhookSubscribeRequest{
    CallbackUrl: "https://your-server.com/webhooks/vayu",
    EventType:   "Overage",
})

Available event types

Event typeDescription
AnonymousCustomerEvent received for an unrecognized customer alias — auto-created as anonymous
OverageCustomer exceeds their provisioned amount for a product
UpcomingRenewalA customer contract is approaching its renewal date
ContractExpiredA customer contract has reached its end date
InvoiceApprovedAn invoice has been approved and is ready to send
CustomerPortalLinkSentA customer portal link was sent (or failed to send)
NewCustomerWithoutContractA new customer was created but has no contract
MonthEndReportThe month-end report has been generated
UnchargedEventsEvents exist that have not been counted against any meter
TierCrossedCustomer usage has crossed into a new pricing tier
CommitmentCrossedCustomer has crossed their committed usage threshold
CommitmentUsageCrossedCustomer commitment usage has crossed a configured threshold percentage
CurrencyCommitmentUsageCrossedCustomer monetary commitment usage has crossed a configured threshold percentage
FinalTierExceededCustomer usage has exceeded the final pricing tier
InvoicePaymentStatusChangedAn invoice payment status has changed (e.g. paid, overdue)
InvoiceSendFailedAn invoice failed to send to the customer
CreditPoolDepletedA customer credit pool balance has reached zero
CreditPoolThresholdCrossedA customer credit pool has crossed a configured usage threshold
CreditPoolIncreasedCredits were added to a customer credit pool
PaymentFailedA payment attempt for an invoice failed
CustomerCreatedA new customer was created
ContractCreatedA new contract was created
PingTestA test event to verify your endpoint is reachable

The event type is delivered in the X-Webhook-Type request header — the JSON body contains only the event's data fields, not a type property. Threshold-based events (CommitmentUsageCrossed, CurrencyCommitmentUsageCrossed, CreditPoolThresholdCrossed) accept optional threshold and recurringThreshold values when subscribing.

Webhook payloads

The type field is not part of the body. Read the event type from the X-Webhook-Type header and parse the body below accordingly. All date fields are ISO 8601 date-time strings.

AnonymousCustomer

{
  "id": "cust_123456789",
  "name": "Acme Corp",
  "externalId": "ext_987654321",
  "aliases": ["ext_987654321"]
}

Overage

{
  "date": "2026-06-20T12:00:00Z",
  "overageAmount": 200,
  "invoiceId": "inv_123456789",
  "customerId": "cust_123456789",
  "customerName": "Acme Corp",
  "currency": "USD"
}

UpcomingRenewal

{
  "date": "2026-06-20T12:00:00Z",
  "contractEndDate": "2026-07-01T00:00:00Z",
  "contractName": "Acme Annual Plan",
  "contractId": "contract_abc123",
  "customerName": "Acme Corp",
  "isTrial": false
}

ContractExpired

{
  "date": "2026-06-20T12:00:00Z",
  "contractEndDate": "2026-06-20T00:00:00Z",
  "contractName": "Acme Annual Plan",
  "contractId": "contract_abc123",
  "customerName": "Acme Corp",
  "isTrial": false
}

InvoiceApproved

{
  "date": "2026-06-20T12:00:00Z",
  "invoiceId": "inv_123456789"
}

CustomerPortalLinkSent

{
  "customerId": "cust_123456789",
  "isSuccessful": true,
  "email": "billing@acme.com",
  "date": "2026-06-20T12:00:00Z"
}

When sending fails, isSuccessful is false and an errorMessage field is included.

NewCustomerWithoutContract

{
  "date": "2026-06-20T12:00:00Z",
  "customerId": "cust_123456789",
  "customerName": "Acme Corp"
}

MonthEndReport

{
  "date": "2026-06-20T12:00:00Z",
  "reportId": "report_123456789"
}

UnchargedEvents

{
  "date": "2026-06-20T12:00:00Z",
  "reportId": "report_123456789"
}

TierCrossed

{
  "date": "2026-06-20T12:00:00Z",
  "customerId": "cust_123456789",
  "contractId": "contract_abc123",
  "customerName": "Acme Corp",
  "contractName": "Acme Annual Plan",
  "variantId": "variant_123"
}

CommitmentCrossed

The payload is the affected product's consumption snapshot.

{
  "productId": "prod_123456789",
  "productName": "API Calls",
  "provisionedAmount": 50000,
  "consumedAmount": 50001,
  "usagePercentage": 100,
  "hasAccess": true,
  "remainingAmount": 0,
  "exceededAmount": 1
}

CommitmentUsageCrossed

{
  "date": "2026-06-20T12:00:00Z",
  "customerId": "cust_123456789",
  "threshold": 80,
  "pctUsage": 80,
  "productConsumption": {
    "productId": "prod_123456789",
    "productName": "API Calls",
    "provisionedAmount": 50000,
    "consumedAmount": 40000,
    "usagePercentage": 80,
    "hasAccess": true,
    "remainingAmount": 10000
  }
}

CurrencyCommitmentUsageCrossed

{
  "date": "2026-06-20T12:00:00Z",
  "productId": "prod_123456789",
  "productName": "API Calls",
  "customerId": "cust_123456789",
  "threshold": 80,
  "pctUsage": 80,
  "currency": "USD",
  "committedAmount": 10000,
  "consumedAmount": 8000,
  "cycleStart": "2026-06-01T00:00:00Z"
}

FinalTierExceeded

{
  "date": "2026-06-20T12:00:00Z",
  "customerId": "cust_123456789",
  "contractId": "contract_abc123",
  "customerName": "Acme Corp",
  "contractName": "Acme Annual Plan",
  "productId": "prod_123456789",
  "productName": "API Calls",
  "accumulatedUnitsValue": 100500
}

InvoicePaymentStatusChanged

billingStatus is one of None, Paid, Rejected, PendingPayment, Overdue.

{
  "invoiceId": "inv_123456789",
  "billingStatus": "Paid",
  "amountDue": 0,
  "amountPaid": 1500,
  "total": 1500,
  "dueDate": "2026-06-15T00:00:00Z",
  "paidAt": "2026-06-14T09:30:00Z",
  "invoicePdfUrl": "https://connect.withvayu.com/invoices/inv_123456789.pdf"
}

InvoiceSendFailed

{
  "invoiceId": "inv_123456789",
  "invoiceNumber": "INV-2026-0042",
  "date": "2026-06-20T12:00:00Z"
}

CreditPoolDepleted

creditKind is either Monetary or Unit.

{
  "date": "2026-06-20T12:00:00Z",
  "customerId": "cust_123456789",
  "creditPoolId": "pool_123456789",
  "creditTypeId": "credit_123456789",
  "creditKind": "Monetary",
  "balance": 0
}

CreditPoolThresholdCrossed

{
  "date": "2026-06-20T12:00:00Z",
  "customerId": "cust_123456789",
  "creditPoolId": "pool_123456789",
  "creditTypeId": "credit_123456789",
  "creditKind": "Monetary",
  "threshold": 80,
  "pctUsage": 80,
  "balance": 200
}

CreditPoolIncreased

{
  "date": "2026-06-20T12:00:00Z",
  "customerId": "cust_123456789",
  "creditPoolId": "pool_123456789",
  "creditTypeId": "credit_123456789",
  "creditKind": "Monetary",
  "grantedAmount": 1000,
  "previousBalance": 200,
  "balance": 1200
}

PaymentFailed

{
  "date": "2026-06-20T12:00:00Z",
  "customerId": "cust_123456789",
  "invoiceId": "inv_123456789",
  "paymentProvider": "Stripe",
  "amount": 1500,
  "currency": "USD",
  "failureReason": "card_declined"
}

CustomerCreated

{
  "date": "2026-06-20T12:00:00Z",
  "customerId": "cust_123456789",
  "name": "Acme Corp",
  "externalId": "ext_987654321",
  "aliases": ["ext_987654321"],
  "integrationEntities": [
    {
      "integrationProvider": "Stripe",
      "integrationExternalId": "cus_abc123",
      "syncStatus": "Synced"
    }
  ]
}

ContractCreated

{
  "date": "2026-06-20T12:00:00Z",
  "contractId": "contract_abc123",
  "customerId": "cust_123456789",
  "contractName": "Acme Annual Plan",
  "status": "Active",
  "startDate": "2026-06-20T00:00:00Z",
  "endDate": "2027-06-20T00:00:00Z",
  "customer": {
    "date": "2026-06-20T12:00:00Z",
    "customerId": "cust_123456789",
    "name": "Acme Corp",
    "externalId": "ext_987654321",
    "aliases": ["ext_987654321"],
    "integrationEntities": []
  }
}

PingTest

Sent to verify your endpoint is reachable. The body carries no event-specific fields.

{}

Handling webhook events

A minimal server that receives Vayu webhooks, routes by event type, and reads the payload:

import express from 'express';
import { Vayu } from 'vayu-ts';

const vayu = new Vayu(process.env.VAYU_API_KEY);
const app = express();
app.use(express.json());

// Register the webhook (run once)
// await vayu.webhooks.subscribe({
//   callbackUrl: 'https://your-server.com/webhooks/vayu',
//   eventType: 'Overage',
// });

app.post('/webhooks/vayu', (req, res) => {
  const eventType = req.headers['x-webhook-type'];
  const payload = req.body;

  switch (eventType) {
    case 'Overage':
      console.log(`Overage of ${payload.overageAmount} ${payload.currency} for ${payload.customerName}`);
      break;

    case 'InvoiceApproved':
      console.log(`Invoice ${payload.invoiceId} approved`);
      break;

    case 'TierCrossed':
      console.log(`${payload.customerName} crossed a tier on ${payload.contractName}`);
      break;

    case 'AnonymousCustomer':
      console.log(`New anonymous customer created: ${payload.externalId}`);
      break;

    default:
      console.log(`Unhandled event: ${eventType}`);
  }

  res.sendStatus(200);
});

app.listen(3000, () => console.log('Listening on :3000'));
from flask import Flask, request
from vayu_sdk import Vayu
import os

vayu = Vayu(api_key=os.environ["VAYU_API_KEY"])
app = Flask(__name__)

# Register the webhook (run once)
# vayu.webhooks.subscribe(
#     callback_url="https://your-server.com/webhooks/vayu",
#     event_type="Overage",
# )

@app.route('/webhooks/vayu', methods=['POST'])
def handle_webhook():
    event_type = request.headers.get('X-Webhook-Type')
    payload = request.get_json()

    if event_type == 'Overage':
        print(f"Overage of {payload['overageAmount']} {payload['currency']} for {payload['customerName']}")

    elif event_type == 'InvoiceApproved':
        print(f"Invoice {payload['invoiceId']} approved")

    elif event_type == 'TierCrossed':
        print(f"{payload['customerName']} crossed a tier on {payload['contractName']}")

    elif event_type == 'AnonymousCustomer':
        print(f"New anonymous customer created: {payload['externalId']}")

    else:
        print(f"Unhandled event: {event_type}")

    return '', 200

if __name__ == '__main__':
    app.run(port=3000)
package main

import (
    "encoding/json"
    "fmt"
    "io"
    "net/http"

    vayu "github.com/vayucode/vayu-sdks/go"
)

// Register the webhook (run once)
// v := vayu.NewVayu(os.Getenv("VAYU_API_KEY"))
// v.Webhooks.Subscribe(vayu.WebhookSubscribeRequest{
//     CallbackUrl: "https://your-server.com/webhooks/vayu",
//     EventType:   "Overage",
// })

func main() {
    http.HandleFunc("/webhooks/vayu", func(w http.ResponseWriter, r *http.Request) {
        body, _ := io.ReadAll(r.Body)
        eventType := r.Header.Get("X-Webhook-Type")

        var payload map[string]interface{}
        json.Unmarshal(body, &payload)

        switch eventType {
        case "Overage":
            fmt.Printf("Overage of %v %v for %v\n",
                payload["overageAmount"], payload["currency"], payload["customerName"])

        case "InvoiceApproved":
            fmt.Printf("Invoice %v approved\n", payload["invoiceId"])

        case "TierCrossed":
            fmt.Printf("%v crossed a tier on %v\n",
                payload["customerName"], payload["contractName"])

        case "AnonymousCustomer":
            fmt.Printf("New anonymous customer created: %v\n", payload["externalId"])

        default:
            fmt.Printf("Unhandled event: %s\n", eventType)
        }

        w.WriteHeader(http.StatusOK)
    })

    fmt.Println("Listening on :3000")
    http.ListenAndServe(":3000", nil)
}

In production, always verify the webhook signature before processing the payload.

Webhook security

All Vayu webhook requests include headers for signature verification:

HeaderDescription
X-Webhook-TypeThe event type (e.g. Overage) — use this to route the payload
X-TimestampUnix timestamp (seconds) of when the request was sent
X-SignatureBase64-encoded RSA-SHA256 signature of timestamp.JSON(payload)
X-Signature-VersionSignature scheme version (currently v1)

The signed message is: ${timestamp}.${JSON.stringify(payload)}

Only HTTPS callback URLs are supported.

Verifying signatures

The TypeScript SDK provides a built-in helper. Built-in verification for the Python and Go SDKs is coming soon — in the meantime, verify manually using Vayu's public key.

import { Vayu } from 'vayu-ts';

const vayu = new Vayu(process.env.VAYU_API_KEY);

app.post('/webhooks', async (req, res) => {
  const payload = JSON.stringify(req.body);
  const timestamp = Number(req.headers['x-timestamp']);
  const signature = String(req.headers['x-signature']);

  const isValid = vayu.webhooks.verifyWebhookSignature({
    payload,
    timestamp,
    signature,
    tolerance: 300, // optional — reject if older than 5 minutes (default)
  });

  if (!isValid) {
    return res.sendStatus(401);
  }

  console.log('Received webhook:', req.headers['x-webhook-type']);
  res.sendStatus(200);
});
import json, time, base64, math
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding
from flask import Flask, request

# Load Vayu's public key (provided during onboarding)
with open("vayu-public.pem", "rb") as f:
    public_key = serialization.load_pem_public_key(f.read())

TOLERANCE = 300  # 5 minutes

app = Flask(__name__)

@app.route('/webhooks', methods=['POST'])
def webhooks():
    payload = request.get_data(as_text=True)
    timestamp = int(request.headers.get('X-Timestamp', '0'))
    signature = request.headers.get('X-Signature', '')

    # Reject stale webhooks
    if abs(time.time() - timestamp) > TOLERANCE:
        return '', 401

    # Reconstruct the signed message
    message = f"{timestamp}.{json.dumps(json.loads(payload))}"

    try:
        public_key.verify(
            base64.b64decode(signature),
            message.encode(),
            padding.PKCS1v15(),
            hashes.SHA256(),
        )
    except Exception:
        return '', 401

    print('Received webhook:', request.headers.get('X-Webhook-Type'))
    return '', 200
import (
    "crypto"
    "crypto/rsa"
    "crypto/sha256"
    "crypto/x509"
    "encoding/base64"
    "encoding/pem"
    "fmt"
    "io"
    "math"
    "net/http"
    "os"
    "strconv"
    "time"
)

// Load Vayu's public key (provided during onboarding)
var publicKey *rsa.PublicKey

func init() {
    keyData, _ := os.ReadFile("vayu-public.pem")
    block, _ := pem.Decode(keyData)
    pub, _ := x509.ParsePKIXPublicKey(block.Bytes)
    publicKey = pub.(*rsa.PublicKey)
}

const tolerance = 300 // 5 minutes

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    timestamp, _ := strconv.ParseInt(r.Header.Get("X-Timestamp"), 10, 64)
    signature := r.Header.Get("X-Signature")

    // Reject stale webhooks
    if math.Abs(float64(time.Now().Unix()-timestamp)) > tolerance {
        http.Error(w, "stale webhook", http.StatusUnauthorized)
        return
    }

    // Reconstruct the signed message
    message := fmt.Sprintf("%d.%s", timestamp, string(body))
    hash := sha256.Sum256([]byte(message))

    sigBytes, _ := base64.StdEncoding.DecodeString(signature)
    err := rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hash[:], sigBytes)
    if err != nil {
        http.Error(w, "invalid signature", http.StatusUnauthorized)
        return
    }

    fmt.Println("Received webhook:", r.Header.Get("X-Webhook-Type"))

    w.WriteHeader(http.StatusOK)
}

On this page