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
  • bundle.cancelled
  • bundle.archived
  • account.configured
  • account.published
  • account.in_review
  • account.pending_corrections
  • account.finalized
  • account.remade
  • account.banned
  • account.ban_appeal.submitted
  • account.ban_appeal.resolved
  • account.ban_resolution.decided
  • video.configured
  • video.in_review
  • video.published
  • video.pending_corrections
  • video.finalized
  • warming.session_started
  • warming.term_verified
  • warming.session_completed
  • subscription.renewed
  • subscription.lapsed
  • subscription.cancelled
  • subscription.reactivated
  • subscription.ended
  • account.revealed
  • credits.restored

TIP: Detect bans and cancellations without polling If you automate posting, subscribe at minimum to account.banned, bundle.cancelled, and account.remade. They are the push signals that a managed account was banned, that a bundle will no longer accept publishes (409 BUNDLE_INVALID_STATUS with current_status: "cancelled"), and that an account was rebuilt. See the dedicated sections below.

TIP: Show your users the full ban lifecycle To surface ban status in your own UI, subscribe to the four ban lifecycle events — account.ban_appeal.submitted (a platform appeal was filed; the account is unavailable but not yet banned), account.ban_appeal.resolved (the platform answered), account.banned (the ban is validated), and account.ban_resolution.decided (staff decided refund / remake / no_remake). The same lifecycle is pollable via GET /account-bans.

TIP: Stop tasks when TokPortal Coverage lapses or ends API and MCP automations should subscribe to subscription.lapsed, subscription.cancelled, subscription.ended, and account.revealed. Stop creating tasks for that saved_account_id immediately after a lapse, cancellation, terminal end, or an account.revealed payload with management_ended: true. Resume only after subscription.reactivated. account.revealed is emitted once after the first successful credential or verification-code reveal; a prior-policy reveal reports management_ended: false. Ordinary metadata reads never emit it. A terminal subscription.ended or detached account can never return to TokPortal Coverage. subscription.renewed confirms a paid 30-day period, bundle.archived reports manager-workspace cleanup without changing Coverage, and credits.restored reports an eligible ban restoration.

Event catalog

GET /webhooks/events

TokPortal Coverage events

Coverage belongs to a saved account, not a bundle. These events always include subscription_id and saved_account_id.

EventMeaningRequired automation behavior
subscription.renewedThe automatic 25-credit debit succeeded and a new 30-day period began.Continue normally.
subscription.lapsedThe renewal debit failed because the workspace lacked credits.Stop every task for the account and show the reactivation requirement.
subscription.cancelledThe client manually paused coverage.Stop every task. Do not create or resume bundles until reactivation.
subscription.reactivatedCoverage resumed atomically. credits_charged is 0 inside an already paid or included period; otherwise it equals the exact unpaid periods paid.Resume work. Read resumed_tasks and refresh schedules before posting.
subscription.endedStaff completed the eligible refund or credit-restoration resolution for a confirmed banned account, permanently ending TokPortal Coverage as ended_ban.Stop all work permanently. Do not retry renewal or offer reactivation. Read status, ended_at, and ended_reason.

The exact reactivation price is always available from GET /accounts/{id}/managed-subscription. It is 0 credits while the original paid or included period is still current. After a boundary, it is the sum of the exact recorded period rates plus any newly projected periods at period_rate_credits. Reactivation keeps the existing billing anchor and never creates an overlapping period. Clients must submit the quoted amount, period end, and lock version; a changed quote is rejected without a debit. On subscription.reactivated, reactivation_was_free is true and credit_transaction_id is null when no credits were charged. On subscription.cancelled, reactivation_may_be_free_until identifies the current period end; the legacy reactivation_requires_current_period field is retained as deprecated and is always false.

bundle.archived is separate. It only removes a bundle from the manager's active workspace after 60 days without a newly scheduled or posted video. Its payload contains account_coverage_changed: false. The bundle reopens automatically when new work is attached, and Coverage continues throughout.

credits.restored reports an eligible confirmed-ban restoration. Its amount covers the initial account setup, warming and unused video-slot credits, never the Coverage period or used or published work. The payload expiration is 60 days after restoration.

