Webhook Integration Guide

This guide will help you integrate with our Webhook system to receive real-time event notifications from YCloud.

Getting Started

What is a Webhook?

A webhook is an HTTP callback that allows YCloud to send real-time event notifications to your server. When specific events occur (such as message status updates or inbound messages), YCloud will automatically send an HTTP POST request to your configured URL with the event data.

Basic Requirements

Before implementing webhooks, you need:

A publicly accessible HTTP endpoint

  • Must be reachable from the internet
  • HTTPS is strongly recommended for security (HTTP is still supported)
  • Must not use internal/private IP addresses

Process Overview

┌─────────────┐         ┌──────────────┐         ┌─────────────┐
│   Event     │         │   YCloud     │         │    Your     │
│  Occurs     │────────▶│   Webhook    │────────▶│   Server    │
│             │         │   System     │         │             │
└─────────────┘         └──────────────┘         └─────────────┘
                              │                         │
                              │    ◀────────────────────┘
                              │    Return 200 OK
                              │
                        ┌─────▼──────┐
                        │  Success   │
                        │  or Retry  │
                        └────────────┘

Flow:

  1. An event occurs on YCloud (e.g., inbound message received, status updated)
  2. YCloud sends an HTTP POST request to your webhook URL
  3. Your server receives and processes the event
  4. Your server responds with a 2xx status code
  5. If the request fails, YCloud will automatically retry

Implement Webhook

Step 1: Create a Webhook Endpoint

Use our API to create a webhook endpoint:

Example Request:

curl -X POST "https://api.ycloud.com/v2/webhookEndpoints" \
  -H "Content-Type: application/json" \
  -H "X-API-Key: YOUR_API_KEY" \
  -d '{
    "url": "https://api.yourcompany.com/webhook",
    "description": "Production webhook endpoint",
    "enabledEvents": [
      "whatsapp.message.updated"
    ],
    "status": "active"
  }'

Response:

{
  "id": "wep_1234567890abcdef",
  "url": "https://api.yourcompany.com/webhook",
  "description": "Production webhook endpoint",
  "enabledEvents": [
    "whatsapp.message.updated"
  ],
  "status": "active",
  "secret": "whsec_a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6",
  "createTime": "2025-11-03T10:00:00.000+08:00",
  "updateTime": "2025-11-03T10:00:00.000+08:00"
}

Important: Save the secret value securely. You'll need it to verify webhook signatures.

For endpoint api detail, please refer to Webhook Endpoint.

Endpoint Limitations:

  • Maximum 20 webhook endpoints per account
  • URL must be publicly accessible (no internal IPs)
  • URL maximum length: 500 characters
  • Description maximum length: 400 characters

Step 2: Handle Webhook Requests

When an event occurs, YCloud will send a POST request to your webhook URL:

Request Headers:

Content-Type: application/json
YCloud-Signature: t=1762224357,s=a1b2c3d4e5f6...
X-Webhook-Endpoint-ID: 6c31924eae964fafbf0c19efc20d6f4d

Request Body Example:

{
  "id": "evt_1234567890abcdef",
  "type": "whatsapp.message.updated",
  "apiVersion": "v2",
  "createTime": "2025-11-03T10:00:00.000+08:00",
  "whatsappMessage": {
    "id": "wamid.1234567890",
    "wabaId": "123456789",
    "from": "+8613800138000",
    "to": "+8613900139000",
    "type": "text",
    "status": "delivered",
    "text": {
      "body": "Hello, World!"
    },
    "createTime": "2025-11-03T09:59:00.000+08:00",
    "updateTime": "2025-11-03T10:00:00.000+08:00"
  }
}

Event Common Structure:

  • id: Unique event identifier
  • type: Event type (e.g., whatsapp.message.updated)
  • apiVersion: API version (currently v2)
  • createTime: Event creation timestamp
  • {eventType}: Event-specific data (e.g., whatsappMessage, smsMessage)

For the event-specific data, please refer to Event Types & Payloads.

Step 3: Verify Webhook Signatures

Always verify the signature to ensure the request is from YCloud and hasn't been tampered with.

Signature Format:

YCloud-Signature: t={timestamp},s={signature}

