Developers · Event Delivery

Receive signed lifecycle events, then verify the receipt

A webhook tells you something changed. Your server still fetches the public receipt and evaluates it before any partner action.

Contract

Schema version 2026-09-18.

Production payload types: receipt.issued, receipt.revoked. Stored as partner.receipt.issued and partner.receipt.revoked.

receipt.expired, receipt.expiring, receipt.invalidated, decision.denied, and integration.health_changed require an expanded outbox CHECK. Until that DEMO-first migration is applied, skip code is event_type_not_supported. Production 067 compatibility remains receipt.issued, receipt.revoked, and TEST EVENT.

A webhook event is not authorization. Fetch GET /api/receipts/{id}/public and verify the signed receipt on your server before granting access.

Delivery is best effort. It is not guaranteed.

  • HTTPS only. HTTP is rejected.
  • No query strings, fragments, or userinfo in the URL.
  • No localhost, link-local, or private IP destinations.
  • Hostname must resolve to a public address at save time and at delivery time.
  • Signing secret is shown once. Store it in your server secret manager.
  • Webhook events are notifications. Fetch and verify the signed public receipt before granting access.
  • Delivery is best effort with bounded retries. It is not guaranteed.

Next.js webhook receiver

// app/api/abraxas/webhooks/route.ts
import { NextRequest, NextResponse } from "next/server";
import { AbraxasPartnerKit, permitProtocolAction } from "@/lib/partner/integrationKit";
import { verifyPartnerWebhookEvent } from "@/lib/partner/eventDelivery";

const kit = new AbraxasPartnerKit({
  partnerId: "your-partner-id",
  policyId: "your-policy-v1",
  environment: "sandbox",
  baseUrl: process.env.ABRAXAS_BASE_URL,
});

const seenEventIds = new Set<string>();

export async function POST(req: NextRequest) {
  const rawBody = await req.text();
  const verified = verifyPartnerWebhookEvent({
    secret: process.env.ABRAXAS_WEBHOOK_SECRET ?? "",
    timestamp: req.headers.get("x-abraxas-webhook-timestamp") ?? "",
    rawBody,
    signatureHeader: req.headers.get("x-abraxas-webhook-signature") ?? "",
    expectedPartnerId: kit.options.partnerId,
    seenEventIds,
  });

  if (!verified.ok) {
    return NextResponse.json({ accepted: false, error: verified.error }, { status: 400 });
  }
  if (verified.duplicate) {
    return NextResponse.json({ accepted: true, duplicate: true });
  }

  const receiptId = verified.payload.receipt_id;
  if (!receiptId) {
    return NextResponse.json({ accepted: true, grant: false, reason: "no_receipt" });
  }

  const result = await kit.verifyReceiptId(receiptId);
  if (!permitProtocolAction(result)) {
    return NextResponse.json({ accepted: true, grant: false, outcome: result.outcome });
  }
  return NextResponse.json({ accepted: true, grant: true, outcome: "permitted" });
}

Express webhook receiver

// express webhook receiver
import express from "express";
import { AbraxasPartnerKit, permitProtocolAction } from "@/lib/partner/integrationKit";
import { verifyPartnerWebhookEvent } from "@/lib/partner/eventDelivery";

const kit = new AbraxasPartnerKit({
  partnerId: "your-partner-id",
  policyId: "your-policy-v1",
  environment: "sandbox",
});

const seenEventIds = new Set();

app.post("/webhooks/abraxas", express.raw({ type: "application/json" }), async (req, res) => {
  const rawBody = Buffer.isBuffer(req.body) ? req.body.toString("utf8") : String(req.body ?? "");
  const verified = verifyPartnerWebhookEvent({
    secret: process.env.ABRAXAS_WEBHOOK_SECRET,
    timestamp: String(req.headers["x-abraxas-webhook-timestamp"] ?? ""),
    rawBody,
    signatureHeader: String(req.headers["x-abraxas-webhook-signature"] ?? ""),
    expectedPartnerId: kit.options.partnerId,
    seenEventIds,
  });

  if (!verified.ok) return res.status(400).json({ accepted: false, error: verified.error });
  if (verified.duplicate) return res.json({ accepted: true, duplicate: true });

  if (!verified.payload.receipt_id) return res.json({ accepted: true, grant: false });
  const result = await kit.verifyReceiptId(verified.payload.receipt_id);
  return res.json({
    accepted: true,
    grant: permitProtocolAction(result),
    outcome: result.outcome,
  });
});

Launchpad: Partner Launchpad · Kit: Integration Kit