Webhooks

frameOS sends webhook events when account and capability statuses change. Use webhooks to get notified when a capability becomes active, an account is restricted, or a transfer completes — so your platform can react in real time without polling the API.

Common use cases include:

  • Unlocking features when a capability like kyc or card_send becomes active
  • Flagging accounts that move to restricted or disabled
  • Updating transaction records when transfers complete or fail

frameOS webhook events

The following events are sent for frameOS resources. Subscribe to the events relevant to your integration.

Account events

EventDescription
account.activatedThe account has completed all required verifications and is fully active.
account.restrictedThe account has been restricted — some capabilities may be limited until requirements are resolved.
account.disabledThe account has been disabled.

Capability events

EventDescription
capability.activatedA capability has moved to active status. All requirements have been verified.
capability.disabledA capability has been disabled. Check disabled_reason for details.

Transfer events

EventDescription
transfer.createdA new transfer has been created and is pending.
transfer.completedA transfer has completed successfully.
transfer.failedA transfer has failed.

Event payload structure

All frameOS webhook events follow the standard Frame event object format. The data field contains the resource that triggered the event.

Every webhook delivery includes X-Frame-Event, X-Frame-Signature, and X-Frame-Webhook-Id headers. See delivery headers for details.

The data field contains the resource that changed. The examples show the payload structure for account and capability events.

ACCOUNT ACTIVATED EVENT
{
  "id": "evt-8a3b1c4d-e5f6-7890-abcd-ef1234567890",
  "type": "account.activated",
  "object": "event",
  "created": 1729161970,
  "livemode": false,
  "data": {
    "object": {
      "id": "99c6b0da-2570-42a7-838a-5eaa318b07df",
      "object": "account",
      "type": "individual",
      "status": "active"
    }
  }
}
CAPABILITY ACTIVATED EVENT
{
  "id": "evt-7b2a9c3d-f4e5-6789-bcda-ef9876543210",
  "type": "capability.activated",
  "object": "event",
  "created": 1729162080,
  "livemode": false,
  "data": {
    "object": {
      "id": "a1b2c3d4-e5f6-7890-abcd-ef1234567891",
      "object": "capability",
      "name": "kyc",
      "account_id": "99c6b0da-2570-42a7-838a-5eaa318b07df",
      "status": "active",
      "disabled_reason": null,
      "currently_due": [],
      "errors": []
    }
  }
}

Listening for capability activation

A common pattern is to wait for a capability.activated event before enabling a feature in your platform. For example, you might require kyc to be active before allowing an account to receive payouts.

Implementation steps

  1. Create an account with the capabilities you need
  2. Start an onboarding session so the user can complete verification
  3. Listen for capability.activated webhooks to know when each capability is ready
  4. Enable features in your platform based on which capabilities are now active
NODE.JS: HANDLE CAPABILITY WEBHOOK
const crypto = require("crypto");

// Use express.raw() to get the raw body for signature verification.
// express.json() parses the body, which changes the bytes and breaks HMAC.
app.post("/webhooks/frame",
  express.raw({ type: "application/json" }),
  (req, res) => {
    const rawBody = req.body; // Buffer
    const signature = req.headers["x-frame-signature"];
    const event = req.headers["x-frame-event"];

    // Verify signature
    const expected = "sha256=" +
      crypto.createHmac("sha256", process.env.FRAME_WEBHOOK_SECRET)
        .update(rawBody)
        .digest("hex");

    if (!crypto.timingSafeEqual(
      Buffer.from(expected),
      Buffer.from(signature)
    )) {
      return res.status(401).send("Invalid signature");
    }

    const payload = JSON.parse(rawBody);

    if (event === "capability.activated") {
      const capability = payload.data.object;
      console.log(
        `Capability ${capability.name} is now active ` +
        `for account ${capability.account_id}`
      );
      // Enable the feature in your platform
    }

    res.status(200).send("OK");
  }
);

Verifying signatures

Always verify the X-Frame-Signature header before processing a webhook. Frame signs payloads using HMAC-SHA256 with the secret from your webhook endpoint configuration.

Key rules:

  • Use the raw request body for verification, not parsed/re-serialized JSON
  • Use a constant-time comparison function to prevent timing attacks
  • Store your webhook secret in an environment variable, never in code

See the main webhooks guide for full details and test vectors.

RUBY: VERIFY AND DISPATCH
class WebhooksController < ActionController::Base
  skip_before_action :verify_authenticity_token

  def create
    raw_body  = request.raw_post
    signature = request.headers["X-Frame-Signature"]
    event     = request.headers["X-Frame-Event"]

    # Verify HMAC-SHA256 signature
    expected = "sha256=" + OpenSSL::HMAC.hexdigest(
      "sha256",
      ENV["FRAME_WEBHOOK_SECRET"],
      raw_body
    )

    unless Rack::Utils.secure_compare(expected, signature)
      head :unauthorized and return
    end

    payload = JSON.parse(raw_body)

    case event
    when "capability.activated"
      handle_capability_activated(payload)
    when "account.activated"
      handle_account_activated(payload)
    end

    head :ok
  end

  private

  def handle_capability_activated(payload)
    capability = payload.dig("data", "object")
    account = Account.find_by(
      frame_account_id: capability["account_id"]
    )
    # Enable features based on capability name
  end
end

Best practices

  • Subscribe only to events you need. This reduces noise and processing overhead.
  • Return 2xx quickly. Process events asynchronously if your handler does heavy work. Frame expects a response within 5 seconds.
  • Use the webhook ID for idempotency. The X-Frame-Webhook-Id header uniquely identifies each delivery. Store it to avoid processing duplicate events.
  • Handle out-of-order delivery. Account and capability status can transition through multiple states. Use status ranking to prevent backward transitions.
  • Log all events. Persist webhook events (including unverified ones) for debugging and auditability.
  • Test with sandbox mode. Use your sandbox API keys to trigger events during development. The livemode field in the payload tells you whether the event is from a live or test environment.