Webhooks

Receive real-time notifications for events in your Outstand account with webhooks instead of polling the API.

Webhooks allow you to receive real-time notifications when events occur in your Outstand account. Instead of polling our API for updates, webhooks push data to your endpoint as soon as something happens.

Overview

When you configure a webhook endpoint, Outstand will send an HTTP POST request to your specified URL whenever a subscribed event occurs. This enables you to:

  • Get instant notifications when posts are published successfully
  • Be alerted immediately when publishing errors occur
  • Know when a social account token expires and needs re-authentication
  • React to new comments on your connected accounts' posts
  • Build automated workflows based on Outstand events
  • Integrate with your existing systems and dashboards

Setting Up Webhooks

Creating a Webhook Endpoint

  1. Navigate to Settings in the Outstand web application
  2. Click on Webhooks in the sidebar
  3. Click the Add Webhook button
  4. Fill in the webhook configuration form:
FieldDescriptionRequired
NameA friendly name to identify this webhook (e.g., "Production Notifications")Yes
Endpoint URLThe HTTPS URL where Outstand will send webhook payloadsYes
Signing SecretA secret key used to sign payloads with HMAC-SHA256No
Custom HeadersAdditional HTTP headers to include in webhook requestsNo
EventsWhich events should trigger this webhookYes
  1. Select the events you want to subscribe to
  2. Click Create to save the webhook

Managing Webhooks

From the Webhooks settings page, you can:

  • Enable/Disable a webhook using the toggle switch
  • Edit a webhook's configuration by clicking the Edit button
  • Test a webhook to verify your endpoint is working
  • Delete a webhook when it's no longer needed

Available Events

EventDescription
post.publishedTriggered when a post is successfully published to at least one social account
post.errorTriggered when a post fails to publish to all targeted social accounts
account.token_expiredTriggered when a social account's OAuth token fails to refresh
import.completedTriggered when a post import job finishes successfully
import.failedTriggered when a post import job finishes with errors or partial results
conversation.startedTriggered when a new conversation starts with a connected account, whether the contact messages first or you start it
message.receivedTriggered when a direct message is received
message.sentTriggered when a direct message is sent from a connected account
message.failedTriggered when a direct message fails to send
comment.receivedTriggered when someone comments on a connected account's post

The conversation.* and message.* events are emitted for Instagram Direct Messages, and only for accounts connected with the instagram_business_manage_messages scope explicitly requested (see Enable Direct Messages).

The comment.received event is emitted for Instagram comments and Threads replies.

Webhook Payload Format

All webhook payloads are sent as JSON with the following structure:

Headers

Content-Type: application/json
User-Agent: Outstand-Webhooks/1.0
X-Outstand-Signature: sha256=<signature> (if signing secret is configured)

Plus any custom headers you configured.

post.published Event

Sent when a post is successfully published to at least one social account.

{
  "event": "post.published",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "postId": "9dyJS",
    "orgId": "kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe",
    "socialAccounts": [
      {
        "accountId": "a1B2c3",
        "network": "threads",
        "username": "myaccount",
        "platformPostId": "12345678901234567",
        "platformPostUrl": "https://www.threads.net/@myaccount/post/DAbCdEfGhIj"
      },
      {
        "accountId": "d4E5f6",
        "network": "linkedin",
        "username": "John Doe",
        "platformPostId": "urn:li:share:7654321",
        "platformPostUrl": "https://www.linkedin.com/feed/update/urn:li:share:7654321"
      },
      {
        "accountId": "g7H8i9",
        "network": "instagram",
        "username": "myinsta",
        "platformPostId": "17851234567890123",
        "platformPostUrl": "https://www.instagram.com/p/DAbCdEfGhIj/"
      }
    ]
  }
}

platformPostUrl is the public URL of the post on the social network.

post.error Event

Sent when a post fails to publish to all targeted social accounts.

{
  "event": "post.error",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "postId": "9dyJS",
    "orgId": "kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe",
    "socialAccounts": [
      {
        "accountId": "a1B2c3",
        "network": "threads",
        "username": "myaccount",
        "error": "Rate limit exceeded. Please try again later."
      },
      {
        "accountId": "g7H8i9",
        "network": "instagram",
        "username": "myinsta",
        "error": "Access token has expired"
      }
    ]
  }
}

