Skip to content

Configure Portal webhooks

View as Markdown

Webhooks let your application react to events in real time. Instead of polling the API for new packages, you register a URL and MASV sends an HTTP request whenever a specified event occurs. In this tutorial, you configure a webhook that fires when a package is uploaded to your portal.

  • Create a webhook subscribed to portal package events
  • Attach the webhook to a portal
  • Parse the webhook payload and extract package information
  • Secure your webhook endpoint
  • A MASV API key. Store it in the API_KEY environment variable.
  • A portal configured in your Team.
  • An HTTPS endpoint that can receive POST requests (use webhook.site for testing).
  • Your Team ID and portal ID stored in TEAM_ID and PORTAL_ID environment variables.

Register a webhook endpoint and subscribe it to package.finalized events:

Terminal window
# Store your API key in an environment variable — never hardcode it.
# See /api/api-keys/ for key management best practices.
curl -X POST "https://api.massive.app/v1/teams/$TEAM_ID/custom_webhooks" \
-H "X-API-KEY: $API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"active\": true,
\"method\": \"POST\",
\"name\": \"Portal Upload Notifications\",
\"url\": \"https://your-server.example.com/webhooks/masv\",
\"events\": [\"package.finalized\"],
\"headers\": {\"X-Webhook-Secret\": \"$WEBHOOK_SECRET\"}
}"

The headers field lets you include a shared secret that your endpoint can validate on every incoming request. This is the primary mechanism for verifying that requests originate from MASV.

A successful response returns 201 Created with the webhook object:

{
"id": "<WEBHOOK_ID>",
"active": true,
"method": "POST",
"name": "Portal Upload Notifications",
"url": "https://your-server.example.com/webhooks/masv",
"events": ["package.finalized"],
"headers": {"X-Webhook-Secret": "<YOUR_SHARED_SECRET>"}
}

Webhooks must be attached to a portal to receive events from it. Update your portal to include the webhook:

Terminal window
curl -X PUT "https://api.massive.app/v1/portals/$PORTAL_ID" \
-H "X-API-KEY: $API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "...",
"subdomain": "...",
"active": true,
"custom_webhooks": [{"id": "$WEBHOOK_ID"}]
}'

Upload a file to your portal to trigger the webhook. Use the MASV web interface, the Agent CLI, or the API.

After the upload finalizes, MASV sends a POST request to your registered URL. The payload looks like this:

{
"event_id": "<EVENT_ID>",
"event_time": "2026-05-04T12:05:30.000Z",
"event_type": "package.finalized",
"body_extras": {},
"object": {
"type": "package",
"id": "<PACKAGE_ID>",
"name": "project-assets",
"portal_id": "<PORTAL_ID>",
"sender": "sender@example.com",
"size": 524288000,
"state": "finalized",
"total_files": 3,
"created_at": "2026-05-04T12:00:00.000Z",
"updated_at": "2026-05-04T12:05:30.000Z"
},
"custom_webhook_id": "<WEBHOOK_ID>"
}

Confirm your endpoint received the request and returned a 200 status code. MASV retries failed deliveries up to 5 times with incremental backoff.

MASV does not include a cryptographic signature on webhook payloads. Use the custom headers field (set in Step 1) to verify requests:

import os
import hmac
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["MASV_WEBHOOK_SECRET"]
@app.route("/webhooks/masv", methods=["POST"])
def handle_webhook():
# Verify the shared secret header
received_secret = request.headers.get("X-Webhook-Secret", "")
if not hmac.compare_digest(received_secret, WEBHOOK_SECRET):
abort(401)
event = request.get_json()
if event["event_type"] == "package.finalized":
package_id = event["object"]["id"]
sender = event["object"]["sender"]
size = event["object"]["size"]
print(f"New package {package_id} from {sender} ({size} bytes)")
# Trigger your downstream workflow here
return "", 200

Return a 200 response promptly. If your processing takes time, acknowledge the webhook immediately and handle the work asynchronously.