Webhooks

Webhooks

Create and manage TokPortal webhook endpoints for emitted bundle and item status events.

Webhooks

Webhooks let TokPortal notify your backend when supported bundle and item lifecycle events happen.

TokPortal currently emits these public webhook events:

  • webhook.test
  • bundle.created
  • bundle.published
  • account.configured
  • account.published
  • account.in_review
  • account.pending_corrections
  • account.finalized
  • video.configured
  • video.in_review
  • video.published
  • video.pending_corrections
  • video.finalized

Event catalog

GET /webhooks/events

Use the event catalog to discover every supported event type, its delivery availability, example payload, payload schema, signature scheme, and required headers before creating an endpoint.

curl https://app.tokportal.com/api/ext/webhooks/events

The response includes events, envelope, delivery, and signature. Events marked emitted are actively delivered today.

Signature model

When a webhook is delivered, TokPortal sends:

HeaderDescription
TokPortal-Event-IdStable event ID for idempotency.
TokPortal-Event-TypeEvent type, for example bundle.published.
TokPortal-SignatureHMAC SHA-256 signature in the format t=<timestamp>,v1=<hex>.

Create endpoints with HTTPS URLs. Store the returned signing_secret immediately; it is only returned on creation.

Create an endpoint

POST /webhooks

Node

const response = await fetch("https://app.tokportal.com/api/ext/webhooks", {
  method: "POST",
  headers: {
    "X-API-Key": process.env.TOKPORTAL_API_KEY!,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({
    url: "https://example.com/tokportal/webhook",
    events: ["bundle.created", "account.in_review", "video.finalized"],
    description: "Production ingestion",
  }),
});

if (!response.ok) {
  throw new Error(await response.text());
}

const endpoint = await response.json();
console.log(endpoint.data.signing_secret);

Python



response = requests.post(
    "https://app.tokportal.com/api/ext/webhooks",
    headers={
        "X-API-Key": os.environ["TOKPORTAL_API_KEY"],
        "Content-Type": "application/json",
    },
    json={
        "url": "https://example.com/tokportal/webhook",
        "events": ["bundle.created", "account.in_review", "video.finalized"],
        "description": "Production ingestion",
    },
    timeout=30,
)
response.raise_for_status()

endpoint = response.json()
print(endpoint["data"]["signing_secret"])

curl

curl -X POST https://app.tokportal.com/api/ext/webhooks \
  -H "X-API-Key: sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/tokportal/webhook",
    "events": ["bundle.created", "account.in_review", "video.finalized"],
    "description": "Production ingestion"
  }'

Response

{
  "data": {
    "id": "0b88db42-1111-4222-9333-e681165e6f4a",
    "url": "https://example.com/tokportal/webhook",
    "description": "Production ingestion",
    "events": ["bundle.created", "account.in_review", "video.finalized"],
    "enabled": true,
    "created_at": "2026-05-25T18:00:00Z",
    "updated_at": "2026-05-25T18:00:00Z",
    "last_delivery_at": null,
    "last_delivery_status": null,
    "failure_count": 0,
    "signing_secret": "whsec_..."
  }
}

List endpoints

GET /webhooks
curl "https://app.tokportal.com/api/ext/webhooks?event=bundle.published&enabled=true" \
  -H "X-API-Key: sk_your_key_here"

Update an endpoint

PATCH /webhooks/{id}
curl -X PATCH https://app.tokportal.com/api/ext/webhooks/0b88db42-1111-4222-9333-e681165e6f4a \
  -H "X-API-Key: sk_your_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "enabled": false
  }'

Delete an endpoint

DELETE /webhooks/{id}
curl -X DELETE https://app.tokportal.com/api/ext/webhooks/0b88db42-1111-4222-9333-e681165e6f4a \
  -H "X-API-Key: sk_your_key_here"

Send a test event

POST /webhooks/{id}/test

This sends a signed webhook.test event to the endpoint and stores the delivery result.

Node

const response = await fetch(
  "https://app.tokportal.com/api/ext/webhooks/0b88db42-1111-4222-9333-e681165e6f4a/test",
  {
    method: "POST",
    headers: {
      "X-API-Key": process.env.TOKPORTAL_API_KEY!,
    },
  },
);

if (!response.ok) {
  throw new Error(await response.text());
}

const delivery = await response.json();
console.log(delivery.data.success, delivery.data.status_code);

Python



response = requests.post(
    "https://app.tokportal.com/api/ext/webhooks/0b88db42-1111-4222-9333-e681165e6f4a/test",
    headers={"X-API-Key": os.environ["TOKPORTAL_API_KEY"]},
    timeout=30,
)
response.raise_for_status()

delivery = response.json()
print(delivery["data"]["success"], delivery["data"]["status_code"])

curl

curl -X POST https://app.tokportal.com/api/ext/webhooks/0b88db42-1111-4222-9333-e681165e6f4a/test \
  -H "X-API-Key: sk_your_key_here"

List delivery attempts

GET /webhooks/{id}/deliveries
curl "https://app.tokportal.com/api/ext/webhooks/0b88db42-1111-4222-9333-e681165e6f4a/deliveries?success=false" \
  -H "X-API-Key: sk_your_key_here"

Each delivery includes the event ID, event type, HTTP status code, success flag, duration, error message, payload, and creation timestamp.

Retry a delivery

POST /webhooks/{id}/deliveries/{delivery_id}/retry

