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
slug- URL-safe identifierurl_path- Full computed pathurl_format- Path pattern usedcategory- Object with id, name, slug
Idempotency
request_id- Unique per publish- Use to prevent duplicate content
- Store and check before inserting
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
- Deploy the receiver - Copy the code for your platform and deploy it to your server
- Create the database table - Run the SQL schema on your database
- Set your secret token - Add
LINKLOOM_WEBHOOK_SECRETas an environment variable - 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})
- Test the webhook - Use the “Test Webhook” button in Site Settings