Dusty Labs Shared Services API
Base URL: https://dustylabs.dustinwells.com
All endpoints require an x-api-key header for authentication. Each site has a unique API key stored in its Vercel env vars.
Authentication
Every request must include:
Headers: Content-Type: application/json x-api-key: <DUSTYLABS_API_KEY> # from env var Origin: <your-site-origin> # validated against allowed origins
Env vars available in each consuming project (set via Vercel):
DUSTYLABS_API_KEY— site-specific API keyDUSTYLABS_SITE_ID— site identifier (e.g. "attackboom", "n8clarity")DUSTYLABS_API_URL—https://dustylabs.dustinwells.com
Newsletter Service
POST /api/newsletter — Subscribe
Subscribes an email address to the newsletter for a given site.
Request Body
{
"email": "user@example.com", // required, valid email
"siteId": "my-site" // required, must match API key's site_id
}Response (200)
{
"success": true,
"message": "Successfully subscribed!",
"subscriber": {
"id": "uuid",
"email": "user@example.com",
"status": "active"
}
}Error Responses
400: { "success": false, "message": "Valid email address is required" }
400: { "success": false, "message": "siteId is required" }
401: { "success": false, "message": "Invalid or inactive API key" }
403: { "success": false, "message": "Origin not allowed" }
405: { "success": false, "message": "Method not allowed" }
429: { "success": false, "message": "Too many requests. Please try again later." }Duplicate handling: If the email is already subscribed and active, returns { "success": true, "message": "You're already subscribed!" }. If previously unsubscribed, resubscribes them.
POST /api/newsletter/unsubscribe — Unsubscribe
Unsubscribes an email address.
Request Body
{
"email": "user@example.com",
"siteId": "my-site"
}Response (200)
{
"success": true,
"message": "You have been unsubscribed."
}Forms Service
POST /api/forms — Submit Form
Submits a form entry. Stores the submission and optionally sends notification emails.
Request Body
{
"formId": "contact-form", // required, identifies the form
"values": { // required, form field values
"name": "Jane Doe",
"email": "jane@example.com",
"message": "Hello!"
},
"metadata": { // optional, extra context
"page": "/contact",
"referrer": "google.com"
}
}Response (200)
{
"success": true,
"message": "Form submitted successfully!",
"submissionId": "uuid"
}Error Responses
400: { "success": false, "message": "formId is required" }
400: { "success": false, "message": "values is required" }
401: { "success": false, "message": "Invalid or inactive API key" }
403: { "success": false, "message": "Origin not allowed" }
429: { "success": false, "message": "Too many requests. Please try again later." }Integration Examples
React Component (Newsletter)
The simplest integration — a self-contained form component. No npm package install needed.
"use client";
import { useState, FormEvent } from "react";
export function NewsletterSignup() {
const [email, setEmail] = useState("");
const [state, setState] = useState<"idle" | "loading" | "success" | "error">("idle");
const [message, setMessage] = useState("");
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setState("loading");
try {
const res = await fetch(process.env.NEXT_PUBLIC_DUSTYLABS_API_URL + "/api/newsletter", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.NEXT_PUBLIC_DUSTYLABS_API_KEY!,
},
body: JSON.stringify({
email,
siteId: process.env.NEXT_PUBLIC_DUSTYLABS_SITE_ID,
}),
});
const data = await res.json();
setState(data.success ? "success" : "error");
setMessage(data.message);
if (data.success) setEmail("");
} catch {
setState("error");
setMessage("Something went wrong. Please try again.");
}
}
if (state === "success") return <p>{message}</p>;
return (
<form onSubmit={handleSubmit}>
<input
type="email"
value={email}
onChange={(e) => setEmail(e.target.value)}
placeholder="Enter your email"
required
disabled={state === "loading"}
/>
<button type="submit" disabled={state === "loading"}>
{state === "loading" ? "Subscribing..." : "Subscribe"}
</button>
{state === "error" && <p style={{ color: "red" }}>{message}</p>}
</form>
);
}React Component (Contact Form)
"use client";
import { useState, FormEvent } from "react";
export function ContactForm() {
const [state, setState] = useState<"idle" | "loading" | "success" | "error">("idle");
const [message, setMessage] = useState("");
async function handleSubmit(e: FormEvent) {
e.preventDefault();
setState("loading");
const form = e.target as HTMLFormElement;
const formData = new FormData(form);
try {
const res = await fetch(process.env.NEXT_PUBLIC_DUSTYLABS_API_URL + "/api/forms", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.NEXT_PUBLIC_DUSTYLABS_API_KEY!,
},
body: JSON.stringify({
formId: "contact-form",
values: Object.fromEntries(formData),
metadata: { page: window.location.pathname },
}),
});
const data = await res.json();
setState(data.success ? "success" : "error");
setMessage(data.message);
if (data.success) form.reset();
} catch {
setState("error");
setMessage("Something went wrong. Please try again.");
}
}
if (state === "success") return <p>{message}</p>;
return (
<form onSubmit={handleSubmit}>
<input name="name" placeholder="Name" required />
<input name="email" type="email" placeholder="Email" required />
<textarea name="message" placeholder="Message" required />
<button type="submit" disabled={state === "loading"}>
{state === "loading" ? "Sending..." : "Send"}
</button>
{state === "error" && <p style={{ color: "red" }}>{message}</p>}
</form>
);
}Server-Side / API Route (Next.js)
If you prefer to keep the API key server-side (recommended for production):
// app/api/subscribe/route.ts
export async function POST(request: Request) {
const { email } = await request.json();
const res = await fetch(process.env.DUSTYLABS_API_URL + "/api/newsletter", {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": process.env.DUSTYLABS_API_KEY!,
},
body: JSON.stringify({
email,
siteId: process.env.DUSTYLABS_SITE_ID,
}),
});
const data = await res.json();
return Response.json(data, { status: res.status });
}Environment Variables
These are pre-configured in each site's Vercel project. For client-side usage, prefix with NEXT_PUBLIC_.
| Variable | Description |
|---|---|
DUSTYLABS_API_KEY | Site-specific API key (server-side only) |
DUSTYLABS_SITE_ID | Site identifier (e.g. "attackboom", "n8clarity") |
DUSTYLABS_API_URL | https://dustylabs.dustinwells.com |
Important: If using the API key client-side (in a React component with NEXT_PUBLIC_ prefix), the key is visible to users. This is OK because the API validates the Origin header — requests from unauthorized origins are rejected regardless of the key.
Rate Limiting
Default: 10 requests per minute per email+site combination. Returns 429 when exceeded.
Registered Sites
| Site ID | Origin | Vercel Project |
|---|---|---|
attackboom | https://www.attackboom.com | attackboom-www |
connectioncards-shop | https://shop.connectioncards.app | connectioncards-shop |
n8clarity | https://app.n8clarity.com | n8clarity |
launchpad-forum | https://www.launchpadforum.com | launchpad-forum-site |
8750-studio | https://www.8750.studio | 8750-studio-www |
Adding a New Site
From the Dusty Labs monorepo:
# 1. Generate API key pnpm --filter @dustylabs/api generate-api-key <site-id> <allowed-origin> # 2. Set env vars on the Vercel project vercel link --project <vercel-project-name> echo "<api-key>" | vercel env add DUSTYLABS_API_KEY production --force echo "<site-id>" | vercel env add DUSTYLABS_SITE_ID production --force echo "https://dustylabs.dustinwells.com" | vercel env add DUSTYLABS_API_URL production --force # Repeat for preview and development environments
Dusty Labs Shared Services — managed from the dusty-labs-tools-components monorepo.