Overview
The Cosmos+ Webhook API lets you receive real-time event notifications directly to any HTTPS endpoint you control. When a relevant event occurs — a moderation action, a giveaway ending, a user gaining XP — Cosmos+ sends a signed HTTP POST to your URL.
You define the path. Use https://your-server.com/api/cosmos, https://worker.example.com/events/any-path, or any public HTTPS URL of your choosing. The path is entirely yours.
Quickstart
1. Create an App — Navigate to My Apps and click New App. Enter a name and your HTTPS endpoint URL.
2. Save your Secret — After creating the app, your Secret Key is shown once. Copy it immediately and store it securely as an environment variable. It cannot be retrieved again.
3. Subscribe to Events — Expand your app and select the events you want to receive. Guild-level events require Administrator permissions in the target server.
4. Test your integration — Use the Test button to send a ping event and verify your endpoint validates the signature correctly.
Payload Structure
All webhooks share the same JSON envelope. Event-specific data is inside the data field.
{
"event": "guild.moderation.banned",
"app_id": "3f4c8b2a-...",
"timestamp": 1718000000,
"data": {
"guild_id": "123456789012345678",
"user_id": "987654321098765432",
"moderator_id": "111222333444555666",
"reason": "Repeated spam"
}
}
Signature Verification
Each request includes the X-Cosmos-Signature header — an HMAC-SHA256 hex digest. Compute the expected signature over the raw request body bytes and compare using a constant-time comparison function to prevent timing attacks.
HMAC-SHA256(key = YOUR_SECRET, message = RAW_REQUEST_BODY_BYTES)
The result is a lowercase hex string. Compare it against the value of X-Cosmos-Signature using a constant-time equals function.
Anti-Replay Protection
Each request includes X-Cosmos-Timestamp (Unix seconds). Reject any request whose timestamp differs from your current time by more than 5 minutes to prevent replay attacks.
import time
ts = int(request.headers['X-Cosmos-Timestamp'])
if abs(time.time() - ts) > 300:
return "Timestamp out of acceptable range", 401
Event Reference
User Events
These fire only for the owner of the app. They cannot be used to monitor other users.
| Event | Description | Key Fields |
|---|---|---|
| user.economy.daily_claimed | App owner claimed their daily reward. | amount_claimed, new_balance |
| user.economy.transfer_received | Currency was transferred to the app owner. | sender_id, amount, new_balance |
| user.economy.transfer_sent | App owner sent currency to someone. | recipient_id, amount |
| user.social.rep_received | App owner received a reputation point. | giver_id, total_rep |
| user.social.marriage_proposed | Someone proposed marriage to the app owner. | proposer_id, message_id |
| user.premium.purchased | App owner acquired Cosmos+ Premium. | plan, expires_at |
| user.giveaway.entered | App owner entered a giveaway. | guild_id, message_id, prize |
| user.giveaway.won | App owner won a giveaway. | guild_id, message_id, prize |
| user.level.up_global | App owner leveled up globally. | new_level, total_xp |
| user.roblox.account_linked | App owner linked their Roblox account. | roblox_id, roblox_username |
Guild / Server Events
Require Administrator or Owner permissions in the target server. You select which server when subscribing to each event.
| Event | Description | Key Fields |
|---|---|---|
| guild.moderation.banned | A user was banned via Cosmos+. | user_id, moderator_id, reason |
| guild.moderation.kicked | A user was kicked via Cosmos+. | user_id, moderator_id, reason |
| guild.moderation.timeout_added | A timeout was applied to a user. | user_id, moderator_id, duration_seconds |
| guild.moderation.warn_added | A warning was issued to a user. | user_id, moderator_id, reason, warn_id |
| guild.automod.triggered | AutoMod detected and acted on a message. | user_id, rule_broken, action_taken |
| guild.antilink.triggered | Antilink deleted a prohibited URL from a message. | user_id, channel_id, link |
| guild.triggerword.triggered | A trigger word was matched in a message. | user_id, word, channel_id |
| guild.giveaway.created | A new giveaway was started in the server. | message_id, prize, end_time, creator_id |
| guild.giveaway.ended | A giveaway ended and winners were drawn. | message_id, prize, winners |
| guild.suggestion.created | A suggestion was submitted. | author_id, content, message_id |
| guild.suggestion.approved | A suggestion was approved by admins. | message_id, approver_id |
| guild.confession.submitted | An anonymous confession was posted. | confession_id, channel_id |
| guild.apply.created | A user submitted an application form. | form_id, response_id, user_id |
| guild.member.invited | A new member joined via a tracked invite. | user_id, inviter_id, invite_code |
| guild.boost.added | The server received a Discord Boost. | user_id, total_boosts |
| guild.level_role.awarded | A member leveled up and received a role. | user_id, new_level, role_id |
| guild.stats.milestone_reached | The server hit a member count milestone. | milestone, member_count |
Request Headers
| Header | Description |
|---|---|
X-Cosmos-Signature | HMAC-SHA256 hex digest of the raw JSON body. |
X-Cosmos-Event | The event type string, e.g. guild.moderation.banned. |
X-Cosmos-Timestamp | Unix timestamp (seconds) of dispatch time. |
Content-Type | Always application/json. |
User-Agent | CosmosWebhooks/1.0 |
Errors & Retries
Cosmos+ retries on 5xx responses up to 2 additional times with exponential backoff (1s, then 2s). Client errors (4xx) are not retried. Each request has a hard timeout of 5 seconds.
If your endpoint fails 10 consecutive times, the app is automatically suspended. You can re-enable it from your app settings. Always return a 2xx status as fast as possible — offload any heavy processing to a background queue on your side.
Rate Limits
| Limit | Value | Notes |
|---|---|---|
| Daily deliveries | 70 per app | Resets at midnight UTC. |
| Monthly deliveries | 2,000 per app | Resets on the 1st of each month. |
| Max apps per account | 2 | Contact support for higher limits. |
| Max subscriptions per app | 50 | Across all events and guilds combined. |
Node.js / Express
const express = require('express');
const crypto = require('crypto');
const app = express();
const SECRET = process.env.COSMOS_SECRET;
app.use(express.json({
verify: (req, _res, buf) => { req.rawBody = buf; }
}));
// Use any path you want — /your-path, /api/events, anything.
app.post('/your-path', (req, res) => {
const signature = req.headers['x-cosmos-signature'];
const timestamp = parseInt(req.headers['x-cosmos-timestamp'], 10);
if (Math.abs(Date.now() / 1000 - timestamp) > 300) {
return res.status(401).json({ error: 'Timestamp out of acceptable range' });
}
const expected = crypto
.createHmac('sha256', SECRET)
.update(req.rawBody)
.digest('hex');
if (!crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected))) {
return res.status(401).json({ error: 'Invalid signature' });
}
const { event, data } = req.body;
console.log('Received:', event, data);
res.sendStatus(200);
});
app.listen(3000);
Python / FastAPI
import hmac, hashlib, time, os
from fastapi import FastAPI, Request, HTTPException
app = FastAPI()
SECRET = os.environ["COSMOS_SECRET"].encode()
# Use any path you want — /your-path, /api/events, anything.
@app.post("/your-path")
async def cosmos_webhook(request: Request):
body = await request.body()
signature = request.headers.get("x-cosmos-signature", "")
timestamp = int(request.headers.get("x-cosmos-timestamp", 0))
if abs(time.time() - timestamp) > 300:
raise HTTPException(401, "Timestamp out of acceptable range")
expected = hmac.new(SECRET, body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(signature, expected):
raise HTTPException(401, "Invalid signature")
payload = await request.json()
print(f"Event: {payload['event']}", payload["data"])
return {"ok": True}
Go / net/http
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"math"
"net/http"
"os"
"strconv"
"time"
)
var secret = []byte(os.Getenv("COSMOS_SECRET"))
// Use any path you want — /your-path, /api/events, anything.
func handler(w http.ResponseWriter, r *http.Request) {
body, _ := io.ReadAll(r.Body)
ts, _ := strconv.ParseInt(r.Header.Get("X-Cosmos-Timestamp"), 10, 64)
if math.Abs(float64(time.Now().Unix()-ts)) > 300 {
http.Error(w, "Timestamp out of range", 401)
return
}
mac := hmac.New(sha256.New, secret)
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
if !hmac.Equal([]byte(r.Header.Get("X-Cosmos-Signature")), []byte(expected)) {
http.Error(w, "Invalid signature", 401)
return
}
w.WriteHeader(200)
}
func main() {
http.HandleFunc("/your-path", handler)
http.ListenAndServe(":8080", nil)
}
Java / Spring Boot
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.util.HexFormat;
@RestController
public class CosmosWebhookController {
private final String secret = System.getenv("COSMOS_SECRET");
// Use any path you want — /your-path, /api/events, anything.
@PostMapping("/your-path")
public ResponseEntity<Void> handle(
@RequestHeader("X-Cosmos-Signature") String signature,
@RequestHeader("X-Cosmos-Timestamp") long timestamp,
@RequestBody byte[] body) throws Exception {
if (Math.abs(System.currentTimeMillis() / 1000L - timestamp) > 300) {
return ResponseEntity.status(401).build();
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(), "HmacSHA256"));
String expected = HexFormat.of().formatHex(mac.doFinal(body));
if (!MessageDigest.isEqual(signature.getBytes(), expected.getBytes())) {
return ResponseEntity.status(401).build();
}
return ResponseEntity.ok().build();
}
}