Retry a stored delivery when your endpoint was temporarily unavailable. TokPortal reuses the stored webhook payload, preserves the original event ID, signs the request again with a fresh TokPortal-Signature, and records the retry as a new delivery attempt.

curl -X POST https://app.tokportal.com/api/ext/webhooks/0b88db42-1111-4222-9333-e681165e6f4a/deliveries/7a1f3e5d-2222-4333-9444-abc123abc123/retry \
  -H "X-API-Key: sk_your_key_here"

Receivers should treat TokPortal-Event-Id as the idempotency key. A retry can have a different delivery row ID while preserving the same event ID.

Verify signatures

The public Node SDK includes verifyWebhookSignature. The manual HMAC examples below are framework-agnostic and production-safe. In both cases, pass the exact raw request body bytes/string received by your HTTP framework, before JSON parsing or re-serialization.

Node



function verifyTokPortalSignature(
  rawBody: Buffer | string,
  signatureHeader: string | null | undefined,
  secret: string,
  toleranceSeconds = 300,
) {
  if (!signatureHeader) {
    return false;
  }

  const parts = Object.fromEntries(
    signatureHeader.split(",").map((part) => {
      const [key, ...value] = part.trim().split("=");
      return [key, value.join("=")];
    }),
  );
  const timestamp = parts.t;
  const expectedHex = parts.v1;

  if (!timestamp || !expectedHex) {
    return false;
  }

  const timestampSeconds = Number(timestamp);
  if (
    !Number.isFinite(timestampSeconds) ||
    Math.abs(Date.now() / 1000 - timestampSeconds) > toleranceSeconds
  ) {
    return false;
  }

  const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody, "utf8");
  const signedPayload = Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), body]);
  const digest = createHmac("sha256", secret).update(signedPayload).digest();
  const expected = Buffer.from(expectedHex, "hex");

  return expected.length === digest.length && timingSafeEqual(expected, digest);
}

const valid = verifyTokPortalSignature(
  rawBody,
  request.headers["tokportal-signature"],
  process.env.TOKPORTAL_WEBHOOK_SECRET!,
);

Python



def verify_tokportal_signature(
    raw_body: bytes,
    signature_header: str | None,
    secret: str,
    tolerance_seconds: int = 300,
) -> bool:
    if not signature_header:
        return False

    parts = dict(
        item.strip().split("=", 1)
        for item in signature_header.split(",")
        if "=" in item
    )
    timestamp = parts.get("t")
    expected = parts.get("v1")

    if not timestamp or not expected:
        return False

    try:
        timestamp_seconds = int(timestamp)
    except ValueError:
        return False

    if abs(time.time() - timestamp_seconds) > tolerance_seconds:
        return False

    signed_payload = timestamp.encode() + b"." + raw_body
    digest = hmac.new(
        secret.encode(),
        signed_payload,
        hashlib.sha256,
    ).hexdigest()

    return hmac.compare_digest(digest, expected)

valid = verify_tokportal_signature(
    raw_body,
    request.headers["TokPortal-Signature"],
    os.environ["TOKPORTAL_WEBHOOK_SECRET"],
)

Go

package main


	"crypto/hmac"
	"crypto/sha256"
	"encoding/hex"
	"io"
	"net/http"
	"os"
	"strconv"
	"strings"
	"time"
)

func verifyTokPortalSignature(rawBody []byte, signatureHeader string, secret string, tolerance time.Duration) bool {
	parts := map[string]string{}
	for _, part := range strings.Split(signatureHeader, ",") {
		keyValue := strings.SplitN(strings.TrimSpace(part), "=", 2)
		if len(keyValue) == 2 {
			parts[keyValue[0]] = keyValue[1]
		}
	}

	timestamp := parts["t"]
	expectedHex := parts["v1"]
	if timestamp == "" || expectedHex == "" {
		return false
	}

	timestampSeconds, err := strconv.ParseInt(timestamp, 10, 64)
	if err != nil {
		return false
	}

	signedAt := time.Unix(timestampSeconds, 0)
	if time.Since(signedAt) > tolerance || time.Until(signedAt) > tolerance {
		return false
	}

	mac := hmac.New(sha256.New, []byte(secret))
	mac.Write([]byte(timestamp + "."))
	mac.Write(rawBody)

	expected, err := hex.DecodeString(expectedHex)
	if err != nil {
		return false
	}

	return hmac.Equal(mac.Sum(nil), expected)
}

func handler(w http.ResponseWriter, r *http.Request) {
	rawBody, err := io.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "invalid body", http.StatusBadRequest)
		return
	}

	valid := verifyTokPortalSignature(
		rawBody,
		r.Header.Get("TokPortal-Signature"),
		os.Getenv("TOKPORTAL_WEBHOOK_SECRET"),
		5*time.Minute,
	)
	if !valid {
		http.Error(w, "invalid signature", http.StatusUnauthorized)
		return
	}
}

The signature is computed over:

<timestamp>.<raw_request_body>

using the endpoint signing_secret. Compare against the v1 value in TokPortal-Signature with a constant-time comparison.

Event payload

{
  "id": "evt_...",
  "type": "webhook.test",
  "api_version": "2026-05-25",
  "created_at": "2026-05-25T18:00:00Z",
  "data": {
    "webhook_endpoint_id": "0b88db42-1111-4222-9333-e681165e6f4a",
    "message": "TokPortal webhook test event"
  }
}