# Integration Examples

Source: https://docs.generalaugment.com/guides/integration-examples/
Description: Runnable-ish app backend, frontend, webhook, tool, approval, and smoke-test patterns for General Augment integrations.

General Augment is the agent backend for your app. These examples are for product and
app engineering teams proving a first integration without exposing project keys to
browser or mobile code.

The snippets use raw HTTP and `fetch` so they can be adapted to any backend. They do not
assume a published SDK. Keep `GENAUG_API_KEY` in server-side secret storage, pass a
stable opaque app user id on every turn, and store the returned response and trace IDs
for support.

> Note:

Browser and mobile clients should call your app backend. Your backend calls
`POST /v1/responses` with a project-scoped General Augment key.

## Environment

Use separate project-scoped keys for production, staging, development, and CI:

```bash
export GENAUG_API_BASE_URL="https://api.generalaugment.com"
export GENAUG_API_KEY="gaadmlive_project_key"
export GENAUG_PROJECT_ID="00000000-0000-0000-0000-000000000000"
```

During local contract tests, point the same app code at the local mock:

```bash
uv run --project packages/cli genaug mock --host 127.0.0.1 --port 8787 --quiet
export GENAUG_API_BASE_URL="http://127.0.0.1:8787"
export GENAUG_API_KEY="local-test"
```

## Integration Rules

- Browser and mobile clients call your backend, never General Augment directly.
- Your backend attaches the signed-in app user id as `user`.
- Use `simple`, `balanced`, or `complex` for `model` so project operators can change
provider models without app code changes.
- Send `metadata.feature` and `metadata.source` so usage, trace, and support evidence
is easy to filter.
- Use `X-Idempotency-Key` for retryable turns.
- Handle `429` rate limits and `402` budget or usage gates without tight retry loops.
- Keep app-owned OAuth credentials and risky side effects in your backend until
delegated tools, identity links, allowlists, and approval UX are ready.

## Server-Side Responses Call

Use this helper from trusted backend code. It extracts the first assistant
`output_text`, keeps the response id and trace id, and preserves machine-readable error
fields for app handling.

```ts
type GeneralAugmentTurnInput = {
  userId: string;
  message: string;
  feature: string;
  conversationId?: string;
};

export class GeneralAugmentError extends Error {
  status: number;
  reason?: string;
  retryAfter?: string | null;

  constructor(message: string, options: { status: number; reason?: string; retryAfter?: string | null }) {
    super(message);
    this.name = "GeneralAugmentError";
    this.status = options.status;
    this.reason = options.reason;
    this.retryAfter = options.retryAfter;
  }
}

export async function runGeneralAugmentTurn(input: GeneralAugmentTurnInput) {
  const baseUrl = process.env.GENAUG_API_BASE_URL ?? "https://api.generalaugment.com";
  const apiKey = process.env.GENAUG_API_KEY;

  if (!apiKey) {
    throw new Error("GENAUG_API_KEY must be configured on the server.");
  }

  const idempotencyKey = [
    "ga-turn",
    input.feature,
    input.userId,
    input.conversationId ?? "single",
    crypto.randomUUID(),
  ].join(":");

  const response = await fetch(`${baseUrl}/v1/responses`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "X-Idempotency-Key": idempotencyKey,
    },
    body: JSON.stringify({
      model: "balanced",
      user: input.userId,
      conversation: input.conversationId,
      input: input.message,
      stream: false,
      metadata: {
        feature: input.feature,
        source: "app-backend",
      },
    }),
  });

  const payload = await response.json().catch(() => ({}));

  if (!response.ok) {
    const reason = payload.reason ?? payload.detail?.reason;
    if (response.status === 429) {
      throw new GeneralAugmentError("General Augment rate limit reached.", {
        status: 429,
        reason,
        retryAfter: response.headers.get("Retry-After"),
      });
    }
    if (response.status === 402) {
      throw new GeneralAugmentError("General Augment budget or usage limit reached.", {
        status: 402,
        reason,
      });
    }
    throw new GeneralAugmentError(payload.message ?? payload.detail ?? "General Augment request failed.", {
      status: response.status,
      reason,
    });
  }

  return {
    text: extractOutputText(payload),
    responseId: payload.id,
    requestId: payload.metadata?.general_augment_request_id ?? payload.metadata?.request_id,
    traceId: payload.metadata?.general_augment_trace_id ?? payload.metadata?.trace_id,
    model: payload.metadata?.general_augment_model ?? payload.model,
    usage: payload.usage,
    costUsd: payload.metadata?.general_augment_cost_usd,
    raw: payload,
  };
}

function extractOutputText(payload: any): string {
  for (const item of payload.output ?? []) {
    for (const part of item.content ?? []) {
      if (part.type === "output_text") {
        return part.text ?? "";
      }
    }
  }
  return "";
}
```

