cross-post API
Programmatically manage connected accounts, upload media, and create or schedule posts. Crosspost provides the dashboard and public API, with Bundle handling social publishing. New to Crosspost? Create an account and generate an API key in the dashboard.
Base URL
https://cross-post.app/api/v1
All requests and responses use JSON. Set the Content-Type: application/json header on all requests with a body.
Supported Platforms
Crosspost supports Instagram, YouTube, TikTok, X (Twitter), Threads, Pinterest, Facebook, and LinkedIn. Availability and media requirements vary by platform.
Codex & Claude agents #
Let your agent produce a video with its own rendering tools, then upload and schedule the finished file through Crosspost. Video generation is not a Crosspost service. Provider writes require an active paid plan, current trial, or complimentary access; creating an API key does not grant these.
- Open Settings > Developer API, select Agent video scheduling, and create a named key. Save the key once in your agent's private environment as
CROSSPOST_API_KEY, not in a prompt or source file. - Download the Python 3 agent connector. It needs no additional packages. Use its command-line interface or run it with
mcpfor a local stdio MCP connection. - Ask your agent to list accounts and check usage first. Select explicit local account IDs, your finished video, caption, and a future date/time with a UTC offset. Accounts in a single post must have the same
account_group_id. - Keep one
job_idper scheduling intent. Reuse that job to resume interrupted work; its private receipt retains the file fingerprint, upload token and exact post idempotency request. A returned post ID means accepted/scheduled, not necessarily published.
The connector exposes list_accounts, get_usage, schedule_video, and get_post. It does not generate media, connect/disconnect accounts, cancel posts, or publish immediately. This first connector supports the confirmed-presigned-upload workflow used by live Bundle. MP4 and MOV are supported; WebM is not. Pinterest scheduling needs board options and is not supported by this connector yet. Known reconnect_required accounts are rejected before uploading. unknown authorization status is not proof of a working token.
Connect Codex
Add this to your Codex configuration, replacing the path with the absolute location where you downloaded the connector. Forward the API key through the local environment; keep it out of the configuration example. Official Codex MCP setup.
[mcp_servers.crosspost]
command = "python3"
args = ["/absolute/path/crosspost-agent.py", "mcp"]
env_vars = ["CROSSPOST_API_KEY"]
tool_timeout_sec = 900
default_tools_approval_mode = "writes"Connect Claude Code
With CROSSPOST_API_KEY available in the launching environment, add a local server using the absolute connector path. Official Claude Code MCP setup. Cloud chat connectors cannot run this local stdio process.
claude mcp add --transport stdio crosspost -- python3 /absolute/path/crosspost-agent.py mcpAgent prompt
“List my Crosspost accounts and confirm my upload allowance. Create a finished MP4 using my approved assets. Show me the accounts, caption and schedule, then schedule it for the time I approve. Preserve the job ID if anything is interrupted, and report the returned post ID.”
Command-line use
python3 crosspost-agent.py accounts
python3 crosspost-agent.py usage
python3 crosspost-agent.py schedule-video \
--file /absolute/path/video.mp4 --accounts 4 5 \
--at 2026-10-02T13:00:00+05:30 \
--caption "Your approved caption" --job-id campaign-video-001
python3 crosspost-agent.py status 123Replace the example account IDs, timestamp and post ID with your own. macOS and Linux are supported. Keep the same job ID and arguments when resuming. Receipts are private under ~/.local/state/crosspost-agent, or your owned mode-0700 CROSSPOST_STATE_DIR. Do not delete that state to bypass an uncertain result.
REST discovery and safety
GET /api/v1/capabilities accepts usage:read (or legacy analytics:read) and returns current media limits, allowed MIME types, confirmation requirements and same-group rules. GET /api/v1/accounts returns local account_group_id and authorization warnings. GET /api/v1/usage includes provider_write_allowed.
Use the five preset scopes only: accounts:read, usage:read, media:write, posts:write, posts:read. Existing scope permissions cover all owned accounts; this is not an account-restricted key, and posts:write also permits deletion through the REST API. The connector itself offers scheduling, not deletion.
Authentication #
Authenticate API requests using a Bearer token. Generate API keys from your cross-post dashboard under Settings > Developer API.
Include your API key in the Authorization header of every request:
curl https://cross-post.app/api/v1/accounts \
-H "Authorization: Bearer cp_live_xxxxx..."
API Key Format
API keys use the prefix cp_live_ followed by 40 hex characters. Keep your keys secret and never expose them in client-side code.
Scopes
Each API key can be assigned granular scopes that limit what it can access:
| Scope | Description |
|---|---|
accounts:read | List connected social accounts |
accounts:write | Connect and disconnect social accounts |
posts:read | List and retrieve posts |
posts:write | Create and delete posts |
media:write | Upload media files |
analytics:read | View analytics and usage data |
usage:read | View subscription usage and limits |
webhooks:read | List registered webhooks |
webhooks:write | Create and delete webhooks |
403 Forbidden error with code insufficient_scope.Rate Limiting #
API requests are rate-limited to 60 requests per minute per newly created API key. There is no dashboard rate-limit editor. Rate limit information is included in authenticated responses via headers:
| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests per window |
X-RateLimit-Remaining | Requests remaining in current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds to wait before retrying (only present on 429 responses) |
When you exceed the limit, the API returns 429 Too Many Requests. Use the Retry-After header to determine how long to wait before retrying.
{
"error": {
"code": "rate_limit_exceeded",
"message": "Rate limit exceeded. Try again in 42 seconds",
"status": 429
}
}
Error Handling #
The API uses standard HTTP status codes and returns errors in a consistent format:
{
"error": {
"code": "validation_error",
"message": "The caption field is required.",
"status": 400
}
}
Error Codes
| Code | Status | Description |
|---|---|---|
auth_required | 401 | Missing or invalid Authorization header |
invalid_api_key | 401 | API key is invalid or has been revoked |
insufficient_scope | 403 | API key lacks the required scope for this endpoint |
validation_error | 400 | Request body or parameters failed validation |
unsupported_fanout | 422 | The selected provider does not support destinations spanning multiple provider profiles or teams |
not_found | 404 | The requested resource does not exist |
method_not_allowed | 405 | HTTP method not supported for this endpoint |
rate_limit_exceeded | 429 | Too many requests — slow down |
limit_exceeded | 403 | Account usage limit reached for your plan |
late_api_error | 500 | An error occurred communicating with the upstream publishing service |
internal_error | 500 | An unexpected error occurred on our end |
Pagination #
List endpoints return paginated results. Control pagination with query parameters:
| Parameter | Default | Max | Description |
|---|---|---|---|
page | 1 | — | Page number |
per_page | 20 | 100 | Items per page |
Paginated responses include a pagination object:
{
"data": [...],
"pagination": {
"page": 1,
"per_page": 20,
"total": 47,
"has_more": true
}
}
List Accounts #
Returns all social accounts connected to your cross-post account.
Each record also includes provider, local account_group_id, authorization_status, authorization_message and authorization_checked_at. Group IDs belong to your account; no upstream team identifiers are exposed. Use one group per Bundle post.
Response
{
"data": [
{
"id": 12,
"platform": "instagram",
"username": "mycreatoraccount",
"display_name": "My Creator Account",
"avatar_url": "https://...",
"connected_at": "2026-01-15 10:30:00",
"is_active": true
}
]
}
Example
curl https://cross-post.app/api/v1/accounts \
-H "Authorization: Bearer cp_live_xxxxx..."
Connect Account #
Generates an OAuth connect URL for the specified platform. Redirect the user to this URL to authorize the connection.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
platform | string | required | One of: instagram, youtube, tiktok, twitter, threads, pinterest, facebook, linkedin |
Response
{
"data": {
"auth_url": "https://accounts.google.com/o/oauth2/v2/auth?...",
"platform": "instagram"
}
}
Example
curl -X POST https://cross-post.app/api/v1/accounts/connect \
-H "Authorization: Bearer cp_live_xxxxx..." \
-H "Content-Type: application/json" \
-d '{"platform": "instagram"}'
Disconnect Account #
Disconnects a social account. Any scheduled posts targeting this account will fail.
Path Parameters
| Parameter | Description |
|---|---|
id | The account ID (integer, e.g., 12) |
Response
{
"data": {
"success": true
}
}
Example
curl -X DELETE https://cross-post.app/api/v1/accounts/12 \
-H "Authorization: Bearer cp_live_xxxxx..."
List Posts #
Returns a paginated list of your posts. Supports filtering by status and platform.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
status | string | — | Filter by status: draft, scheduled, publishing, published, partial, failed |
platform | string | — | Filter by target platform (e.g., instagram) |
page | integer | 1 | Page number |
per_page | integer | 20 | Results per page (max 100) |
Response
{
"data": [
{
"id": 42,
"caption": "Check out our new feature!",
"status": "published",
"publish_mode": "now",
"scheduled_at": null,
"published_at": "2026-03-15 14:22:00",
"created_at": "2026-03-15 14:20:00",
"media": [
{
"url": "https://cdn.cross-post.app/media/abc123.jpg",
"filename": "abc123.jpg",
"mime_type": "image/jpeg",
"size": 284720,
"sort_order": 0
}
],
"destinations": [
{
"account_id": 12,
"platform": "instagram",
"status": "published",
"published_at": "2026-03-15 14:22:00",
"error_message": null
}
]
}
],
"pagination": {
"page": 1,
"per_page": 20,
"total": 47,
"has_more": true
}
}
Get Post #
Retrieves a single post by ID, including its media and per-destination status.
Response
Returns the same post object shape as the list endpoint, wrapped in a data key (not an array).
Example
curl https://cross-post.app/api/v1/posts/42 \
-H "Authorization: Bearer cp_live_xxxxx..."
Create Post #
Creates a new post and publishes it to the specified social accounts. Upload media first using the media upload endpoint. When that response sets requires_confirm, complete the confirmation step before creating the post.
account_group_id metadata from the accounts endpoint; never send upstream team IDs. Upload separately for each group and pass the finalized media_id with its confirmed URL. URL-only media imports and cross-group fan-out are not supported.Request Body
| Field | Type | Required | Description |
|---|---|---|---|
caption | string | optional | Post caption / text content |
media_urls | array | optional | HTTPS URL strings, or objects with url, type, and confirmed media_id. When Bundle is selected, URL strings and objects without a finalized media_id are rejected; use the upload and confirmation endpoints first. |
destination_account_ids | integer[] | required | Account IDs to publish to (integers) |
publish_mode | string | optional | now, schedule, or draft. Defaults to schedule. |
scheduled_at | string | if schedule | Datetime string (e.g., 2026-03-20T15:00:00). Must be in the future. |
timezone | string | optional | IANA timezone (e.g., America/New_York). Defaults to UTC. |
Idempotency
X-Idempotency-Key header is required. Save one key and the exact JSON bytes per intended post before sending it. Reuse both on uncertain retries; a changed body is a conflict. Keys bound to a local post are retained and cannot be reused even after 24 hours. Keep completed job receipts indefinitely and read the existing post rather than starting another creation.Response
{
"data": {
"id": 57,
"caption": "Check out our new feature!",
"status": "scheduled",
"publish_mode": "schedule",
"scheduled_at": "2026-03-20T15:00:00",
"published_at": null,
"created_at": "2026-03-17 10:00:00",
"media": [
{
"url": "https://cdn.cross-post.app/media/abc123.jpg",
"filename": "abc123.jpg",
"mime_type": null,
"size": null,
"sort_order": 0
}
],
"destinations": [
{
"account_id": 12,
"platform": "instagram",
"status": "pending",
"published_at": null,
"error_message": null
},
{
"account_id": 15,
"platform": "twitter",
"status": "pending",
"published_at": null,
"error_message": null
}
]
}
}
Example
curl -X POST https://cross-post.app/api/v1/posts \
-H "Authorization: Bearer cp_live_xxxxx..." \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: saved-unique-post-intent" \
-d '{
"caption": "Check out our new feature!",
"destination_account_ids": [12, 15],
"publish_mode": "schedule",
"scheduled_at": "2026-10-02T15:00:00Z",
"timezone": "UTC"
}'
Delete Post #
Deletes a post. Only posts with status draft, scheduled, or failed can be deleted. Published or publishing posts cannot be deleted.
Response
{
"data": {
"success": true
}
}
Example
curl -X DELETE https://cross-post.app/api/v1/posts/42 \
-H "Authorization: Bearer cp_live_xxxxx..."
Upload Media #
Returns a presigned upload URL. Upload the file with PUT. If requires_confirm is true, finalize it through /api/v1/media/confirm; otherwise the returned public_url is ready immediately.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
filename | string | required | Original filename with extension |
content_type | string | required | MIME type (e.g., image/jpeg, video/mp4) |
size_bytes | integer | required | Exact file size in bytes. Files above the account plan limit are rejected before an upload URL is issued. |
destination_account_id | integer | conditional | Local connected-account ID from GET /api/v1/accounts. Required when your accounts span multiple account groups; optional with a single group. Never supply an upstream team ID. |
Multiple Instagram accounts: add "destination_account_id": 123 to upload requests, using the account you intend to publish to. Uploads stay bound to that account group. A post may select several platforms within the same group, but cannot publish across groups; upload separately for another Instagram account. Omitting the selector when several groups exist returns 409 conflict.
Response
{
"data": {
"upload_url": "https://storage.cross-post.app/presigned/...",
"public_url": null,
"media_id": "opaque_upload_token",
"requires_confirm": true
}
}
Upload Flow
Step 2:
PUT the raw file to upload_url with the matching Content-Type.Step 3: If
requires_confirm is true, call the confirmation endpoint with media_id and the same byte size. A 202 response means the provider is still processing; retry with the newly returned media_id. Create the post only after ready is true and public_url is non-null.Example
# Step 1: Get presigned URL
curl -X POST https://cross-post.app/api/v1/media/upload \
-H "Authorization: Bearer cp_live_xxxxx..." \
-H "Content-Type: application/json" \
-d '{"filename": "photo.jpg", "content_type": "image/jpeg", "size_bytes": 245760}'
# Step 2: Upload the file
curl -X PUT "https://storage.cross-post.app/presigned/..." \
-H "Content-Type: image/jpeg" \
--data-binary @photo.jpg
# Step 3 when requires_confirm is true
curl -X POST https://cross-post.app/api/v1/media/confirm \
-H "Authorization: Bearer cp_live_xxxxx..." \
-H "Content-Type: application/json" \
-d '{"media_id": "opaque_upload_token", "size_bytes": 245760}'
Confirm Media #
Finalizes a provider upload. Call this only when the upload response sets requires_confirm to true.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
media_id | string | required | Opaque token returned by the latest upload or confirmation response |
size_bytes | integer | optional | Exact uploaded file size; when provided it must match initialization |
Ready Response — 200
{
"data": {
"media_id": "confirmed_media_token",
"public_url": "https://cdn.example/media/photo.jpg",
"ready": true,
"requires_confirm": false
}
}
Still Processing — 202
Retry this endpoint with the newly returned media_id. The token is designed for safe retries and does not repeat provider finalization.
{
"data": {
"media_id": "pending_finalized_token",
"public_url": null,
"ready": false,
"requires_confirm": true
}
}
Get Analytics #
Returns analytics for your account, including post counts, platform breakdown, and daily posting trends.
Query Parameters
| Parameter | Type | Default | Description |
|---|---|---|---|
period | string | 30d | Time period: 7d, 30d, 90d, or all |
Response
{
"data": {
"period": "30d",
"total_posts": 142,
"published_count": 128,
"failed_count": 3,
"scheduled_count": 11,
"by_status": {
"published": 128,
"failed": 3,
"scheduled": 11
},
"platforms": {
"instagram": 45,
"twitter": 38,
"youtube": 22,
"tiktok": 18,
"threads": 12,
"bluesky": 7
},
"success_rate": 97.7,
"posts_over_time": [
{ "date": "2026-03-01", "count": 5 },
{ "date": "2026-03-02", "count": 3 }
]
}
}
Get Usage #
Returns current usage counts, subscription tier limits and provider_write_allowed. A limit value of -1 means unlimited. Use usage:read; analytics:read is accepted for older keys.
Response
{
"data": {
"subscription_tier": "pro",
"subscription_expires_at": "2026-04-15 00:00:00",
"posts": {
"used": 42,
"limit": 500,
"remaining": 458
},
"scheduled_posts": {
"used": 8,
"limit": 100,
"remaining": 92
},
"social_accounts": {
"used": 4,
"limit": 15,
"remaining": 11
},
"max_media_size_mb": 100
}
}
List Webhooks #
Returns all active webhook endpoints for your account. The secret is not included in list responses.
Response
{
"data": [
{
"id": 3,
"url": "https://example.com/webhooks/crosspost",
"events": ["post_published", "post_failed"],
"created_at": "2026-02-10 08:00:00"
}
]
}
Create Webhook #
Registers a new webhook endpoint. You will receive a secret in the response — store it securely for signature verification. Maximum 5 webhooks per user.
Request Body
| Field | Type | Required | Description |
|---|---|---|---|
url | string | required | HTTPS URL to receive webhook payloads |
events | string[] | required | Events to subscribe to (see events list) |
Response
{
"data": {
"id": 3,
"url": "https://example.com/webhooks/crosspost",
"events": ["post_published", "post_failed"],
"secret": "a1b2c3d4e5f6...",
"created_at": "2026-03-17 10:00:00"
}
}
secret is only returned once at creation time. Store it securely — you will need it to verify webhook signatures.Example
curl -X POST https://cross-post.app/api/v1/webhooks \
-H "Authorization: Bearer cp_live_xxxxx..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/crosspost",
"events": ["post_published", "post_failed"]
}'
Delete Webhook #
Deletes a registered webhook. No further events will be delivered to this endpoint.
Response
{
"success": true
}
Example
curl -X DELETE https://cross-post.app/api/v1/webhooks/3 \
-H "Authorization: Bearer cp_live_xxxxx..."
Webhook Events & Signatures #
Available Events
| Event | Description |
|---|---|
post_created | A new post was created (any publish mode) |
post_published | A post was successfully published to all destinations |
post_failed | A post failed to publish on one or more destinations |
account_connected | A new social account was connected |
account_disconnected | A social account was disconnected |
Delivery
Webhooks are delivered as POST requests to your registered URL with a JSON payload. Respond with a 2xx status code within 10 seconds to acknowledge receipt.
Retry Policy
Failed deliveries are retried up to 5 times with exponential backoff: 30 seconds, 2 minutes, 8 minutes, 32 minutes, 2 hours.
Payload Format
The webhook body is a JSON object with event, data, and timestamp fields. The data contents vary by event type.
{
"event": "post_created",
"data": {
"post_id": 57,
"status": "scheduled",
"publish_mode": "schedule",
"platform_count": 2
},
"timestamp": "2026-03-17T14:30:00+00:00"
}
{
"event": "account_disconnected",
"data": {
"account_id": 12,
"platform": "instagram"
},
"timestamp": "2026-03-17T14:30:00+00:00"
}
Signature Verification
Every webhook delivery includes these headers:
| Header | Description |
|---|---|
X-Webhook-Signature | HMAC-SHA256 signature of the raw request body: sha256={hex} |
X-Webhook-Event | The event type (e.g., post_created) |
User-Agent | cross-post-webhooks/1.0 |
Content-Type | application/json |
The signature is computed over the raw JSON body using your webhook secret as the HMAC key. Always verify signatures to ensure payloads are authentic.
The header format is: sha256={hex-encoded HMAC}
Node.js
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express middleware example
app.post('/webhooks/crosspost', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const isValid = verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET);
if (!isValid) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body);
console.log('Received event:', event.event);
res.sendStatus(200);
});
Python
import hmac
import hashlib
def verify_webhook_signature(payload: bytes, signature: str, secret: str) -> bool:
expected = 'sha256=' + hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, expected)
# Flask example
from flask import Flask, request, abort
app = Flask(__name__)
@app.route('/webhooks/crosspost', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature', '')
if not verify_webhook_signature(request.data, signature, WEBHOOK_SECRET):
abort(401)
event = request.get_json()
print(f"Received event: {event['event']}")
return '', 200
Code Examples #
Scheduling a video with an agent
Use the agent connector for the complete file-upload, confirmation and durable scheduling workflow. It preserves progress across interruptions. The basic REST sequence is:
GET /accounts,GET /usageandGET /capabilities: choose owned account IDs in one group and verify current entitlement, byte limits and supported media types.POST /media/upload: send filename, MIME type, exactsize_bytesand one selecteddestination_account_id. Save the response privately before continuing. Do not repeat an uncertain initialization.- PUT the actual bytes to
upload_urlusingupload_headers. Never send the Bearer key to storage. An expired URL is not permission to create another upload blindly. POST /media/confirm: returnmedia_idand exact size. HTTP 202 means processing. Persist each returnedmedia_idbefore another confirmation; do not create a post untilready=trueand a confirmed public URL are present.POST /posts: send confirmed media URL/token, destination IDs,publish_mode="schedule", future offset-aware timestamp andtimezone="UTC". Save the exact serialized JSON andX-Idempotency-Keybefore sending.- Save the returned post ID and use
GET /posts/{id}to observe status. A network error orreconciliation_requiredis not permission to create another post.
Minimal text-only REST scheduling
Prepare a private post.json containing your caption, selected account IDs, schedule mode, future ISO timestamp with offset, and UTC timezone. The same-group rule still applies. Store a unique key for this job as CROSSPOST_POST_KEY; keep that value and post.json unchanged for retries. These commands assume the API key is already in your private environment.
curl --fail-with-body https://cross-post.app/api/v1/posts \
-H "Authorization: Bearer $CROSSPOST_API_KEY" \
-H "Content-Type: application/json" \
-H "X-Idempotency-Key: $CROSSPOST_POST_KEY" \
--data-binary @post.jsonListing Accounts
curl https://cross-post.app/api/v1/accounts \
-H "Authorization: Bearer cp_live_xxxxx..."
const res = await fetch('https://cross-post.app/api/v1/accounts', {
headers: { 'Authorization': `Bearer ${API_KEY}` },
});
const { data: accounts } = await res.json();
accounts.forEach(account => {
console.log(`${account.platform}: ${account.username} (${account.is_active ? 'active' : 'inactive'})`);
});
import requests
res = requests.get('https://cross-post.app/api/v1/accounts',
headers={'Authorization': f'Bearer {API_KEY}'}
)
accounts = res.json()['data']
for account in accounts:
status = 'active' if account['is_active'] else 'inactive'
print(f"{account['platform']}: {account['username']} ({status})")
Setting Up a Webhook
curl -X POST https://cross-post.app/api/v1/webhooks \
-H "Authorization: Bearer cp_live_xxxxx..." \
-H "Content-Type: application/json" \
-d '{
"url": "https://example.com/webhooks/crosspost",
"events": [
"post_published",
"post_failed",
"account_connected"
]
}'
const res = await fetch('https://cross-post.app/api/v1/webhooks', {
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
url: 'https://example.com/webhooks/crosspost',
events: ['post_published', 'post_failed', 'account_connected'],
}),
});
const { data: webhook } = await res.json();
console.log('Webhook ID:', webhook.id);
console.log('Secret (store this!):', webhook.secret);
import requests
res = requests.post('https://cross-post.app/api/v1/webhooks',
headers={'Authorization': f'Bearer {API_KEY}'},
json={
'url': 'https://example.com/webhooks/crosspost',
'events': ['post_published', 'post_failed', 'account_connected'],
}
)
webhook = res.json()['data']
print(f"Webhook ID: {webhook['id']}")
print(f"Secret (store this!): {webhook['secret']}")
Need help? Open Support chat or email kolagames07@gmail.com.