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):


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_.

VariableDescription
DUSTYLABS_API_KEYSite-specific API key (server-side only)
DUSTYLABS_SITE_IDSite identifier (e.g. "attackboom", "n8clarity")
DUSTYLABS_API_URLhttps://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 IDOriginVercel Project
attackboomhttps://www.attackboom.comattackboom-www
connectioncards-shophttps://shop.connectioncards.appconnectioncards-shop
n8clarityhttps://app.n8clarity.comn8clarity
launchpad-forumhttps://www.launchpadforum.comlaunchpad-forum-site
8750-studiohttps://www.8750.studio8750-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.