Store `responseId`, `requestId`, `traceId`, `usage`, `model`, `costUsd`, the app user id,
and the idempotency key in your app logs. Those fields are the support bridge between a
user-visible issue and General Augment traces, usage, and audit surfaces.

## Next.js Route Handler Proxy

In Next.js App Router, expose your own route such as `POST /api/assistant`. The client
sends only the message and optional conversation id. The route reads the signed-in user
from your server session and calls General Augment with the server-side project key.

```ts
// app/api/assistant/route.ts

type SignedInUser = {
  id: string;
};

async function getSignedInUser(request: NextRequest): Promise<SignedInUser | null> {
  // Replace this with your app auth. Do not trust a user id from the browser body.
  const userId = request.headers.get("x-demo-user-id");
  return userId ? { id: userId } : null;
}

export async function POST(request: NextRequest) {
  const user = await getSignedInUser(request);
  if (!user) {
    return NextResponse.json({ error: "Unauthorized" }, { status: 401 });
  }

  const { message, conversationId } = await request.json();
  if (typeof message !== "string" || message.trim().length === 0) {
    return NextResponse.json({ error: "Message is required." }, { status: 400 });
  }

  const baseUrl = process.env.GENAUG_API_BASE_URL ?? "https://api.generalaugment.com";
  const apiKey = process.env.GENAUG_API_KEY;
  if (!apiKey) {
    return NextResponse.json({ error: "Server is missing General Augment configuration." }, { status: 500 });
  }

  const upstream = await fetch(`${baseUrl}/v1/responses`, {
    method: "POST",
    headers: {
      Authorization: `Bearer ${apiKey}`,
      "Content-Type": "application/json",
      "X-Idempotency-Key": `assistant:${user.id}:${conversationId ?? "new"}:${crypto.randomUUID()}`,
    },
    body: JSON.stringify({
      model: "balanced",
      user: user.id,
      conversation: conversationId,
      input: message,
      stream: false,
      metadata: {
        feature: "in-app-assistant",
        source: "nextjs-route",
      },
    }),
  });

  const payload = await upstream.json().catch(() => ({}));

  if (!upstream.ok) {
    return NextResponse.json(
      {
        error: payload.message ?? payload.detail ?? "Assistant request failed.",
        reason: payload.reason ?? payload.detail?.reason,
        retryAfter: upstream.headers.get("Retry-After"),
      },
      { status: upstream.status },
    );
  }

  return NextResponse.json({
    text: extractOutputText(payload),
    responseId: payload.id,
    traceId: payload.metadata?.general_augment_trace_id ?? payload.metadata?.trace_id,
    usage: payload.usage,
  });
}

function extractOutputText(payload: any): string {
  return (
    payload.output
      ?.flatMap((item: any) => item.content ?? [])
      ?.find((part: any) => part.type === "output_text")
      ?.text ?? ""
  );
}
```

## React Client Calling Your Backend

The client uses your app route only. It does not import a General Augment key, base URL,
or admin endpoint.

```tsx

type AssistantMessage = {
  role: "user" | "assistant";
  text: string;
};

export function AssistantPanel() {
  const [messages, setMessages] = useState<AssistantMessage[]>([]);
  const [input, setInput] = useState("");
  const [error, setError] = useState<string | null>(null);
  const [isLoading, setIsLoading] = useState(false);

  async function submit(event: FormEvent) {
    event.preventDefault();
    const message = input.trim();
    if (!message) return;

    setMessages((current) => [...current, { role: "user", text: message }]);
    setInput("");
    setError(null);
    setIsLoading(true);

    const response = await fetch("/api/assistant", {
      method: "POST",
      headers: { "Content-Type": "application/json" },
      body: JSON.stringify({ message, conversationId: "support-thread-42" }),
    });

    const payload = await response.json().catch(() => ({}));
    setIsLoading(false);

    if (!response.ok) {
      if (response.status === 429) {
        setError(`The assistant is busy. Try again after ${payload.retryAfter ?? "the reset window"}.`);
        return;
      }
      if (response.status === 402) {
        setError("This assistant has reached its usage limit for now.");
        return;
      }
      setError(payload.error ?? "The assistant could not answer.");
      return;
    }

    setMessages((current) => [...current, { role: "assistant", text: payload.text ?? "" }]);
  }

  return (
    <form onSubmit={submit}>

        {messages.map((message, index) => (
          <p key={index}>
            **{message.role === "user" ? "You" : "Assistant"}:** {message.text}

        ))}

      {error ? <p role="alert">{error} : null}
      <textarea value={input} onChange={(event) => setInput(event.target.value)} />
      <button type="submit" disabled={isLoading}>
        {isLoading ? "Sending..." : "Send"}
      </button>
    </form>
  );
}
```

