From hudu
Manages Hudu website records for SSL/TLS monitoring, email security (DMARC, DKIM, SPF), DNS records, and company linking. Covers CRUD, monitoring fields, and verification patterns.
npx claudepluginhub wyre-technology/msp-claude-plugins --plugin huduThis skill uses the workspace's default tool permissions.
Websites in Hudu represent website records associated with client companies. Beyond basic URL tracking, Hudu provides monitoring for SSL/TLS certificates and email security standards (DMARC, DKIM, SPF). MSPs use website records to track client web properties, monitor certificate expiration, and verify email authentication configuration.
Fetches up-to-date documentation from Context7 for libraries and frameworks like React, Next.js, Prisma. Use for setup questions, API references, and code examples.
Retrieves current documentation, API references, and code examples for libraries, frameworks, SDKs, CLIs, and services via Context7 CLI. Ideal for API syntax, configs, migrations, and setup queries.
Uses ctx7 CLI to fetch current library docs, manage AI coding skills (install/search/generate), and configure Context7 MCP for AI editors.
Websites in Hudu represent website records associated with client companies. Beyond basic URL tracking, Hudu provides monitoring for SSL/TLS certificates and email security standards (DMARC, DKIM, SPF). MSPs use website records to track client web properties, monitor certificate expiration, and verify email authentication configuration.
Hudu can automatically monitor websites for:
| Monitoring Area | Description |
|---|---|
| SSL/TLS | Certificate validity, expiration date, issuer |
| DMARC | Domain-based Message Authentication, Reporting & Conformance |
| DKIM | DomainKeys Identified Mail |
| SPF | Sender Policy Framework |
| HTTP Status | Whether the site is reachable |
The three email security standards that Hudu tracks:
| Standard | Purpose | DNS Record Type |
|---|---|---|
| SPF | Specifies authorized mail servers | TXT record on domain |
| DKIM | Cryptographic email authentication | TXT record on selector._domainkey |
| DMARC | Policy for handling failed SPF/DKIM | TXT record on _dmarc subdomain |
Each website record is linked to a company. A company can have multiple website records (e.g., primary domain, marketing site, client portal).
| Field | Type | Required | Description |
|---|---|---|---|
id | integer | System | Auto-generated unique identifier |
company_id | integer | Yes | Parent company |
name | string | Yes | Website display name / URL |
slug | string | System | URL-friendly identifier |
notes | string | No | Additional notes |
paused | boolean | No | Whether monitoring is paused |
disable_dns | boolean | No | Disable DNS checks |
disable_ssl | boolean | No | Disable SSL checks |
disable_whois | boolean | No | Disable WHOIS checks |
| Field | Type | Description |
|---|---|---|
url | string | The monitored URL |
monitoring_status | string | Current monitoring status |
| Field | Type | Description |
|---|---|---|
ssl_status | string | SSL certificate status |
ssl_expiration | datetime | SSL certificate expiration date |
ssl_issuer | string | SSL certificate issuer |
| Field | Type | Description |
|---|---|---|
dmarc_status | string | DMARC record status |
dmarc_policy | string | DMARC policy (none, quarantine, reject) |
dkim_status | string | DKIM record status |
spf_status | string | SPF record status |
spf_record | string | SPF record value |
| Field | Type | Description |
|---|---|---|
dns_a_records | array | A record values |
dns_mx_records | array | MX record values |
dns_ns_records | array | NS record values |
| Field | Type | Description |
|---|---|---|
created_at | datetime | Creation timestamp |
updated_at | datetime | Last update timestamp |
object_type | string | Always "Website" |
company_name | string | Parent company name (read-only) |
GET /api/v1/websites
x-api-key: YOUR_API_KEY
Content-Type: application/json
By Company:
GET /api/v1/websites?company_id=123
By Name:
GET /api/v1/websites?name=acme.com
With Pagination:
GET /api/v1/websites?page=1
GET /api/v1/websites/456
x-api-key: YOUR_API_KEY
Response:
{
"website": {
"id": 456,
"company_id": 123,
"company_name": "Acme Corporation",
"name": "acme.com",
"url": "https://www.acme.com",
"notes": "Primary company website",
"paused": false,
"disable_dns": false,
"disable_ssl": false,
"disable_whois": false,
"monitoring_status": "up",
"ssl_status": "valid",
"ssl_expiration": "2026-08-15T00:00:00.000Z",
"ssl_issuer": "Let's Encrypt Authority X3",
"dmarc_status": "pass",
"dmarc_policy": "reject",
"dkim_status": "pass",
"spf_status": "pass",
"spf_record": "v=spf1 include:_spf.google.com include:spf.protection.outlook.com ~all",
"dns_a_records": ["203.0.113.10"],
"dns_mx_records": ["aspmx.l.google.com"],
"dns_ns_records": ["ns1.example.com", "ns2.example.com"],
"created_at": "2024-03-15T10:30:00.000Z",
"updated_at": "2026-02-20T08:00:00.000Z"
}
}
POST /api/v1/websites
Content-Type: application/json
x-api-key: YOUR_API_KEY
{
"website": {
"name": "acme.com",
"company_id": 123,
"notes": "Primary company website. Hosted on AWS.",
"paused": false,
"disable_dns": false,
"disable_ssl": false,
"disable_whois": false
}
}
PUT /api/v1/websites/456
Content-Type: application/json
x-api-key: YOUR_API_KEY
{
"website": {
"notes": "Primary company website. Migrated to Azure on 2026-02-15.",
"paused": false
}
}
DELETE /api/v1/websites/456
x-api-key: YOUR_API_KEY
async function getExpiringSslCerts(daysAhead = 30) {
const today = new Date();
const futureDate = new Date();
futureDate.setDate(futureDate.getDate() + daysAhead);
const websites = await fetchAllWebsites();
return websites
.filter(w => {
if (!w.ssl_expiration || w.paused) return false;
const expiry = new Date(w.ssl_expiration);
return expiry >= today && expiry <= futureDate;
})
.map(w => ({
name: w.name,
company: w.company_name,
sslExpiration: w.ssl_expiration,
daysRemaining: Math.ceil(
(new Date(w.ssl_expiration) - today) / (1000 * 60 * 60 * 24)
),
issuer: w.ssl_issuer
}))
.sort((a, b) => a.daysRemaining - b.daysRemaining);
}
async function emailSecurityAudit(companyId) {
const websites = await fetchWebsites({ company_id: companyId });
return websites.map(w => ({
domain: w.name,
spf: {
status: w.spf_status || 'Not checked',
record: w.spf_record || 'Not found'
},
dkim: {
status: w.dkim_status || 'Not checked'
},
dmarc: {
status: w.dmarc_status || 'Not checked',
policy: w.dmarc_policy || 'Not set'
},
overallScore: calculateEmailSecurityScore(w)
}));
}
function calculateEmailSecurityScore(website) {
let score = 0;
if (website.spf_status === 'pass') score += 33;
if (website.dkim_status === 'pass') score += 33;
if (website.dmarc_status === 'pass') score += 34;
if (website.dmarc_policy === 'reject') score += 10; // bonus for strict policy
return Math.min(score, 100);
}
async function generateWebsiteReport(companyId) {
const websites = await fetchWebsites({ company_id: companyId });
return {
total: websites.length,
monitored: websites.filter(w => !w.paused).length,
paused: websites.filter(w => w.paused).length,
sslValid: websites.filter(w => w.ssl_status === 'valid').length,
sslExpiring: websites.filter(w => {
if (!w.ssl_expiration) return false;
const daysLeft = Math.ceil(
(new Date(w.ssl_expiration) - new Date()) / (1000 * 60 * 60 * 24)
);
return daysLeft <= 30;
}).length,
emailSecurityComplete: websites.filter(w =>
w.spf_status === 'pass' &&
w.dkim_status === 'pass' &&
w.dmarc_status === 'pass'
).length,
websites: websites.map(w => ({
name: w.name,
ssl: w.ssl_status,
sslExpiry: w.ssl_expiration,
spf: w.spf_status,
dkim: w.dkim_status,
dmarc: w.dmarc_status
}))
};
}
async function onboardClientDomains(companyId, domains) {
const results = [];
for (const domain of domains) {
const website = await createWebsite({
name: domain.name,
company_id: companyId,
notes: domain.notes || `Added during onboarding on ${new Date().toLocaleDateString()}`,
paused: false
});
results.push(website);
}
return results;
}
| Code | Message | Resolution |
|---|---|---|
| 400 | Name can't be blank | Provide website name/domain |
| 400 | Company is required | Include company_id |
| 401 | Invalid API key | Check HUDU_API_KEY |
| 404 | Website not found | Verify website ID |
| 422 | Validation failed | Check required fields |
| Error | Cause | Fix |
|---|---|---|
| Name required | Missing name | Add domain name to request |
| Company required | No company_id | Include company_id |
| Invalid company | Bad company_id | Query /companies first |
| Duplicate domain | Domain already tracked | Check existing websites first |
async function safeCreateWebsite(data) {
try {
return await createWebsite(data);
} catch (error) {
if (error.status === 422 && error.message?.includes('already')) {
// Website already exists - find and return it
const existing = await fetchWebsites({
company_id: data.company_id,
name: data.name
});
return existing[0];
}
throw error;
}
}