@alisamadiillc/agency-api
v0.1.3
Published
TypeScript client for the agency API — emails and uploads.
Maintainers
Readme
@alisamadiillc/agency-api
TypeScript client for the agency API — send emails and manage uploads with full type safety. Zero dependencies, works in Node 18+, browsers, and edge runtimes.
Install
npm install @alisamadiillc/agency-apiQuickstart
Create a shared client once:
// lib/agency.ts
import { AgencyClient } from "@alisamadiillc/agency-api";
export const agency = new AgencyClient(process.env.AGENCY_API_KEY!);
// custom base URL: new AgencyClient(key, { baseUrl: "http://localhost:8787" })Then import it anywhere:
import { agency } from "@/lib/agency";
const { data, error } = await agency.emails.send({
from: "[email protected]",
to: "[email protected]", // or ["[email protected]", "[email protected]"]
subject: "Welcome",
html: "<p>Hello!</p>",
});
if (error) {
console.error(error.code, error.message); // e.g. "RATE_LIMIT_EXCEEDED"
} else {
console.log(data.id);
}Every method returns { data, error } — it never throws for API or network failures.
Contact form
sendContact is made for public contact forms: the API renders the notification email (name, message, source, device, IP, a reply button) and delivers it to your account email — the caller never chooses the recipient, so a browser-exposed key can't be used to spam anyone. The visitor's address becomes the Reply-To.
const { data, error } = await agency.emails.sendContact({
name: "Jane Doe",
email: "[email protected]",
message: "Hi! I'd like to work with you.",
subject: "Project inquiry", // optional; defaults to "New contact from Jane Doe"
source: "My Site — Contact Form", // optional; defaults to the page Origin
metadata: {
// optional; any extra form fields your site collects —
Phone: "+1 555 010 1234", // rendered as a Details section in the email
Company: "Acme Inc",
Budget: "$5k–10k",
},
});Rate limited to 1 request per 10 minutes per IP — surface RATE_LIMIT_EXCEEDED (429) as a "please try again in a few minutes" message.
Uploads
One-call upload (presign + PUT handled for you):
const { data, error } = await agency.uploads.upload(file, {
path: "avatars",
naming: "uuid-filename", // "filename" | "uuid" | "uuid-filename"
});
if (data) console.log(data.publicUrl);Or do the two steps yourself:
const { data: presign } = await agency.uploads.presign({
filename: file.name,
contentType: file.type,
contentLength: file.size,
});
// Plain fetch — the signature is in the URL. Do NOT add an
// Authorization header here; it breaks the signature.
await fetch(presign!.uploadUrl, {
method: "PUT",
headers: presign!.headers,
body: file,
});List and delete:
const { data } = await agency.uploads.list({ prefix: "avatars/" });
// data.objects: [{ key, size, lastModified, url }], data.nextCursor for paging
await agency.uploads.delete({ key: "avatars/photo.jpg" });Who am I
const { data } = await agency.me.get();
// data.keyPrefix, data.user.email, data.user.bucketName, ...Errors
error is an AgencyError with:
| field | meaning |
| --- | --- |
| status | HTTP status (0 for network failures) |
| code | stable identifier, safe to branch on |
| message | human-readable, one line |
| causeHint | optional: why it happened / how to fix |
code is typed as AgencyErrorCode — a union of every code the API returns, so you get autocomplete when branching:
if (error.code === "FILE_ALREADY_EXISTS") { /* ... */ }Common codes: MISSING_API_KEY, UNKNOWN_API_KEY, API_KEY_REVOKED, VALIDATION_FAILED, RATE_LIMIT_EXCEEDED (contact form, 1 per 10 min per IP), SENDER_DOMAIN_MISMATCH, EMAIL_DOMAIN_NOT_CONFIGURED, BUCKET_NOT_CONFIGURED, FILE_ALREADY_EXISTS, OBJECT_NOT_FOUND, NETWORK_ERROR.
Limits
- Uploads: 50 MB max per file; presigned URLs expire after 15 minutes.
- Emails:
frommust be on your configured domain (send);sendContactis limited to 1 request per 10 minutes per IP.
