LinkLoom

Back

Webhook Integration Guide

Receive published content from LinkLoom on your website or application

Webhook Payload Structure

When content is published, LinkLoom sends a POST request with this JSON structure

{
  "title": "My Article Title",
  "content": "<p>HTML content of the article...</p>",
  "excerpt": "Brief summary of the article",
  "author": "John Doe",
  "published_at": "2026-01-18T12:00:00Z",
  "site_name": "My Tech Blog",
  "cover_image_url": "https://example.com/image.jpg",
  "slug": "my-article-title",
  "url_format": "/blog/{slug}",
  "url_path": "/blog/my-article-title",
  "category": {
    "id": "uuid-here",
    "name": "Technology",
    "slug": "technology"
  },
  "request_id": "req_abc123_1705579200000"
}

Path Fields

Idempotency

Security

All webhook requests include an Authorization header with your security token:

Authorization: Bearer your-secret-token

Always validate this token before processing the request. The token is configured in your Site Settings → Webhook Settings.

Receiver Code

Copy-paste ready code for your platform

SupabasePHPNode.jsPythonWordPressDatabase

Deploy a Supabase Edge Function to receive and store content from LinkLoom.

Download Starter

Edge Function Code

// supabase/functions/receive-content/index.ts
// Deploy this Edge Function on your Supabase project to receive LinkLoom content

import { createClient } from 'https://esm.sh/@supabase/supabase-js@2'

const corsHeaders = {
  'Access-Control-Allow-Origin': '*',
  'Access-Control-Allow-Headers': 'authorization, x-client-info, apikey, content-type',
}

Deno.serve(async (req) => {
  // Handle CORS preflight
  if (req.method === 'OPTIONS') {
    return new Response(null, { headers: corsHeaders })
  }

  try {
    // Validate authorization token
    const authHeader = req.headers.get('Authorization') || ''
    const token = authHeader.replace('Bearer ', '')
    const expectedToken = Deno.env.get('LINKLOOM_WEBHOOK_SECRET')

    if (!expectedToken || token !== expectedToken) {
      console.error('Token validation failed')
      return new Response(
        JSON.stringify({ error: 'Unauthorized' }),
        { status: 401, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
      )
    }

    // Parse payload
    const payload = await req.json()

    const {
      title,
      content,
      excerpt,
      author,
      published_at,
      site_name,
      cover_image_url,
      slug,
      url_format,
      url_path,
      category,
      request_id
    } = payload

    console.log(`Receiving content: ${title} -> ${url_path}`)

    // Initialize Supabase client with service role key
    const supabaseUrl = Deno.env.get('SUPABASE_URL')!
    const supabaseKey = Deno.env.get('SUPABASE_SERVICE_ROLE_KEY')!
    const supabase = createClient(supabaseUrl, supabaseKey)

    // Check for duplicate (idempotency)
    if (request_id) {
      const { data: existing } = await supabase
        .from('published_content')
        .select('id')
        .eq('request_id', request_id)
        .single()

      if (existing) {
        console.log(`Duplicate request detected: ${request_id}`)
        return new Response(
          JSON.stringify({
            success: true,
            message: 'Content already received',
            content_id: existing.id,
            duplicate: true
          }),
          { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
        )
      }
    }

    // Insert content into database
    const { data, error } = await supabase
      .from('published_content')
      .insert({
        request_id,
        title,
        slug,
        content,
        excerpt,
        author,
        category,
        url_path,
        url_format,
        cover_image_url,
        source_site: site_name,
        published_at,
        status: 'received'
      })
      .select('id')
      .single()

    if (error) {
      console.error('Database error:', error)
      return new Response(
        JSON.stringify({ error: 'Failed to save content', details: error.message }),
        { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
      )
    }

    console.log(`Content saved with ID: ${data.id}`)

    return new Response(
      JSON.stringify({
        success: true,
        message: 'Content received successfully',
        content_id: data.id,
        url_path
      }),
      { headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )

  } catch (error) {
    console.error('Webhook error:', error)
    return new Response(
      JSON.stringify({ error: error.message }),
      { status: 500, headers: { ...corsHeaders, 'Content-Type': 'application/json' } }
    )
  }
})

config.toml Configuration

# Add this to your supabase/config.toml

[functions.receive-content]
verify_jwt = false  # We validate the token manually

Setup Commands

# 1. Create the Edge Function directory
mkdir -p supabase/functions/receive-content

# 2. Create the function file
# Copy the code above to supabase/functions/receive-content/index.ts

# 3. Set your webhook secret
supabase secrets set LINKLOOM_WEBHOOK_SECRET=your-secure-token-here

# 4. Deploy the function
supabase functions deploy receive-content

# 5. Your webhook URL will be:
# https://<your-project-ref>.supabase.co/functions/v1/receive-content

Database Table Required

Create the published_content table using the PostgreSQL schema in the Database tab.

Setup Steps

  1. Deploy the receiver - Copy the code for your platform and deploy it to your server
  2. Create the database table - Run the SQL schema on your database
  3. Set your secret token - Add LINKLOOM_WEBHOOK_SECRET as an environment variable
  4. Configure LinkLoom- In Site Settings → Webhook Settings:
    • Enable webhooks
    • Enter your receiver URL (e.g., https://yoursite.com/webhook/receive-content)
    • Enter the same security token
    • Choose your URL format (e.g., /blog/{slug})
  5. Test the webhook - Use the “Test Webhook” button in Site Settings