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
kycorcard_sendbecomesactive - Flagging accounts that move to
restrictedordisabled - Updating transaction records when transfers complete or fail
You must first register a webhook endpoint in the Frame dashboard before you can receive events. See the main webhooks guide for setup, signature verification, and retry behavior.
frameOS webhook events
The following events are sent for frameOS resources. Subscribe to the events relevant to your integration.
Account events
| Event | Description |
|---|---|
| account.activated | The account has completed all required verifications and is fully active. |
| account.restricted | The account has been restricted — some capabilities may be limited until requirements are resolved. |
| account.disabled | The account has been disabled. |
Capability events
| Event | Description |
|---|---|
| capability.activated | A capability has moved to active status. All requirements have been verified. |
| capability.disabled | A capability has been disabled. Check disabled_reason for details. |
Transfer events
| Event | Description |
|---|---|
| transfer.created | A new transfer has been created and is pending. |
| transfer.completed | A transfer has completed successfully. |
| transfer.failed | A 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.
{
"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"
}
}
}
{
"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.
- Create an account with the capabilities you need
- Start an onboarding session so the user can complete verification
- Listen for
capability.activatedwebhooks to know when each capability is ready - Enable features in your platform based on which capabilities are now active
Webhooks can arrive out of order. If you track capability status locally, guard against backward transitions — for example, don't overwrite an active status with pending from a late-arriving event.
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");
}
);
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.
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-Idheader 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
livemodefield in the payload tells you whether the event is from a live or test environment.