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:
- An event occurs on YCloud (e.g., inbound message received, status updated)
- YCloud sends an HTTP POST request to your webhook URL
- Your server receives and processes the event
- Your server responds with a
2xxstatus code - 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 identifiertype: Event type (e.g.,whatsapp.message.updated)apiVersion: API version (currentlyv2)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:
- Extract the timestamp (
t) and signature (s) from the header (the timestamp is unix timestamp in seconds) - Construct the signed payload:
{timestamp}.{request_body} - Compute HMAC-SHA256:
HMAC-SHA256(signed_payload, secret) - 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:
-
Return a
2xxstatus code (e.g.,200,201,204)- Any non-2xx response will trigger a retry
-
Respond quickly (within 6 seconds recommended)
- Fast responses improve your webhook priority
- Slow responses (>10 seconds) may be deprioritized
-
Process asynchronously (recommended)
- Return
200 OKimmediately - Process the event in a background job/queue
- Return
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
-
Verify signatures - Always verify the
YCloud-Signatureheader to ensure requests are from YCloud -
Use HTTPS - Recommended for encrypting data in transit and preventing attacks
-
Respond quickly - Return
200 OKwithin 6 seconds for optimal delivery priority -
Handle idempotency - Use event IDs to detect and skip duplicate events
-
Process asynchronously - Return
200 OKimmediately, 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 OKeven if processing fails (handle retries internally)
Additional Resources
- API Reference: Webhook API documentation
- Event Types Reference: Event types documentation