account.token_expired Event

Sent when a social account's OAuth token fails to refresh. This typically means the user needs to re-authenticate the account.

{
  "event": "account.token_expired",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "orgId": "kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe",
    "accountId": 42,
    "network": "instagram",
    "username": "myinsta",
    "error": "Failed to refresh Instagram token: The access token is invalid"
  }
}

import.completed Event

Sent when a post import job finishes with all posts successfully imported.

{
  "event": "import.completed",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "import_id": "9dyJS",
    "status": "completed",
    "imported": 42,
    "skipped": 0,
    "failed": 0
  }
}

import.failed Event

Sent when a post import job finishes with errors or only partially succeeds. The status field can be partial (some posts imported, some failed) or failed (nothing imported). The error field is only present when a specific error message is available.

{
  "event": "import.failed",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "import_id": "9dyJS",
    "status": "partial",
    "imported": 30,
    "skipped": 2,
    "failed": 10,
    "error": "Some posts could not be fetched from the platform"
  }
}

conversation.started Event

Sent the first time a contact messages one of your connected accounts, or when you start a conversation with them. It is delivered once per conversation, alongside the event for its first message: message.received, or message.sent when you started it.

{
  "event": "conversation.started",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "conversationId": "9dyJS",
    "orgId": "kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe",
    "network": "instagram",
    "participantId": "17841400000000000"
  }
}

message.received Event

Sent when a contact sends a direct message to one of your connected accounts. When the contact sent it by tapping a quick reply, data also carries interaction: { "type": "quick_reply", "id": "..." } with the id you gave that button.

{
  "event": "message.received",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "conversationId": "9dyJS",
    "messageId": "k2Lm8",
    "orgId": "kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe",
    "network": "instagram",
    "content": "Hi, is this still in stock?",
    "senderId": "17841400000000000",
    "sentAt": "2024-12-29T10:29:58.000Z",
    "sender": {
      "isFollower": true,
      "isFollowedByAccount": false,
      "followerCount": 25,
      "isVerified": false
    }
  }
}

sender is what Instagram reveals about the contact, looked up when the message arrives, so it reflects a follow that happened since their last message:

FieldDescription
isFollowerThe contact follows your connected account
isFollowedByAccountYour connected account follows the contact
followerCountThe contact's follower count
isVerifiedThe contact has a verified Instagram account

sender is null when the lookup fails, and a field inside it is null when Instagram does not return it. Treat null as unknown, never as "not a follower".

message.sent Event

Sent when a direct message goes out from one of your connected accounts. This covers messages sent through the API (source: "api") and messages the account owner sends from the Instagram app (source: "native_app"). App-sent messages are relayed but not stored, so their event has messageId: null and carries the message content; see Message lifecycle and webhooks.

{
  "event": "message.sent",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "conversationId": "9dyJS",
    "messageId": "p7Qx3",
    "orgId": "kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe",
    "network": "instagram",
    "platformMessageId": "aWdfZAG1faXRlbToxOk...",
    "source": "api"
  }
}

message.failed Event

Sent when a message submitted through the API could not be delivered. The error field carries the reason returned by the platform.

{
  "event": "message.failed",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "conversationId": "9dyJS",
    "messageId": "p7Qx3",
    "orgId": "kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe",
    "network": "instagram",
    "error": "Instagram send failed: 403"
  }
}

comment.received Event

Sent when someone comments on, or replies to a comment on, a post owned by one of your connected accounts. This covers posts published outside Outstand too. Comments written by the connected account itself, including replies sent through the API, do not trigger it.