The five Coverage lifecycle events in the table, plus account.revealed, credits.restored, bundle cancellations produced by a confirmed-ban resolution, and bundle.archived, use a transactional outbox. The state or financial transition and its stable event ID commit together. Delivery happens asynchronously after the API response and is retried with backoff. Delivery is at least once, so store TokPortal-Event-Id with a unique constraint and make your handler idempotent. The same business event keeps the same event ID on every automatic or manual retry.

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.

Do not send Idempotency-Key when creating an endpoint. The successful response contains a one-time signing secret, so TokPortal rejects the header before execution with 400 IDEMPOTENCY_KEY_NOT_ALLOWED_FOR_SENSITIVE_RESPONSE. If the response is lost after an uncertain request, list endpoints and reconcile by URL and description before trying again. If the endpoint exists but its one-time secret was not received, delete that specific endpoint and create a replacement so you can store a new secret. Never log or send the secret to support.

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"
  }
}

Bundle, account, and video event payloads

Every event wraps its data in the envelope above. The data object shape is shared per category. These schemas (and live example payloads) are also served programmatically from GET /webhooks/events.

bundle.* events (bundle.created, bundle.publishedbundle.cancelled has its own payload, see below)

FieldTypeDescription
bundle_idstring (UUID)The bundle. Stable across remakes.
external_refstring | nullYour reference set on the bundle.
statusstringBundle status.
platformstringtiktok, instagram, or youtube.
bundle_typestringaccount_only, account_and_videos, or videos_only.

account.* events (account.configured, account.in_review, account.published, account.pending_corrections, account.finalized)

FieldTypeDescription
account_idstring (UUID)The bundle's account listing id — see the note below. Stable across remakes.
saved_account_idstring (UUID) | nullThe saved account id (the real created account; what GET /accounts/{id} uses). null until the account exists — it's populated from account.in_review onward (configured/published send null). Changes on every remake.
bundle_idstring (UUID)The owning bundle. Stable across remakes.
usernamestring | nullCurrent requested handle.
platformstringtiktok, instagram, or youtube.
statusstringNew account status.
previous_statusstring | nullStatus before this transition.

video.* events (video.configured, video.in_review, video.published, video.pending_corrections, video.finalized)

FieldTypeDescription
video_idstring (UUID)The video listing.
bundle_idstring (UUID)The owning bundle.
positioninteger1-based slot position.
statusstringNew video status.
previous_statusstring | nullStatus before this transition.
platform_urlstring | nullPosted URL once available.

CAUTION: Two different "account" ids — listing vs saved TokPortal has two distinct concepts, both informally called "account":

  • Account listing — the account slot/spec on the bundle (what you order and configure). Its id is account_id in account.* events. Stable across remakes (the listing is rebuilt in place).
  • Saved account — the real created social account (credentials, the thing GET /accounts/{id} returns). Its id is saved_account_id (in account.* events from in_review onward, and on GET /bundles/{id}). Replaced on every remake — the old one 404s; account.remade.old_account_id is that old saved-account id.

How to get the new saved-account id after a remake: wait for account.in_review or account.finalized on the bundle (they carry saved_account_id), or pull GET /bundles/{bundle_id} once the account exists (it returns saved_account_id). Before the account is created, saved_account_id is null.

Bottom line: key your records on bundle_id (+ your external_ref) — it's in every event and never changes. Read saved_account_id from in_review/finalized (or GET /bundles/{id}) when you need to act on the real account.

The account.remade event

When an account is banned or lost, TokPortal remakes it: the bundle is rebuilt in place under the same bundle_id, the previous saved account is removed, and the manager re-creates the account (usually under a slightly different handle).

Subscribe to account.remade to track remakes without polling. This is the recommended way for resellers to keep downstream clients in sync when an account is replaced.