Verification Algorithm:

  1. Extract the timestamp (t) and signature (s) from the header (the timestamp is unix timestamp in seconds)
  2. Construct the signed payload: {timestamp}.{request_body}
  3. Compute HMAC-SHA256: HMAC-SHA256(signed_payload, secret)
  4. Compare the computed signature with the received signature

Example (Pseudocode):

function verifySignature(payload, signatureHeader, secret) {
  // Parse the header
  const parts = signatureHeader.split(',');
  const timestamp = parts[0].split('=')[1];
  const signature = parts[1].split('=')[1];
  
  // Construct signed payload
  const signedPayload = `${timestamp}.${payload}`;
  
  // Compute expected signature
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  
  // Compare signatures (use constant-time comparison in production)
  return signature === expectedSignature;
}

Step 4: Respond to Webhooks

Response Requirements:

  1. Return a 2xx status code (e.g., 200, 201, 204)

    • Any non-2xx response will trigger a retry
  2. Respond quickly (within 6 seconds recommended)

    • Fast responses improve your webhook priority
    • Slow responses (>10 seconds) may be deprioritized
  3. Process asynchronously (recommended)

    • Return 200 OK immediately
    • Process the event in a background job/queue

Example Response:

HTTP/1.1 200 OK
Content-Type: application/json

{
  "received": true
}

Note: The response body is ignored. Only the status code matters.


Implementation Example

const express = require('express');
const crypto = require('crypto');

const app = express();
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET;

// Middleware to capture raw body for signature verification
app.use(express.json({
  verify: (req, res, buf) => {
    req.rawBody = buf.toString('utf8');
  }
}));

// Webhook endpoint
app.post('/webhook', (req, res) => {
  const signature = req.headers['ycloud-signature'];
  const payload = req.rawBody;
  
  // Verify signature
  if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
    console.error('Invalid signature');
    return res.status(401).send('Unauthorized');
  }
  
  // Process event
  const event = req.body;
  console.log('Received event:', event.type, event.id);
  
  // Handle different event types
  switch (event.type) {
    case 'whatsapp.message.updated':
      handleWhatsAppMessage(event.whatsappMessage);
      break;
    case 'whatsapp.inbound.message':
      handleInboundMessage(event.whatsappInboundMessage);
      break;
    default:
      console.log('Unhandled event type:', event.type);
  }
  
  // Respond immediately
  res.status(200).json({ received: true });
});

function verifySignature(payload, signatureHeader, secret) {
  if (!signatureHeader) return false;
  
  const parts = signatureHeader.split(',');
  const timestamp = parts[0].split('=')[1];
  const signature = parts[1].split('=')[1];
  
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  
  // Use constant-time comparison
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}

function handleWhatsAppMessage(message) {
  // Process message asynchronously
  console.log('WhatsApp message status:', message.status);
  // Add to queue, update database, etc.
}

function handleInboundMessage(message) {
  // Process inbound message
  console.log('Received message from:', message.from);
  // Add to queue, trigger auto-reply, etc.
}

app.listen(3000, () => {
  console.log('Webhook server listening on port 3000');
});
import org.springframework.web.bind.annotation.*;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;

@RestController
public class WebhookController {
    
    private static final String WEBHOOK_SECRET = "<YOUR_WEBHOOK_SECRET>";
    
    @PostMapping("/webhook")
    public ResponseEntity<Map<String, Boolean>> handleWebhook(
            @RequestHeader("YCloud-Signature") String signature,
            @RequestBody String payload) {
        
        // Verify signature
        if (!verifySignature(payload, signature, WEBHOOK_SECRET)) {
            return ResponseEntity.status(401).build();
        }
        
        // Parse event
        ObjectMapper mapper = new ObjectMapper();
        JsonNode event = mapper.readTree(payload);
        String eventType = event.get("type").asText();
        String eventId = event.get("id").asText();
        
        System.out.println("Received event: " + eventType + " - " + eventId);
        
        // Handle different event types
        switch (eventType) {
            case "whatsapp.message.updated":
                handleWhatsAppMessage(event.get("whatsappMessage"));
                break;
            case "whatsapp.inbound.message":
                handleInboundMessage(event.get("whatsappInboundMessage"));
                break;
            default:
                System.out.println("Unhandled event type: " + eventType);
        }
        
        // Respond immediately
        return ResponseEntity.ok(Map.of("received", true));
    }
    
