Skip to main content

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.

Form Webhook Endpoint

POST /v1/forms//webhook

Submit data to a specific form to generate an automated message.
form-id
string
required
The unique identifier for your GoBlue form. Found in the form settings within the GoBlue app.

Request Format

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

phoneNumber
string
required
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
firstName
string
Recipient’s first name. Used for personalizing messages with {{firstName}} variables.Constraints:
  • Maximum 50 characters
  • UTF-8 encoding supported
lastName
string
Recipient’s last name. Used for personalizing messages with {{lastName}} variables.Constraints:
  • Maximum 50 characters
  • UTF-8 encoding supported
email
string
Recipient’s email address. Optional field for record keeping and additional personalization.Format: Valid email address format
[customField]
string
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

Response Format

  • 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"
  }
}
status
string
Always “success” for successful requests
message
string
Human-readable success message
data.messageId
string
Unique identifier for the generated message
data.contactId
string
Unique identifier for the contact record (created or updated)
data.queuePosition
integer
Position in the message sending queue
data.estimatedDelivery
string
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

Basic Contact Form Submission

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.
{
  "status": "error",
  "message": "Invalid phone number format",
  "code": "VALIDATION_ERROR",
  "details": {
    "field": "phoneNumber",
    "provided": "123",
    "requirement": "Must be a valid mobile phone number"
  }
}
Solution: Validate phone numbers before sending. Use E.164 format when possible.
{
  "status": "error",
  "message": "Form not found",
  "code": "NOT_FOUND",
  "details": {
    "formId": "invalid-form-id"
  }
}
Solution: Verify the form ID in your webhook URL matches your GoBlue form.
{
  "status": "error",
  "message": "Form capturing is currently disabled",
  "code": "FORM_DISABLED",
  "details": {
    "formId": "abc123",
    "action": "Enable capturing in form settings"
  }
}
Solution: Enable “Enable capturing” in your form settings within the GoBlue app.
{
  "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