{
  "id": "evt_...",
  "type": "account.remade",
  "api_version": "2026-05-25",
  "created_at": "2026-06-09T00:36:24Z",
  "data": {
    "bundle_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
    "external_ref": "partner-order-123",
    "platform": "tiktok",
    "old_username": "launchprofile",
    "new_username": "launchprofile_",
    "old_account_id": "0d1e2f3a-4b5c-6789-8abc-def012345678",
    "reason": "account banned/not found",
    "mode": "republish",
    "remade_count": 1,
    "remade_at": "2026-06-09T00:36:24Z"
  }
}
FieldTypeDescription
bundle_idstring (UUID)The bundle that was remade. Stable across remakes — use it (or your external_ref) as the correlation anchor.
external_refstring | nullYour own reference set when the bundle was created. Also stable across remakes.
platformstringtiktok, instagram, or youtube.
old_usernamestring | nullHandle before the remake.
new_usernamestring | nullTarget handle the account is being rebuilt under.
old_account_idstring (UUID) | nullThe saved account that was removed. Retire this idGET /accounts/{old_account_id} will return 404 after a remake.
reasonstring | nullWhy the account was remade (e.g. account banned/not found).
modestringrepublish or publish_and_assign.
remade_countintegerTotal number of times this bundle has been remade.
remade_atstring (ISO 8601) | nullWhen the remake happened.

NOTE: note There is no new_account_id in this payload: the rebuilt saved account is created later by the manager. It surfaces through subsequent account.* events (e.g. account.published, account.finalized) carried on the same bundle_id, or via GET /bundles/{bundle_id} once the account exists. Always key your records on bundle_id / external_ref, not on the saved account id (which changes on every remake).

The account.banned event

When a managed account is banned by the platform and the ban is validated (the manager reported it with no appeal available, the platform refused the ban appeal, or staff confirmed the ban), TokPortal marks the account banned and emits account.banned to the bundle owner.

A validated ban also cancels every active bundle and order on the account (a bundle.cancelled event fires alongside for each), stops the Account Owning Fee if one was active, and surfaces the ban on GET /accounts/{id} (banned, ban.reason, ban.banned_at, ban.screenshot_url).

{
  "id": "evt_...",
  "type": "account.banned",
  "api_version": "2026-05-25",
  "created_at": "2026-07-06T19:13:43Z",
  "data": {
    "bundle_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
    "saved_account_id": "7c9e0f1a-2b3c-4d5e-8f6a-1b2c3d4e5f60",
    "username": "launchprofile",
    "platform": "tiktok",
    "reason": "Community guidelines violation notice shown in-app",
    "appeal_status": "no_appeal_banned",
    "banned_at": "2026-07-06T19:13:43Z"
  }
}
FieldTypeDescription
bundle_idstring (UUID) | nullThe bundle the ban report is attached to. Stable across remakes.
saved_account_idstring (UUID) | nullThe banned saved account (still readable via GET /accounts/{id} — banned accounts stay visible).
usernamestring | nullHandle of the banned account.
platformstringtiktok, instagram, or youtube.
reasonstring | nullBan reason as reported by the manager or staff.
appeal_statusstringno_appeal_banned (no platform appeal was available) or appeal_refused (the platform refused the appeal).
banned_atstring (ISO 8601)When the ban was validated.

NOTE: note A ban does not automatically remake the account. If the account is later remade, you'll receive a separate account.remade event on the same bundle_id.

The account.ban_appeal.submitted event

When the manager reports a ban and files a platform appeal, the account enters limbo: it is unavailable while the platform decides, but it is not marked banned — no account.banned fires and its bundles stay open. This event is your first signal that something happened to the account, typically days before the appeal resolves. Show it to your users instead of leaving them without feedback.

{
  "id": "evt_...",
  "type": "account.ban_appeal.submitted",
  "api_version": "2026-05-25",
  "created_at": "2026-07-01T08:30:12Z",
  "data": {
    "appeal_id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d",
    "bundle_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
    "saved_account_id": "7c9e0f1a-2b3c-4d5e-8f6a-1b2c3d4e5f60",
    "username": "launchprofile",
    "platform": "tiktok",
    "appeal_status": "appeal_pending",
    "submitted_at": "2026-07-01T08:30:00Z"
  }
}
FieldTypeDescription
appeal_idstring (UUID)The ban report — poll it via GET /account-bans.
bundle_idstring (UUID) | nullThe bundle the report is attached to. Stable across remakes.
saved_account_idstring (UUID) | nullThe account under appeal.
usernamestring | nullHandle of the account.
platformstringtiktok, instagram, or youtube.
appeal_statusstringAlways appeal_pending.
submitted_atstring (ISO 8601)When the ban was first reported and the appeal filed.

NOTE: note While an appeal is pending, TokPortal deliberately does not recreate the account: creating a duplicate while the platform reviews the appeal can trigger a ban-evasion strike. The next event on this appeal_id is account.ban_appeal.resolved.

