n8n Webhook Endpoints
Base URL: https://hooks.mad-monkey-creations.com
GET /scan
Description: Log QR code scan and redirect to landing page
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
| qr_id | string | Yes | Card ID (e.g., "T42", "N123") |
Headers Captured
X-Forwarded-FororX-Real-IP- Client IP addressUser-Agent- Browser/device informationReferer- Previous page URL (if available)
Example Request
curl -L "https://hooks.mad-monkey-creations.com/scan?qr_id=T42"
Response
HTTP 302 Redirect to: https://mad-monkey-creations.com/benny?qr_id=T42
Workflow Actions
- Extract qr_id from query parameter
- Capture IP address and User-Agent
- Anonymize IP address (remove last octet)
- Call geolocation API (ipapi.co or ipinfo)
- Insert record into PostgreSQL
scanstable - Update
cards.last_scan_tsand incrementtotal_scans - Return 302 redirect with Location header
Error Handling
| Error | HTTP Code | Response |
|---|---|---|
| Missing qr_id | 400 | {"error": "Missing qr_id parameter"} |
| Invalid qr_id | 404 | {"error": "Card not found"} |
| Database error | 500 | {"error": "Internal server error"} |
POST /entry
Description: Process contest entry form submission
Request Body (JSON)
{
"qr_id": "T42",
"email": "user@example.com",
"name": "John Doe",
"user_city": "Chicago, IL",
"consent": true,
"captcha_token": "optional-captcha-token"
}
Validation Rules
| Field | Type | Required | Validation |
|---|---|---|---|
| qr_id | string | Yes | Must exist in cards table |
| string | Yes | Valid email format | |
| name | string | No | Max 100 characters |
| user_city | string | Yes | Max 100 characters |
| consent | boolean | Yes | Must be true |
Rate Limiting
- Max 3 submissions per email per hour
- Max 1 verified entry per email per month
- Max 10 submissions per IP per hour
Example Request
curl -X POST https://hooks.mad-monkey-creations.com/entry \
-H "Content-Type: application/json" \
-d '{
"qr_id": "T42",
"email": "user@example.com",
"name": "John Doe",
"user_city": "Chicago, IL",
"consent": true
}'
Success Response
{
"success": true,
"message": "Entry submitted! Check your email to confirm.",
"entry_id": 12345
}
Workflow Actions
- Validate all required fields
- Check rate limits (email, IP)
- Check if email already entered this month
- Insert entry to PostgreSQL
entriestable (verified=false) - Calculate month_bucket (YYYY-MM format)
- Call Listmonk API to create/update subscriber
- Add tags: qr_campaign, benny, entry_month_YYYY-MM
- Listmonk automatically sends double opt-in email
- Return success response
Error Responses
| Error | HTTP Code | Response |
|---|---|---|
| Invalid email format | 400 | {"error": "Invalid email address"} |
| Missing consent | 400 | {"error": "Consent required"} |
| Rate limit exceeded | 429 | {"error": "Too many entries, please try again later"} |
| Duplicate entry (same month) | 409 | {"error": "You've already entered this month"} |
POST /listmonk/confirm
Description: Handle Listmonk webhook on subscriber confirmation
Request Body (from Listmonk)
{
"event": "subscriber.confirmed",
"subscriber": {
"id": 123,
"email": "user@example.com",
"name": "John Doe",
"status": "enabled",
"lists": [1]
}
}
Workflow Actions
- Validate webhook signature or source IP
- Extract subscriber email from payload
- Update PostgreSQL
entries.verified=truefor email - Set
verified_attimestamp - Update
userstable with confirmed status - Trigger welcome email (immediate)
- Schedule story email (Day 1 delay - 24 hours)
- Schedule reward email (Day 3 delay - 72 hours)
Success Response
{
"success": true,
"message": "Subscriber confirmed"
}
Shlink API
Base URL: http://10.0.0.250:8081 (internal) or https://admin.mmlnk.us/rest (external, IP-restricted)
API Version: v3
X-Api-Key header
GET /rest/v3/short-urls
Description: List all short URLs
Example Request
curl -X GET "http://10.0.0.250:8081/rest/v3/short-urls" \ -H "X-Api-Key: your-api-key-here"
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| page | integer | Page number (default: 1) |
| itemsPerPage | integer | Items per page (default: 10) |
| searchTerm | string | Filter by short code or long URL |
| tags[] | array | Filter by tags |
Example Response
{
"shortUrls": {
"data": [
{
"shortCode": "T42",
"shortUrl": "https://mmlnk.us/T42",
"longUrl": "https://hooks.mad-monkey-creations.com/scan?qr_id=T42",
"dateCreated": "2024-12-01T10:30:00Z",
"visitsCount": 42,
"tags": ["benny", "traveler"],
"domain": "mmlnk.us"
}
],
"pagination": {
"currentPage": 1,
"pagesCount": 10,
"itemsPerPage": 10,
"itemsInCurrentPage": 10,
"totalItems": 95
}
}
}
POST /rest/v3/short-urls
Description: Create a new short URL
Request Body
{
"longUrl": "https://hooks.mad-monkey-creations.com/scan?qr_id=T42",
"customSlug": "T42",
"domain": "mmlnk.us",
"tags": ["benny", "traveler"],
"title": "Benny Traveler Card 42",
"findIfExists": true
}
Example Request
curl -X POST "http://10.0.0.250:8081/rest/v3/short-urls" \
-H "X-Api-Key: your-api-key-here" \
-H "Content-Type: application/json" \
-d '{
"longUrl": "https://hooks.mad-monkey-creations.com/scan?qr_id=T42",
"customSlug": "T42",
"domain": "mmlnk.us",
"tags": ["benny", "traveler"]
}'
Success Response
{
"shortCode": "T42",
"shortUrl": "https://mmlnk.us/T42",
"longUrl": "https://hooks.mad-monkey-creations.com/scan?qr_id=T42",
"dateCreated": "2024-12-01T10:30:00Z",
"tags": ["benny", "traveler"],
"domain": "mmlnk.us"
}
GET /rest/v3/short-urls/{shortCode}/visits
Description: Get visit statistics for a short URL
Example Request
curl -X GET "http://10.0.0.250:8081/rest/v3/short-urls/T42/visits" \ -H "X-Api-Key: your-api-key-here"
Example Response
{
"visits": {
"data": [
{
"date": "2024-12-01T15:23:10Z",
"referer": "",
"userAgent": "Mozilla/5.0 (iPhone; CPU iPhone OS 17_0 like Mac OS X)",
"visitLocation": {
"cityName": "Chicago",
"countryName": "United States",
"regionName": "Illinois"
}
}
],
"pagination": {
"currentPage": 1,
"pagesCount": 3,
"totalItems": 42
}
}
}
Listmonk API
Base URL: http://10.0.0.250:9000/api
API Documentation: https://listmonk.app/docs/apis/apis
POST /api/subscribers
Description: Create or update a subscriber
Request Body
{
"email": "user@example.com",
"name": "John Doe",
"status": "enabled",
"lists": [1],
"attribs": {
"city": "Chicago",
"qr_id": "T42",
"entry_month": "2024-12"
},
"preconfirm_subscriptions": false
}
Example Request
curl -X POST "http://10.0.0.250:9000/api/subscribers" \
-u "admin:your-password" \
-H "Content-Type: application/json" \
-d '{
"email": "user@example.com",
"name": "John Doe",
"status": "enabled",
"lists": [1],
"preconfirm_subscriptions": false
}'
Success Response
{
"data": {
"id": 123,
"created_at": "2024-12-01T10:30:00Z",
"updated_at": "2024-12-01T10:30:00Z",
"email": "user@example.com",
"name": "John Doe",
"status": "enabled",
"lists": [
{
"id": 1,
"name": "Mad Monkey - Benny Campaign",
"subscription_status": "unconfirmed"
}
]
}
}
preconfirm_subscriptions is false, Listmonk automatically sends a double opt-in email. Set to true to skip confirmation (not recommended for compliance).
GET /api/subscribers
Description: List subscribers with filters
Query Parameters
| Parameter | Type | Description |
|---|---|---|
| query | string | SQL WHERE clause (e.g., "subscribers.email LIKE '%@gmail.com'") |
| list_id | integer | Filter by list ID |
| page | integer | Page number |
| per_page | integer | Items per page (max 100) |
Example Request
curl -X GET "http://10.0.0.250:9000/api/subscribers?list_id=1&query=subscribers.attribs->>'entry_month'='2024-12'" \ -u "admin:your-password"
POST /api/campaigns/{id}/status
Description: Send a campaign (transactional email)
Request Body
{
"status": "running"
}
Example Request
curl -X PUT "http://10.0.0.250:9000/api/campaigns/5/status" \
-u "admin:your-password" \
-H "Content-Type: application/json" \
-d '{"status": "running"}'
POST /api/tx
Description: Send transactional email (for drip campaigns)
Request Body
{
"subscriber_email": "user@example.com",
"template_id": 2,
"data": {
"name": "John",
"story": "Benny was spotted in Chicago, then traveled to Milwaukee...",
"discount_code": "BANANAS10"
}
}
Example Request
curl -X POST "http://10.0.0.250:9000/api/tx" \
-u "admin:your-password" \
-H "Content-Type: application/json" \
-d '{
"subscriber_email": "user@example.com",
"template_id": 2,
"data": {
"name": "John",
"discount_code": "BANANAS10"
}
}'
Geolocation APIs
ipapi.co (Recommended)
Base URL: https://ipapi.co
Rate Limit: 1,000 requests/day (free tier), 30,000/month (paid)
GET /{ip}/json
Example Request
curl "https://ipapi.co/203.0.113.42/json/"
Example Response
{
"ip": "203.0.113.42",
"city": "Chicago",
"region": "Illinois",
"region_code": "IL",
"country": "US",
"country_name": "United States",
"postal": "60601",
"latitude": 41.8781,
"longitude": -87.6298,
"timezone": "America/Chicago",
"org": "Comcast Cable Communications LLC"
}
https://ipapi.co/{{ $json.ip }}/json/
ipinfo.io (Alternative)
Base URL: https://ipinfo.io
Rate Limit: 50,000 requests/month (free tier)
GET /{ip}/json
Example Request
curl "https://ipinfo.io/203.0.113.42/json?token=YOUR_TOKEN"
Example Response
{
"ip": "203.0.113.42",
"city": "Chicago",
"region": "Illinois",
"country": "US",
"loc": "41.8781,-87.6298",
"postal": "60601",
"timezone": "America/Chicago",
"org": "AS7922 Comcast Cable Communications LLC"
}
External APIs
Printify API (Prize Fulfillment)
Base URL: https://api.printify.com/v1
Documentation: https://developers.printify.com
Authorization: Bearer YOUR_TOKEN
POST /shops/{shop_id}/orders.json
Request Body (Simplified)
{
"external_id": "prize-2024-12",
"label": "Mad Monkey Winner - December 2024",
"line_items": [
{
"product_id": "your-product-id",
"variant_id": 12345,
"quantity": 1
}
],
"shipping_method": 1,
"address_to": {
"first_name": "John",
"last_name": "Doe",
"email": "user@example.com",
"address1": "123 Main St",
"city": "Chicago",
"region": "IL",
"zip": "60601",
"country": "US"
}
}
Example Request
curl -X POST "https://api.printify.com/v1/shops/YOUR_SHOP_ID/orders.json" \ -H "Authorization: Bearer YOUR_TOKEN" \ -H "Content-Type: application/json" \ -d @order.json
Etsy API (Product Links)
Base URL: https://openapi.etsy.com/v3
Documentation: https://developers.etsy.com
x-api-key header
GET /application/shops/{shop_id}/listings/active
Example Request
curl "https://openapi.etsy.com/v3/application/shops/YOUR_SHOP_ID/listings/active" \ -H "x-api-key: YOUR_API_KEY"