## Usage-Threshold Webhook Receiver Sketch

General Augment can post usage-threshold events when a project has
`billing_webhook_url` configured and crosses 80% or 100% of its daily agent-turn limit.
This is an advisory usage signal, not a generic response-completed webhook and not an
approval callback webhook. Approval lifecycle callbacks use the separate
`approval_webhook_url` project setting and signed approval event contract.

> Note:

The usage-threshold webhook posts JSON to the configured URL without a documented
General Augment signature header. Put the receiver behind your own gateway controls
where possible, use an unguessable path, validate `project_id`, and reconcile with
the project usage endpoint before making billing or suspension decisions.

```ts
// app/api/webhooks/general-augment/usage/route.ts

type UsageThresholdEvent = {
  event: "usage.threshold";
  project_id: string;
  date: string;
  plan: string;
  pricing_tier: string;
  threshold_percent: 80 | 100;
  agent_turns_count: number;
  agent_turns_per_day: number;
  messages_count: number;
};

export async function POST(request: NextRequest) {
  const payload = (await request.json()) as Partial<UsageThresholdEvent>;

  if (payload.event !== "usage.threshold") {
    return NextResponse.json({ error: "Unsupported event." }, { status: 400 });
  }

  if (payload.project_id !== process.env.GENAUG_PROJECT_ID) {
    return NextResponse.json({ error: "Unknown project." }, { status: 403 });
  }

  if (payload.threshold_percent !== 80 && payload.threshold_percent !== 100) {
    return NextResponse.json({ error: "Unsupported threshold." }, { status: 400 });
  }

  await enqueueUsageAlert({
    projectId: payload.project_id,
    date: payload.date,
    thresholdPercent: payload.threshold_percent,
    agentTurnsCount: payload.agent_turns_count,
    agentTurnsPerDay: payload.agent_turns_per_day,
  });

  return NextResponse.json({ ok: true });
}

async function enqueueUsageAlert(event: {
  projectId?: string;
  date?: string;
  thresholdPercent?: number;
  agentTurnsCount?: number;
  agentTurnsPerDay?: number;
}) {
  // Send an internal Slack alert, create a customer-success task, or mark the account
  // for review. Reconcile with GET /api/v1/admin/projects/{project_id}/usage before
  // changing product access.
  console.info("general_augment_usage_threshold", event);
}
```

## Tool And Action Definition Pattern

Use one of two patterns:

1. **App-owned execution:** General Augment returns a summary, draft, or structured
action proposal. Your app shows confirmation and performs the side effect with
app-held credentials.
2. **Delegated tool execution:** expose a hosted MCP server or OpenAPI operation.
General Augment registers the operation as a project-defined tool, applies the
allowlist and risk policy, and executes through the configured credential boundary.

For a first integration, define app actions as narrow backend endpoints with explicit
input schemas and stable operation IDs. Read endpoints can usually auto-execute after
review. Write endpoints should require approval. Destructive endpoints should stay
disabled until explicitly accepted.

```yaml
openapi: 3.1.0
info:
  title: Acme Assistant Actions
  version: 1.0.0
paths:
  /agent-actions/account-summary:
    post:
      operationId: create_account_summary
      summary: Create a read-only account summary for the signed-in user.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [account_id]
              properties:
                account_id:
                  type: string
      responses:
        "200":
          description: Account summary.
  /agent-actions/support-reply:
    post:
      operationId: draft_support_reply
      summary: Draft a support reply for human review.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              type: object
              required: [ticket_id, tone]
              properties:
                ticket_id:
                  type: string
                tone:
                  type: string
                  enum: [concise, warm, technical]
      responses:
        "200":
          description: Draft reply.
```

Register and review the generated tools:

```bash
genaug integrate https://api.example.com/openapi.json \
  --name acme-agent \
  --description "Acme app assistant"

genaug tools list --project acme-agent
genaug tools toggle draft_support_reply --project acme-agent --enable
genaug deploy ./acme-agent/genaug-agent.yaml
```

If you review generated OpenAPI tool files under `tools/`, keep the risk metadata
readable:

```yaml
id: draft_support_reply
enabled: true
risk_level: medium
requires_approval: true
description: Draft a support reply for human review before any send action.
```

General Augment does not expose arbitrary inline `/v1/responses` function callbacks as
the stable public tool interface today. Use OpenAPI-generated tools or MCP servers for
delegated execution.

## Human Approval Pattern

For V1, many apps should keep sensitive actions app-owned: ask General Augment for an
action proposal, show your own confirmation screen, then execute with your own OAuth
credentials.