{
  "event": "comment.received",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "orgId": "kR7mQ2xLpN4vT8wZ3bC6yH9sJ1dF5gAe",
    "network": "instagram",
    "accountId": "x8Kp2",
    "postId": "a1B2c",
    "platformPostId": "17912345678901234",
    "commentId": "17865799348089039",
    "parentCommentId": null,
    "authorId": "17841400000000000",
    "authorUsername": "jane.doe",
    "text": "Love this! Is it available in blue?",
    "author": null
  }
}
FieldDescription
accountIdThe connected social account that owns the post, as returned by GET /v1/social-accounts
postIdThe Outstand post the comment is on, or null when the post was not published through Outstand
platformPostIdThe post's ID on the network. On Threads, the first post of the thread the reply is in
commentIdThe comment's ID on the network. When postId is set, pass it as parent_comment_id to publish a reply under it. To answer the commenter in a direct message instead (Comment-to-DM), start a conversation with it as instagram.comment_id, authorId as instagram.author_id and accountId as social_account_id
parentCommentIdThe comment being replied to, or null for a top-level comment
authorId, authorUsernameThe commenter on the network. authorId is null on Threads, which only sends the username
textThe comment text
authorThe commenter's follow relationship and follower count, in the same shape as sender on message.received. Instagram only reveals it to accounts the person has messaged, so it is null unless the commenter has sent your account a DM since it was connected, and always null on other networks. null means unknown, never "not a follower"

Comments are not stored by Outstand, so this event is the only record of them we pass on. Use commentId to deduplicate: a comment can occasionally be delivered more than once.

Verifying Webhook Signatures

If you configure a signing secret, Outstand will include an X-Outstand-Signature header with each request. This allows you to verify that the webhook came from Outstand and wasn't tampered with.

The signature is computed as an HMAC-SHA256 hash of the raw request body using your signing secret:

// Node.js example
const crypto = require('crypto');

function verifySignature(payload, signature, secret) {
  const expectedSignature = 'sha256=' + crypto
    .createHmac('sha256', secret)
    .update(payload, 'utf8')
    .digest('hex');
  
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

// In your webhook handler
app.post('/webhook', (req, res) => {
  const signature = req.headers['x-outstand-signature'];
  const isValid = verifySignature(
    JSON.stringify(req.body),
    signature,
    process.env.WEBHOOK_SECRET
  );
  
  if (!isValid) {
    return res.status(401).send('Invalid signature');
  }
  
  // Process the webhook...
  res.status(200).send('OK');
});
# Python example
import hmac
import hashlib

def verify_signature(payload: str, signature: str, secret: str) -> bool:
    expected = 'sha256=' + hmac.new(
        secret.encode('utf-8'),
        payload.encode('utf-8'),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)

# In your webhook handler
@app.route('/webhook', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-Outstand-Signature')
    is_valid = verify_signature(
        request.get_data(as_text=True),
        signature,
        os.environ['WEBHOOK_SECRET']
    )
    
    if not is_valid:
        return 'Invalid signature', 401
    
    # Process the webhook...
    return 'OK', 200

Delivery & Retries

Outstand provides a scalable and reliable delivery mechanism to deliver webhooks, which provides:

  • Automatic retries: Failed deliveries are retried up to 5 times with exponential backoff
  • Retry intervals: Starting at 10 seconds, increasing up to 5 minutes between attempts
  • Timeout: Each delivery attempt has a 30-second timeout

Response Requirements

Your endpoint should:

  • Return a 2xx status code (200-299) to acknowledge receipt
  • Respond within 30 seconds
  • Be idempotent (handle duplicate deliveries gracefully)

If your endpoint returns a 4xx or 5xx status code, or times out, the delivery will be retried.

Best Practices

  1. Always verify signatures if you've configured a signing secret
  2. Respond quickly - do heavy processing asynchronously after acknowledging the webhook
  3. Handle duplicates - use the postId and timestamp (or commentId for comment.received) to deduplicate if needed
  4. Use HTTPS - webhook endpoints must use HTTPS for security
  5. Test your endpoint - use the Test button in the UI before going live
  6. Monitor failures - set up alerting for failed webhook deliveries on your end

Testing Webhooks

You can test your webhook configuration directly from the Outstand UI:

  1. Go to Settings → Webhooks
  2. Find the webhook you want to test
  3. Click the Test button

This will send a test payload to your endpoint:

{
  "event": "test",
  "timestamp": "2024-12-29T10:30:00.000Z",
  "data": {
    "message": "This is a test webhook from Outstand",
    "endpointId": 123
  }
}

The test result will show you the HTTP status code returned by your endpoint.