The account.ban_appeal.resolved event

Once the platform answers a pending appeal, this event reports the outcome:

  • resolution: "appeal_accepted" — the account survived; nothing else changes.
  • resolution: "appeal_refused" — the ban is validated; an account.banned event fires alongside, and staff then decides the commercial outcome (see account.ban_resolution.decided).

The payload carries appeal_id, bundle_id, saved_account_id, username, platform, resolution, reason, and decided_at.

The account.ban_resolution.decided event

After a ban is confirmed (appeal_refused or no_appeal_banned), staff reviews the case in the Ban Appeals queue and decides the commercial outcome. This event announces that decision:

  • resolution: "refund" — eligible credits are restored (refund_credits carries the amount; a credits.restored event can fire alongside for TokPortal Coverage accounts).
  • resolution: "remake" — the mission is reset and relisted under the same bundle_id; an account.remade event fires alongside.
  • resolution: "no_remake" — the case is closed without compensation. reason_code: "tos_ban" means the account was banned for a direct Terms of Service violation and is not refundable.
{
  "id": "evt_...",
  "type": "account.ban_resolution.decided",
  "api_version": "2026-05-25",
  "created_at": "2026-07-04T16:45:10Z",
  "data": {
    "appeal_id": "5a6b7c8d-9e0f-4a1b-8c2d-3e4f5a6b7c8d",
    "bundle_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
    "saved_account_id": "7c9e0f1a-2b3c-4d5e-8f6a-1b2c3d4e5f60",
    "username": "launchprofile",
    "platform": "tiktok",
    "resolution": "refund",
    "reason_code": "eligible_refund",
    "refund_credits": 42,
    "resolved_at": "2026-07-04T16:45:00Z"
  }
}
FieldTypeDescription
appeal_idstring (UUID)The ban report the decision belongs to.
bundle_idstring (UUID) | nullThe bundle the report is attached to.
saved_account_idstring (UUID) | nullThe banned account. Null when a staff reset deleted the account row — key on bundle_id.
usernamestring | nullHandle of the banned account.
platformstringtiktok, instagram, or youtube.
resolutionstringrefund, remake, or no_remake.
reason_codestring | nullMachine-readable reason, e.g. eligible_refund, eligible_credit_restoration, replacement_approved, tos_ban, tos_violation, content_related, custom.
refund_creditsintegerCredits restored by the decision (0 unless resolution is refund).
resolved_atstring (ISO 8601)When staff made the decision.

The bundle.cancelled event

Emitted when a bundle is cancelled and will never accept publishes again — most commonly because the bundle's account was banned (account_banned: true), or after a ban-check was accepted with the cancel & refund resolution. After this event, POST .../publish calls on the bundle return 409 BUNDLE_INVALID_STATUS with current_status: "cancelled" and a cancelled_reason.

{
  "id": "evt_...",
  "type": "bundle.cancelled",
  "api_version": "2026-05-25",
  "created_at": "2026-07-06T19:13:43Z",
  "data": {
    "bundle_id": "9f3a7b2e-1c4d-4e8f-a5b6-7d9e0f1a2b3c",
    "external_ref": "partner-order-123",
    "platform": "tiktok",
    "reason": "Account banned: Community guidelines violation",
    "cancelled_at": "2026-07-06T19:13:43Z",
    "account_banned": true,
    "saved_account_id": "7c9e0f1a-2b3c-4d5e-8f6a-1b2c3d4e5f60",
    "username": "launchprofile"
  }
}
FieldTypeDescription
bundle_idstring (UUID)The cancelled bundle.
external_refstring | nullYour reference set on the bundle.
platformstringtiktok, instagram, or youtube.
reasonstringWhy the bundle was cancelled (also on GET /bundles/{id} as cancelled_reason).
cancelled_atstring (ISO 8601)When the bundle was cancelled.
account_bannedbooleantrue when the cancellation was caused by the account being banned — an account.banned event fires alongside.
saved_account_idstring (UUID) | nullThe account the bundle was attached to, when relevant.
usernamestring | nullHandle of that account.

Warming events

warming.session_started, warming.term_verified, and warming.session_completed track Advanced Warming sessions. Their payload schemas and example payloads are served from GET /webhooks/events; see the Advanced Warming page for the session lifecycle.