When you delegate writes to General Augment, approval-required tools create
project-scoped approval rows before side effects run. Built-in approval-required tools
include `email_send`, `calendar_create`, and `calendar_update`. Generated `POST`,
`PUT`, and `PATCH` OpenAPI tools require approval by default, and generated `DELETE`
tools are disabled by default.

Your backend can show and resolve pending approvals with admin endpoints:

```ts
export async function listPendingApprovals() {
  const baseUrl = process.env.GENAUG_API_BASE_URL ?? "https://api.generalaugment.com";
  const projectId = process.env.GENAUG_PROJECT_ID;
  const apiKey = process.env.GENAUG_API_KEY;

  const response = await fetch(`${baseUrl}/api/v1/admin/projects/${projectId}/approvals?status=pending`, {
    headers: { "X-Admin-Key": apiKey ?? "" },
  });

  if (!response.ok) {
    throw new Error(`Could not list approvals: ${response.status}`);
  }

  return response.json();
}

export async function resolveApproval(approvalId: string, decision: "approve" | "deny") {
  const baseUrl = process.env.GENAUG_API_BASE_URL ?? "https://api.generalaugment.com";
  const projectId = process.env.GENAUG_PROJECT_ID;
  const apiKey = process.env.GENAUG_API_KEY;

  const response = await fetch(
    `${baseUrl}/api/v1/admin/projects/${projectId}/approvals/${approvalId}/${decision}`,
    {
      method: "POST",
      headers: { "X-Admin-Key": apiKey ?? "" },
    },
  );

  if (!response.ok) {
    throw new Error(`Could not ${decision} approval: ${response.status}`);
  }

  return response.json();
}
```

Current approval limitations to design around:

- Approval rows expire after the runtime approval TTL, currently about five minutes.
- When `approval_webhook_url` is configured, General Augment sends signed
`approval.pending`, `approval.approved`, `approval.denied`, and `approval.expired`
events with a stable `event_id` for receiver idempotency.
- WhatsApp and Telegram can send interactive approval buttons; SMS uses `YES` or `NO`.
- App UIs should still poll or refresh pending approvals as the reconciliation fallback.
- Approval is not a credential substitute. The project/user/provider credential and
identity-link requirements must still be satisfied before a delegated tool can run.

## Local Smoke Checklist

Run this before product teams consider the integration ready for shared testing:

```bash
uv run --project packages/cli genaug mock --host 127.0.0.1 --port 8787 --quiet
curl -sS http://127.0.0.1:8787/health/ready
```

Then point the app backend at the mock:

```bash
export GENAUG_API_BASE_URL="http://127.0.0.1:8787"
export GENAUG_API_KEY="local-test"
```

Checklist:

- Your browser or mobile client calls only your backend route.
- No General Augment project key is present in client bundles, client logs, analytics,
prompts, or metadata.
- The app backend can call `/v1/responses` and render the returned `output_text`.
- App logs store response id, request id, trace id, user id, feature, and idempotency
key.
- `402` and `429` responses produce useful product states and do not trigger tight
retry loops.
- If memory is in scope, two test users have separate store/search/profile/delete
checks.
- If tools are in scope, generated tools are reviewed, allowlisted, and disabled where
risky.
- If writes are in scope, the app has either app-owned confirmation UX or a delegated
General Augment approval path.
- Hosted launch evidence includes `genaug smoke`, `genaug verify`, and
`genaug onboarding verify --project <project> --json` for the same project.

## First 60 Minutes Integration Plan

1. Pick the reversible slice. Choose one product surface, one stable app user id, one
feature name for metadata, and one project-scoped key.

2. Add the backend client. Add `GENAUG_API_BASE_URL` and `GENAUG_API_KEY` to server-side
secret storage. Implement the `/v1/responses` helper with `model`, `user`, `input`,
`metadata`, and `X-Idempotency-Key`.

3. Proxy through your app backend. Add a route such as `POST /api/assistant`. Read the
signed-in user from your app session, call General Augment from the server, return
`text`, `responseId`, and `traceId`.

4. Wire a minimal client. Add a UI or product workflow that calls your backend route.
Confirm the browser never sees `GENAUG_API_KEY`.

5. Test locally. Run the local mock, call the app route, verify `output_text`, and
confirm app logs include the response id, trace id, feature, and stable user id.
Exercise `402` and `429` handling in app tests or fixtures.

6. Decide the next layer. Pick exactly one follow-up: explicit memory, one read-only
generated tool, one approval-gated write, or hosted smoke evidence.

Done means the app backend can produce one traced assistant response for one signed-in
user, the key stayed server-side, and the app team has the IDs needed to debug that
turn with General Augment support.