    private boolean verifySignature(String payload, String signatureHeader, String secret) {
        try {
            // Parse header
            String[] parts = signatureHeader.split(",");
            String timestamp = parts[0].split("=")[1];
            String signature = parts[1].split("=")[1];
            
            // Compute expected signature
            String signedPayload = timestamp + "." + payload;
            Mac mac = Mac.getInstance("HmacSHA256");
            SecretKeySpec secretKey = new SecretKeySpec(
                secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256");
            mac.init(secretKey);
            byte[] hash = mac.doFinal(signedPayload.getBytes(StandardCharsets.UTF_8));
            
            String expectedSignature = bytesToHex(hash);
            
            // Constant-time comparison
            return MessageDigest.isEqual(
                signature.getBytes(StandardCharsets.UTF_8),
                expectedSignature.getBytes(StandardCharsets.UTF_8)
            );
        } catch (Exception e) {
            return false;
        }
    }
    
    private String bytesToHex(byte[] bytes) {
        StringBuilder result = new StringBuilder();
        for (byte b : bytes) {
            result.append(String.format("%02x", b));
        }
        return result.toString();
    }
    
    private void handleWhatsAppMessage(JsonNode message) {
        // Process message asynchronously
        System.out.println("WhatsApp message status: " + message.get("status").asText());
        // Add to queue, update database, etc.
    }
    
    private void handleInboundMessage(JsonNode message) {
        // Process inbound message
        System.out.println("Received message from: " + message.get("from").asText());
        // Add to queue, trigger auto-reply, etc.
    }
}

Important Notes

Best Practices

  1. Verify signatures - Always verify the YCloud-Signature header to ensure requests are from YCloud

  2. Use HTTPS - Recommended for encrypting data in transit and preventing attacks

  3. Respond quickly - Return 200 OK within 6 seconds for optimal delivery priority

  4. Handle idempotency - Use event IDs to detect and skip duplicate events

  5. Process asynchronously - Return 200 OK immediately, then process events in background workers

Idempotency Example :

const Redis = require('ioredis');
const redis = new Redis();

app.post('/webhook', async (req, res) => {
  const event = req.body;
  const eventId = event.id;
  
  // Check if already processed (TTL: 7 days)
  const exists = await redis.get(`webhook:processed:${eventId}`);
  if (exists) {
    console.log('Duplicate event, skipping:', eventId);
    return res.status(200).json({ received: true });
  }
  
  // Mark as processed
  await redis.setex(`webhook:processed:${eventId}`, 7 * 24 * 3600, '1');
  
  // Process event asynchronously
  await queue.add('process-webhook', event);
  
  res.status(200).json({ received: true });
});
@Autowired
private RedisTemplate<String, String> redisTemplate;

@PostMapping("/webhook")
public ResponseEntity<Map<String, Boolean>> handleWebhook(@RequestBody String payload) {
    JsonNode event = objectMapper.readTree(payload);
    String eventId = event.get("id").asText();
    
    // Check if already processed
    String key = "webhook:processed:" + eventId;
    Boolean exists = redisTemplate.hasKey(key);
    if (Boolean.TRUE.equals(exists)) {
        logger.info("Duplicate event, skipping: {}", eventId);
        return ResponseEntity.ok(Map.of("received", true));
    }
    
    // Mark as processed (TTL: 7 days)
    redisTemplate.opsForValue().set(key, "1", 7, TimeUnit.DAYS);
    
    // Process event asynchronously
    eventQueue.send(event);
    
    return ResponseEntity.ok(Map.of("received", true));
}

Error Handling

Retry Mechanism:

If your server returns a non-2xx status code or doesn't respond, YCloud will automatically retry:

  • Retry schedule: 10s → 30s → 5m → 30m → 1h → 2h → 2h
  • Maximum retries: 7 attempts
  • After 7 failures: No more retries for that event

URL Suspension:

To protect system resources, URLs that fail frequently will be temporarily suspended:

  • Trigger: 200 failures per minute OR 10 minutes of cumulative failure time per minute
  • Suspension duration: 3 minutes
  • During suspension: No webhook requests will be sent
  • After suspension: Automatic resume

Best Practices:

  • Monitor your webhook endpoint's uptime
  • Set up alerts for webhook failures
  • Return 200 OK even if processing fails (handle retries internally)

Additional Resources