/** AgentForms API client — thin wrapper over /api/v2/. */
import type {
ApiKey,
FieldDefinition,
Form,
FormsResponse,
KeysResponse,
Permission,
Submission,
SubmissionsResponse,
} from './types';
// ---------------------------------------------------------------------------
// Error class
// ---------------------------------------------------------------------------
export class AgentFormsError extends Error {
public readonly statusCode: number;
public readonly response: unknown;
constructor(message: string, statusCode: number = 0, response: unknown = null) {
super(message);
this.name = 'AgentFormsError';
this.statusCode = statusCode;
this.response = response;
}
}
// ---------------------------------------------------------------------------
// Base client internals
// ---------------------------------------------------------------------------
interface ClientInternals {
readonly apiKey: string;
readonly baseURL: string;
readonly timeout: number;
}
/**
* Make an HTTP request and parse the JSON response.
* Throws AgentFormsError on status >= 400.
*/
async function request(
internals: ClientInternals,
method: string,
path: string,
options?: {
params?: Record<string, string | number>;
body?: Record<string, unknown>;
},
): Promise<unknown> {
const url = new URL(`${internals.baseURL}/${path.replace(/^\//, '')}`);
if (options?.params) {
for (const [key, value] of Object.entries(options.params)) {
url.searchParams.append(key, String(value));
}
}
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), internals.timeout * 1000);
try {
const headers: Record<string, string> = {
Authorization: `Bearer ${internals.apiKey}`,
Accept: 'application/json',
};
const fetchOptions: RequestInit = {
method,
headers,
signal: controller.signal,
};
if (options?.body) {
headers['Content-Type'] = 'application/json';
fetchOptions.body = JSON.stringify(options.body);
}
const resp = await fetch(url.toString(), fetchOptions);
if (resp.status >= 400) {
let message: string;
let body: unknown;
try {
body = await resp.json();
message = (body as Record<string, unknown>).error as string ?? resp.statusText;
} catch {
body = await resp.text();
message = body as string;
}
throw new AgentFormsError(message, resp.status, body);
}
if (resp.status === 204) {
return {};
}
return resp.json() as Promise<unknown>;
} finally {
clearTimeout(timer);
}
}
/**
* Enrich a Form object with computed URL properties.
*/
function enrichForm(raw: Form): Form {
return {
...raw,
public_url: `https://agentforms.io/f/${raw.token}`,
share_url: `https://agentforms.io/${raw.token}`,
};
}
/**
* Enrich a list of Form objects.
*/
function enrichFormsResponse(raw: FormsResponse): FormsResponse {
return {
...raw,
forms: (raw.forms ?? []).map(enrichForm),
};
}
// ---------------------------------------------------------------------------
// FormsClient
// ---------------------------------------------------------------------------
export class FormsClient {
private readonly _c: ClientInternals;
constructor(client: ClientInternals) {
this._c = client;
}
/** List all forms. */
async list(limit: number = 50, offset: number = 0): Promise<FormsResponse> {
const result = await request(this._c, 'GET', '/forms', {
params: { limit, offset },
});
return enrichFormsResponse(result as FormsResponse);
}
/**
* Create a new form programmatically.
*
* @param name - Form display name
* @param fields - List of field definitions
* @param metadata - Optional JSON metadata attached to the form
*/
async create(
name: string,
fields: FieldDefinition[],
metadata?: Record<string, unknown>,
): Promise<Form> {
const body: Record<string, unknown> = { name, fields };
if (metadata) {
body.metadata = metadata;
}
const result = await request(this._c, 'POST', '/forms', { body });
return enrichForm(result as Form);
}
/**
* Generate a form from natural language using AI.
* Tier-gated (Starter+).
*
* @param prompt - Natural language description (e.g., "A survey for event feedback")
* @param formName - Override the AI-generated form name
*/
async generate(prompt: string, formName?: string): Promise<Form> {
const body: Record<string, unknown> = { prompt };
if (formName) {
body.form_name = formName;
}
const result = await request(this._c, 'POST', '/forms/generate', { body });
return enrichForm(result as Form);
}
/** Get form details by token. */
async get(token: string): Promise<Form> {
const result = await request(this._c, 'GET', `/forms/${token}`);
return enrichForm(result as Form);
}
/** Replace all fields on a form. */
async updateFields(token: string, fields: FieldDefinition[]): Promise<Record<string, unknown>> {
return request(
this._c,
'PUT',
`/forms/${token}/fields`,
{ body: { fields } },
) as Promise<Record<string, unknown>>;
}
/** Delete a form and all submissions. */
async delete(token: string): Promise<Record<string, unknown>> {
return request(this._c, 'DELETE', `/forms/${token}`) as Promise<Record<string, unknown>>;
}
/** Get public form config. */
async config(token: string): Promise<Record<string, unknown>> {
return request(this._c, 'GET', `/forms/${token}/config`) as Promise<Record<string, unknown>>;
}
}
// ---------------------------------------------------------------------------
// KeysClient
// ---------------------------------------------------------------------------
export class KeysClient {
private readonly _c: ClientInternals;
constructor(client: ClientInternals) {
this._c = client;
}
/** List all API keys. */
async list(): Promise<ApiKey[]> {
const result = (await request(this._c, 'GET', '/keys')) as KeysResponse;
return result.keys ?? [];
}
/**
* Create a new API key.
*
* @param name - Human-readable name
* @param permissions - List of permissions (defaults to all if omitted)
* @returns Key details including the full_key (returned once — store it securely)
*/
async create(name: string, permissions?: Permission[]): Promise<Record<string, unknown>> {
const body: Record<string, unknown> = { name };
if (permissions) {
const permObj: Record<string, boolean> = {};
for (const p of permissions) {
permObj[p] = true;
}
body.permissions = permObj;
}
return request(this._c, 'POST', '/keys', {
body,
}) as Promise<Record<string, unknown>>;
}
/** Revoke an API key. */
async revoke(keyId: number): Promise<Record<string, unknown>> {
return request(this._c, 'DELETE', `/keys/${keyId}`) as Promise<Record<string, unknown>>;
}
}
// ---------------------------------------------------------------------------
// SubmissionsClient
// ---------------------------------------------------------------------------
export class SubmissionsClient {
private readonly _c: ClientInternals;
constructor(client: ClientInternals) {
this._c = client;
}
/** List submissions for a form. */
async list(
formToken: string,
limit: number = 50,
offset: number = 0,
): Promise<SubmissionsResponse> {
return request(
this._c,
'GET',
`/forms/${formToken}/submissions`,
{ params: { limit, offset } },
) as Promise<SubmissionsResponse>;
}
/** Get a single submission. */
async get(formToken: string, submissionId: number): Promise<Submission> {
return request(
this._c,
'GET',
`/forms/${formToken}/submissions/${submissionId}`,
) as Promise<Submission>;
}
}
// ---------------------------------------------------------------------------
// Main client
// ---------------------------------------------------------------------------
/**
* Client for the AgentForms Agent API.
*
* @example
* ```ts
* import { AgentForms } from '@agentforms/sdk';
*
* const af = new AgentForms('afk_live_...');
*
* // Create a form
* const form = await af.forms.create('Customer Feedback', [
* { name: 'rating', label: 'Rating', type: 'select', options: ['1','2','3','4','5'], required: true },
* { name: 'feedback', label: 'Feedback', type: 'textarea' },
* ]);
* console.log(form.share_url);
* ```
*/
export class AgentForms {
public readonly forms: FormsClient;
public readonly keys: KeysClient;
public readonly submissions: SubmissionsClient;
/**
* @param apiKey - Your AgentForms API key
* @param baseURL - API base URL (default: https://agentforms.io/api/v2)
* @param timeout - Request timeout in seconds (default: 30)
*/
constructor(
public readonly apiKey: string,
public readonly baseURL: string = 'https://agentforms.io/api/v2',
public readonly timeout: number = 30,
) {
const internals: ClientInternals = {
apiKey,
baseURL: baseURL.replace(/\/+$/, ''),
timeout,
};
this.forms = new FormsClient(internals);
this.keys = new KeysClient(internals);
this.submissions = new SubmissionsClient(internals);
}
}