Webhook Endpoints
GoBlue uses webhooks as the primary method for receiving data from external systems. Each form in your GoBlue account has a unique webhook endpoint that can receive JSON data and automatically generate personalized messages.
POST /v1/forms//webhook
Submit data to a specific form to generate an automated message.
The unique identifier for your GoBlue form. Found in the form settings within the GoBlue app.
POST https://api.goblue.app/v1/forms/{form-id}/webhook
Content-Type : application/json
{
"phoneNumber" : "+1234567890" ,
"firstName" : "John" ,
"lastName" : "Doe" ,
"email" : "[email protected] " ,
"customField" : "Custom data"
}
Request Body
The recipient’s phone number in any standard format. Must be a valid mobile number capable of receiving iMessages. Accepted formats:
+1234567890 (E.164 format - preferred)
(123) 456-7890
123-456-7890
123.456.7890
1234567890
Recipient’s first name. Used for personalizing messages with {{firstName}} variables. Constraints:
Maximum 50 characters
UTF-8 encoding supported
Recipient’s last name. Used for personalizing messages with {{lastName}} variables. Constraints:
Maximum 50 characters
UTF-8 encoding supported
Recipient’s email address. Optional field for record keeping and additional personalization. Format: Valid email address format
Any additional fields you’ve configured in your GoBlue form. Field names must match exactly (case-sensitive). Constraints:
Maximum 500 characters per field
Field names must match form configuration
UTF-8 encoding supported
Success Response
Error Response
{
"status" : "success" ,
"message" : "Message queued successfully" ,
"data" : {
"messageId" : "550e8400-e29b-41d4-a716-446655440000" ,
"contactId" : "123e4567-e89b-12d3-a456-426614174000" ,
"queuePosition" : 1 ,
"estimatedDelivery" : "2024-01-15T10:30:00Z"
}
}
Always “success” for successful requests
Human-readable success message
Unique identifier for the generated message
Unique identifier for the contact record (created or updated)
Position in the message sending queue
ISO 8601 timestamp of estimated delivery time
Status Codes
Request processed successfully and message queued for delivery.
Invalid request data. Common causes:
Missing required phoneNumber field
Invalid JSON format
Invalid phone number format
Field validation failures
Form not found. Causes:
Invalid form ID in the URL
Form has been deleted
Typo in the webhook URL
Rate limit exceeded. You’re sending requests too quickly.
Limit: 100 requests per minute per form
Include exponential backoff in retry logic
Form capturing is disabled or service temporarily unavailable.
Check that “Enable capturing” is turned on in form settings
Try again later if it’s a temporary service issue
Request Examples
curl -X POST https://api.goblue.app/v1/forms/abc123/webhook \
-H "Content-Type: application/json" \
-d '{
"firstName": "Sarah",
"lastName": "Johnson",
"phoneNumber": "+1555123456",
"email": "[email protected] ",
"serviceType": "Web Development",
"message": "Interested in redesigning my company website"
}'
E-commerce Order Webhook
curl -X POST https://api.goblue.app/v1/forms/def456/webhook \
-H "Content-Type: application/json" \
-d '{
"firstName": "Mike",
"lastName": "Chen",
"phoneNumber": "(555) 987-6543",
"email": "[email protected] ",
"orderNumber": "ORD-12345",
"orderTotal": "$249.99",
"items": "Wireless Headphones, Phone Case",
"shippingAddress": "123 Main St, Anytown, ST 12345"
}'
Lead Capture with Custom Fields
curl -X POST https://api.goblue.app/v1/forms/ghi789/webhook \
-H "Content-Type: application/json" \
-d '{
"firstName": "Lisa",
"lastName": "Rodriguez",
"phoneNumber": "555.444.3333",
"email": "[email protected] ",
"company": "Acme Corp",
"jobTitle": "Marketing Director",
"budget": "$10,000-$25,000",
"timeline": "3-6 months",
"leadSource": "Google Ads",
"interests": ["SEO", "Content Marketing", "Social Media"]
}'
Appointment Booking Confirmation
curl -X POST https://api.goblue.app/v1/forms/jkl012/webhook \
-H "Content-Type: application/json" \
-d '{
"firstName": "David",
"lastName": "Kim",
"phoneNumber": "+1-555-222-1111",
"email": "[email protected] ",
"appointmentDate": "2024-01-20",
"appointmentTime": "2:30 PM",
"serviceType": "Consultation",
"duration": "60 minutes",
"location": "Office",
"notes": "First-time client"
}'
Programming Language Examples
JavaScript/Node.js
const axios = require ( 'axios' );
async function sendToGoBlue ( formId , data ) {
try {
const response = await axios . post (
`https://api.goblue.app/v1/forms/ ${ formId } /webhook` ,
data ,
{
headers: {
'Content-Type' : 'application/json'
},
timeout: 15000
}
);
console . log ( 'Message queued:' , response . data );
return response . data ;
} catch ( error ) {
if ( error . response ) {
console . error ( 'API Error:' , error . response . data );
throw new Error ( `GoBlue API error: ${ error . response . data . message } ` );
} else {
console . error ( 'Network Error:' , error . message );
throw error ;
}
}
}
// Usage
sendToGoBlue ( 'your-form-id' , {
firstName: 'John' ,
lastName: 'Doe' ,
phoneNumber: '+1234567890' ,
serviceType: 'consulting'
});
Python
import requests
import json
def send_to_goblue ( form_id , data ):
url = f "https://api.goblue.app/v1/forms/ { form_id } /webhook"
headers = {
'Content-Type' : 'application/json'
}
try :
response = requests.post(
url,
json = data,
headers = headers,
timeout = 15
)
response.raise_for_status()
result = response.json()
print ( f "Message queued: { result } " )
return result
except requests.exceptions.RequestException as e:
print ( f "Error sending to GoBlue: { e } " )
if hasattr (e, 'response' ) and e.response is not None :
print ( f "Response: { e.response.text } " )
raise
# Usage
send_to_goblue( 'your-form-id' , {
'firstName' : 'Jane' ,
'lastName' : 'Smith' ,
'phoneNumber' : '+1234567890' ,
'serviceType' : 'web development'
})
PHP
<? php
function sendToGoBlue ( $formId , $data ) {
$url = "https://api.goblue.app/v1/forms/{ $formId }/webhook" ;
$payload = json_encode ( $data );
if ( json_last_error () !== JSON_ERROR_NONE ) {
throw new Exception ( 'Invalid JSON data: ' . json_last_error_msg ());
}
$ch = curl_init ();
curl_setopt_array ( $ch , [
CURLOPT_URL => $url ,
CURLOPT_POST => true ,
CURLOPT_POSTFIELDS => $payload ,
CURLOPT_RETURNTRANSFER => true ,
CURLOPT_TIMEOUT => 15 ,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json' ,
'Content-Length: ' . strlen ( $payload )
]
]);
$response = curl_exec ( $ch );
$httpCode = curl_getinfo ( $ch , CURLINFO_HTTP_CODE );
$error = curl_error ( $ch );
curl_close ( $ch );
if ( $error ) {
throw new Exception ( "cURL error: { $error }" );
}
$decoded = json_decode ( $response , true );
if ( json_last_error () !== JSON_ERROR_NONE ) {
throw new Exception ( 'Invalid JSON response: ' . json_last_error_msg ());
}
if ( $httpCode !== 200 ) {
$message = $decoded [ 'message' ] ?? 'Unknown error' ;
throw new Exception ( "GoBlue API error ({ $httpCode }): { $message }" );
}
return $decoded ;
}
// Usage
try {
$result = sendToGoBlue ( 'your-form-id' , [
'firstName' => 'Bob' ,
'lastName' => 'Wilson' ,
'phoneNumber' => '+1234567890' ,
'serviceType' => 'design'
]);
echo "Message queued successfully! \n " ;
print_r ( $result );
} catch ( Exception $e ) {
echo "Error: " . $e -> getMessage () . " \n " ;
}
?>
Ruby
require 'net/http'
require 'json'
require 'uri'
def send_to_goblue ( form_id , data )
uri = URI ( "https://api.goblue.app/v1/forms/ #{ form_id } /webhook" )
http = Net :: HTTP . new (uri. host , uri. port )
http. use_ssl = true
http. read_timeout = 15
request = Net :: HTTP :: Post . new (uri)
request[ 'Content-Type' ] = 'application/json'
request. body = data. to_json
begin
response = http. request (request)
case response. code . to_i
when 200
result = JSON . parse (response. body )
puts "Message queued: #{ result } "
result
else
error_data = JSON . parse (response. body ) rescue {}
error_message = error_data[ 'message' ] || 'Unknown error'
raise "GoBlue API error ( #{ response. code } ): #{ error_message } "
end
rescue Net :: TimeoutError => e
raise "Request timeout: #{ e. message } "
rescue JSON :: ParserError => e
raise "Invalid JSON response: #{ e. message } "
end
end
# Usage
begin
result = send_to_goblue ( 'your-form-id' , {
firstName: 'Alice' ,
lastName: 'Cooper' ,
phoneNumber: '+1234567890' ,
serviceType: 'marketing'
})
puts "Success: #{ result } "
rescue => e
puts "Error: #{ e. message } "
end
Error Handling
Common Error Scenarios
{
"status" : "error" ,
"message" : "Missing required field: phoneNumber" ,
"code" : "VALIDATION_ERROR" ,
"details" : {
"field" : "phoneNumber" ,
"requirement" : "Valid phone number required for message delivery"
}
}
Solution : Always include a phoneNumber field in your webhook payload.
Invalid Phone Number Format
{
"status" : "error" ,
"message" : "Rate limit exceeded. Please try again later." ,
"code" : "RATE_LIMIT_EXCEEDED" ,
"details" : {
"limit" : 100 ,
"window" : "1 minute" ,
"retryAfter" : 60
}
}
Solution : Implement exponential backoff and respect the retryAfter value.
Implementing Robust Error Handling
class GoBlueWebhookClient {
constructor ( formId , options = {}) {
this . formId = formId ;
this . baseUrl = 'https://api.goblue.app/v1' ;
this . maxRetries = options . maxRetries || 3 ;
this . timeout = options . timeout || 15000 ;
}
async send ( data , attempt = 1 ) {
const url = ` ${ this . baseUrl } /forms/ ${ this . formId } /webhook` ;
try {
const response = await fetch ( url , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( data ),
timeout: this . timeout
});
const result = await response . json ();
if ( ! response . ok ) {
throw new GoBlueError ( result , response . status );
}
return result ;
} catch ( error ) {
if ( this . shouldRetry ( error , attempt )) {
const delay = this . calculateDelay ( attempt , error );
await new Promise ( resolve => setTimeout ( resolve , delay ));
return this . send ( data , attempt + 1 );
}
throw error ;
}
}
shouldRetry ( error , attempt ) {
if ( attempt >= this . maxRetries ) return false ;
// Retry on network errors and rate limiting
if ( error . name === 'TypeError' || error . name === 'TimeoutError' ) {
return true ;
}
if ( error instanceof GoBlueError ) {
return error . code === 'RATE_LIMIT_EXCEEDED' || error . status >= 500 ;
}
return false ;
}
calculateDelay ( attempt , error ) {
// Use retryAfter from rate limit response if available
if ( error instanceof GoBlueError && error . retryAfter ) {
return error . retryAfter * 1000 ;
}
// Exponential backoff: 1s, 2s, 4s, 8s...
return Math . min ( 1000 * Math . pow ( 2 , attempt - 1 ), 30000 );
}
}
class GoBlueError extends Error {
constructor ( apiResponse , status ) {
super ( apiResponse . message );
this . name = 'GoBlueError' ;
this . code = apiResponse . code ;
this . status = status ;
this . details = apiResponse . details ;
this . retryAfter = apiResponse . details ?. retryAfter ;
}
}
// Usage
const client = new GoBlueWebhookClient ( 'your-form-id' );
try {
const result = await client . send ({
firstName: 'John' ,
lastName: 'Doe' ,
phoneNumber: '+1234567890'
});
console . log ( 'Message queued:' , result );
} catch ( error ) {
if ( error instanceof GoBlueError ) {
console . error ( `GoBlue API error ( ${ error . code } ):` , error . message );
// Handle specific error types
switch ( error . code ) {
case 'VALIDATION_ERROR' :
// Fix data and retry
break ;
case 'FORM_DISABLED' :
// Alert user to enable form
break ;
case 'RATE_LIMIT_EXCEEDED' :
// Implement longer delay
break ;
}
} else {
console . error ( 'Network or other error:' , error . message );
}
}
Webhook Security
Request Validation
GoBlue validates all incoming webhook requests:
JSON Validation Ensures valid JSON structure and data types
Field Validation Validates required fields and data formats
Size Limits Enforces payload size limits (max 10KB)
Rate Limiting Prevents abuse with request rate limiting
Best Practices
Always use HTTPS URLs for webhook endpoints. HTTP requests are automatically redirected but may expose data during the redirect.
Validate phone numbers and required fields in your application before sending to GoBlue to avoid unnecessary API calls.
Include unique identifiers in your payloads to prevent duplicate message creation: {
"firstName" : "John" ,
"phoneNumber" : "+1234567890" ,
"externalId" : "lead-12345" ,
"source" : "website-form"
}
Log and monitor API response codes to identify and resolve issues quickly: const response = await sendToGoBlue ( data );
// Log for monitoring
console . log ({
timestamp: new Date (). toISOString (),
formId: 'abc123' ,
status: response . status ,
messageId: response . data ?. messageId ,
phoneNumber: data . phoneNumber // Consider hashing for privacy
});
Testing Webhooks
Manual Testing with cURL
# Test basic webhook functionality
curl -X POST https://api.goblue.app/v1/forms/YOUR_FORM_ID/webhook \
-H "Content-Type: application/json" \
-d '{
"firstName": "Test",
"lastName": "User",
"phoneNumber": "+1234567890",
"testField": "webhook test"
}' \
-v
# Test error handling - missing phone number
curl -X POST https://api.goblue.app/v1/forms/YOUR_FORM_ID/webhook \
-H "Content-Type: application/json" \
-d '{
"firstName": "Test",
"lastName": "User"
}' \
-v
# Test invalid form ID
curl -X POST https://api.goblue.app/v1/forms/invalid-form-id/webhook \
-H "Content-Type: application/json" \
-d '{
"firstName": "Test",
"phoneNumber": "+1234567890"
}' \
-v
Automated Testing
// Jest test suite example
describe ( 'GoBlue Webhook Integration' , () => {
const webhookUrl = 'https://api.goblue.app/v1/forms/test-form-id/webhook' ;
test ( 'should accept valid webhook data' , async () => {
const data = {
firstName: 'John' ,
lastName: 'Doe' ,
phoneNumber: '+1234567890' ,
email: '[email protected] '
};
const response = await fetch ( webhookUrl , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( data )
});
expect ( response . status ). toBe ( 200 );
const result = await response . json ();
expect ( result . status ). toBe ( 'success' );
expect ( result . data . messageId ). toBeDefined ();
});
test ( 'should reject data without phone number' , async () => {
const data = {
firstName: 'John' ,
lastName: 'Doe' ,
email: '[email protected] '
};
const response = await fetch ( webhookUrl , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: JSON . stringify ( data )
});
expect ( response . status ). toBe ( 400 );
const result = await response . json ();
expect ( result . status ). toBe ( 'error' );
expect ( result . code ). toBe ( 'VALIDATION_ERROR' );
});
test ( 'should handle malformed JSON' , async () => {
const response = await fetch ( webhookUrl , {
method: 'POST' ,
headers: { 'Content-Type' : 'application/json' },
body: 'invalid json'
});
expect ( response . status ). toBe ( 400 );
});
});
Webhook Monitoring
Logging Best Practices
const winston = require ( 'winston' );
const logger = winston . createLogger ({
level: 'info' ,
format: winston . format . combine (
winston . format . timestamp (),
winston . format . json ()
),
transports: [
new winston . transports . File ({ filename: 'goblue-webhooks.log' })
]
});
async function sendWebhookWithLogging ( formId , data ) {
const requestId = Date . now (). toString ( 36 );
logger . info ( 'Webhook request started' , {
requestId ,
formId ,
hasPhoneNumber: !! data . phoneNumber ,
fieldCount: Object . keys ( data ). length
});
try {
const startTime = Date . now ();
const response = await sendToGoBlue ( formId , data );
const duration = Date . now () - startTime ;
logger . info ( 'Webhook request successful' , {
requestId ,
messageId: response . data . messageId ,
duration ,
queuePosition: response . data . queuePosition
});
return response ;
} catch ( error ) {
logger . error ( 'Webhook request failed' , {
requestId ,
error: error . message ,
code: error . code || 'UNKNOWN' ,
status: error . status
});
throw error ;
}
}
Metrics and Monitoring
Track key metrics for your webhook integration:
Success Rate Percentage of successful webhook requests vs total requests
Response Time Average API response time and 95th percentile latency
Error Rate by Type Breakdown of error types (validation, rate limiting, etc.)
Queue Depth Number of messages in your GoBlue queue over time
Next Steps