Skip to content

Webhooks

Subscribe Flow supports bidirectional webhooks: incoming (from Resend and Stripe) and outgoing (to your systems). All outgoing webhooks are scoped to the organization associated with your API key.

Webhook Limits by Tier

Tier Webhook Limit
Free 0 (not available)
Starter 3
Professional Unlimited

Free-tier organizations cannot create webhooks. Attempting to do so returns a 403 Forbidden with error code limit_exceeded.

Outgoing Webhooks

Receive notifications when events occur in Subscribe Flow.

Creating a Webhook

Bash
1
2
3
4
5
6
7
8
curl -X POST /api/v1/webhooks \
  -H "X-API-Key: $API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://api.example.com/webhooks/subscribeflow",
    "events": ["subscriber.created", "subscriber.updated", "tag.subscribed"],
    "secret": "whsec_your-secret-key"
  }'

The webhook is automatically associated with your organization.

Events

Event Description
subscriber.created New subscriber created
subscriber.updated Subscriber data changed
subscriber.deleted Subscriber deleted
tag.subscribed Subscriber subscribed to a tag
tag.unsubscribed Subscriber unsubscribed from a tag
subscriber.note_added Note added to subscriber

Payload Format

JSON
{
  "id": "evt_123abc",
  "type": "tag.subscribed",
  "created_at": "2024-01-15T10:30:00Z",
  "data": {
    "subscriber": {
      "id": "sub_abc123",
      "email": "user@example.com"
    },
    "tag": {
      "id": "tag_xyz789",
      "name": "product-updates"
    },
    "source": "preference_center"
  }
}

Signature Verification

Webhooks are signed with HMAC-SHA256:

Python
import hmac
import hashlib

def verify_webhook(payload: bytes, signature: str, secret: str) -> bool:
    expected = hmac.new(
        secret.encode(),
        payload,
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(f"sha256={expected}", signature)

# In Flask
@app.route('/webhooks/subscribeflow', methods=['POST'])
def handle_webhook():
    signature = request.headers.get('X-SubscribeFlow-Signature')
    if not verify_webhook(request.data, signature, WEBHOOK_SECRET):
        return 'Invalid signature', 401

    event = request.json
    # Process event...
    return 'OK', 200

Retry Policy

Attempt Wait Time
1 Immediately
2 5 minutes
3 30 minutes
4 2 hours
5 24 hours

After 5 failed attempts, the webhook is deactivated.

Incoming Webhooks (Resend)

Subscribe Flow receives webhooks from Resend for email events.

Setup

  1. Configure the webhook URL in Resend:

    Text Only
    https://api.subscribeflow.net/webhooks/resend
    

  2. Enable events:

  3. email.bounced
  4. email.complained
  5. email.delivered

Event Processing

sequenceDiagram
    participant R as Resend
    participant SF as Subscribe Flow
    participant DB as Database

    R->>SF: Webhook: email.bounced
    SF->>SF: Verify signature
    SF->>DB: Subscriber status -> bounced
    SF->>SF: Trigger outgoing webhook
    SF->>R: 200 OK

Supported Resend Events

Resend Event Subscribe Flow Action
email.bounced Status -> bounced
email.complained Status -> complained
email.delivered Update statistics

Incoming Webhooks (Stripe)

Subscribe Flow receives webhooks from Stripe for billing events. These are separate from the subscriber webhook system and are handled by the BillingService.

Setup

  1. Configure the webhook URL in Stripe Dashboard:

    Text Only
    https://api.subscribeflow.net/webhooks/stripe
    

  2. Required events:

  3. checkout.session.completed
  4. customer.subscription.updated
  5. customer.subscription.deleted
  6. invoice.payment_failed

Supported Stripe Events

Stripe Event Subscribe Flow Action
checkout.session.completed Activate subscription, update org tier
customer.subscription.updated Sync tier and limits
customer.subscription.deleted Downgrade to free tier
invoice.payment_failed Notify admin, grace period

Stripe vs. Subscriber Webhooks

Stripe webhooks are a system-level integration for billing and are not configurable per organization. Outgoing subscriber webhooks are organization-scoped and configurable via the API.

Webhook Management

Listing Webhooks

Bash
curl /api/v1/webhooks \
  -H "X-API-Key: $API_KEY"

Returns only webhooks belonging to your organization.

Testing a Webhook

Bash
curl -X POST /api/v1/webhooks/:id/test \
  -H "X-API-Key: $API_KEY"

Webhook Logs

Bash
curl /api/v1/webhooks/:id/logs \
  -H "X-API-Key: $API_KEY"

Response:

JSON
{
  "data": [
    {
      "id": "log_123",
      "event_type": "subscriber.created",
      "status": "delivered",
      "response_code": 200,
      "duration_ms": 150,
      "created_at": "2024-01-15T10:30:00Z"
    },
    {
      "id": "log_124",
      "event_type": "tag.subscribed",
      "status": "failed",
      "response_code": 500,
      "error": "Connection timeout",
      "retry_count": 2,
      "created_at": "2024-01-15T10:25:00Z"
    }
  ]
}

Deleting a Webhook

Bash
curl -X DELETE /api/v1/webhooks/:id \
  -H "X-API-Key: $API_KEY"

Best Practices

Idempotency

Process webhooks idempotently:

Python
def handle_webhook(event):
    # Check if the event has already been processed
    if redis.exists(f"processed:{event['id']}"):
        return 'Already processed'

    # Process event
    process_event(event)

    # Mark as processed (24h TTL)
    redis.setex(f"processed:{event['id']}", 86400, '1')
    return 'OK'

Asynchronous Processing

Python
from celery import Celery

@app.route('/webhooks/subscribeflow', methods=['POST'])
def handle_webhook():
    # Respond quickly
    process_webhook.delay(request.json)
    return 'OK', 200

@celery.task
def process_webhook(event):
    # Long processing here
    pass

Error Handling

Python
@app.route('/webhooks/subscribeflow', methods=['POST'])
def handle_webhook():
    try:
        event = request.json
        process_event(event)
        return 'OK', 200
    except TemporaryError:
        # 5xx -> Retry
        return 'Retry later', 503
    except PermanentError:
        # 4xx -> No retry
        return 'Invalid', 400

Troubleshooting

Webhook is not received
  1. Is the URL reachable from the internet?
  2. Is HTTPS configured?
  3. Check firewall rules
  4. Is the webhook active?
  5. Does your plan support webhooks? (Free tier cannot use webhooks)
Signature error
  1. Is the secret correct?
  2. Use the raw body for signature verification
  3. Check encoding (UTF-8)
High latency
  1. Process asynchronously
  2. Respond quickly (< 5s)
  3. Use a queue for long operations