Skip to main content

Read this first

This guide is written for autonomous AI agents operating Puffle on behalf of a human customer. Use only the public endpoints documented under API Reference. Dashboard-only, admin-only, inbound webhook, and private UI routes are intentionally outside this contract. If something appears missing from the docs, it is either intentionally out of scope (admin-only tools, inbound webhook receivers) or a gap to report back to the human — do not guess endpoints.

Authentication

Every public API call must include a Puffle API key as a Bearer token:
Authorization: Bearer pk_live_...
  • Keys are created by the human customer in the Puffle dashboard. The /api/user/api-key endpoints are session-authenticated dashboard endpoints, not Bearer-token backend integration endpoints.
  • Keys are scoped to a single workspace.
  • Never log or echo the key back to the customer.
  • On 401 unauthorized, stop and ask the human for a valid key — do not retry blindly.
See API Overview authentication and API key management.

Boot sequence — the first thing to do

Before issuing any workflow call, read the workspace profile. Skipping it leads to agents operating on incomplete assumptions (wrong ICP, empty context, insufficient credits).
1. GET  /api/context              → what do I know about this user?

What does the workspace know?

GET /api/context
Returns the user’s company profile, positioning, ICP description, target market. Every downstream action that generates content (messages, lead filters) implicitly uses this context server-side, but you as the agent should still read it:
  • So you can explain the user’s business back to them when asked (“You’re targeting Series A startups in fintech…”)
  • So you can detect a mismatch with the task (if the user asks you to outreach to retail buyers but their ICP is B2B SaaS, flag it)
  • So you can self-select which journey to run (e.g. “lead reply inbox” is only meaningful if at least one campaign has launched)
If context or profile is null, stop and escalate to the human — no point launching campaigns, running research, or configuring signals for a user who hasn’t finished setting up their ICP and company profile. The UI blocks those actions too. After this call, branch into the appropriate user journey below.

Credit system — check before spending

Puffle charges credits for actions that call paid third-party services, such as enrichment, signal scans, and LLM-powered generation. Before starting a large paid operation, read the public credit configuration and estimate cost from the requested batch size.
GET /api/credits/config
The response lists the current per-unit credit costs. If the user gave you a credit budget, stay inside it. If exact affordability matters and the public API cannot confirm the current balance, ask the human before running the paid operation. If a billable endpoint reports insufficient credits, stop and tell the human which operation failed and what smaller batch size you can try. Do not partially-execute a paid workflow after a credit failure unless the user explicitly approves the reduced scope.

Async patterns

Expensive operations are async. The pattern is uniform:
  1. POST kicks off the work → responds 202 Accepted with a correlationId (also in the x-correlation-id header) and a reference to a status endpoint.
  2. GET the status endpoint by ID → returns status: "pending" | "running" | "done" | "failed" plus a result payload when done.
  3. Poll with backoff (start 2 s, double up to 30 s). Most operations finish in 30 s–5 min.
Every async response includes the correlationId. If the human reports an issue, capture and pass this ID back — support can trace every downstream call and background job through a single query. Do not busy-loop without backoff. Do not poll more than once per second. Do surface the correlationId to the human in every error message.

Error handling

The API Overview documents the current error shapes. Prefer structured errors when present:
{ "error": { "code": "invalid_request", "message": "..." } }
Some route-specific errors are still returned as a flat string, for example { "error": "..." }. Use each endpoint page’s response examples as the source of truth.
StatusAgent response
400 invalid_requestFix the payload from the message content. Do not retry the same body.
401 unauthorizedStop. Ask the human for a valid API key.
403 forbiddenStop. This workspace lacks access to this feature; inform the human.
404 not_foundResource doesn’t exist or belongs to another workspace. Do not retry.
409 conflictResource already exists. Fetch the existing one and continue.
422 unprocessable_entitySemantic error — reread the endpoint doc. Do not retry without changes.
429 rate_limitedBack off. Respect Retry-After if present, otherwise wait 30 s.
500 internal_errorRetry once after 5 s unless the endpoint says the action already happened. For one-off email sends, if the response says the message was sent but could not be saved, do not retry.

Core user journeys

The five sequences below cover 90% of what a customer-role agent needs to do. Each chains endpoints in a specific order — follow it.

Journey 1 — Launch a campaign end-to-end

1. POST  /api/leads/search                               → queue search and create/populate a result list
2. GET   /api/leads/search?taskId={taskId}                → poll until status=done
3. GET   /api/senders?type=email|linkedin                → choose an available sender for the campaign channel
4. POST  /api/campaigns                                  → create a draft campaign shell
5. PATCH /api/campaigns/{id}                             → set `sender_account_ids` and any full-sequence edits
6. POST  /api/campaigns/{id}/leads/import-from-list      → attach leads from the result list
7. POST  /api/campaigns/{id}/launch                      → start sending
8. GET   /api/campaigns/{id}                             → poll `status` and `stats`
Related docs: Campaigns · Launch campaign · Lists · Leads search.

Journey 2 — Import leads into a list

1. POST  /api/lists                                      → create target list
2. POST  /api/lists/import { preview: true, listId, leads } → validate normalized Leads
3. POST  /api/lists/import { listId, leads }             → upsert Leads and attach them
4. GET   /api/lists/{id}/leads                           → confirm imported Leads
Related: Create list · Import leads.

Journey 3 — Configure signals and query the feed

1. GET   /api/signals/types                      → list built-in and custom signal types
2. POST  /api/signals/types                      → create a custom keyword signal type
3. GET   /api/signals/sources                    → confirm enabled data sources
4. PATCH /api/signals/sources                    → adjust source enablement if needed
5. GET   /api/signals?minScore=3                 → read high-scoring signals
6. PATCH /api/signals                            → vote, comment, save drafts, or set `status: "dismissed"`
7. POST  /api/signals/generate                   → generate DM/comment drafts for selected signals
Signals scan on a schedule. Creating a type does not immediately populate the feed — allow up to 1 hour for the first scan. Related: Signals documentation · signals feed · types · sources.

Journey 4 — Manage replies in Unibox

1. GET   /api/threads                           → list active threads
2. GET   /api/threads/{id}/messages             → read the thread
3. POST  /api/threads/{id}/messages             → send a follow-up from the right sender
4. PATCH /api/threads/{id}                      → update thread status or assignment
If the thread requires a sender account that is not connected, direct the human to connect it in the dashboard. Connection requires an interactive OAuth or inbox setup flow that agents cannot complete autonomously. Related: Unibox · Threads · Send message.

Journey 5 — Send a one-off email

1. GET   /api/senders?type=email                 → choose a ready email sender
2. POST  /api/threads                            → create a draft one-off email thread
3. POST  /api/threads/{id}/messages              → send with `threading_mode: "new_thread"`
4. GET   /api/threads/{id}/messages              → confirm the outbound message is saved
Use this only for direct one-off email. Campaign outreach should still use Campaigns endpoints and sequence nodes. If step 3 returns a 500 that says the message was sent but could not be saved, do not retry the send. The recipient may already have received the email. Stop and report the response instead of using step 4 as a reason to send again. Related: Create thread · Send message · Senders.

What agents MUST NOT do

  • Do not call endpoints under /api/admin/* — these are internal team tools, not part of the product surface.
  • Do not call inbound webhook receivers or internal integration callbacks — they are called by external services, never by you.
  • Do not attempt OAuth flows (Gmail, LinkedIn, Reddit, or sender account connection) autonomously — they require a human-in-the-loop browser session. If a feature requires an OAuth-connected account, tell the human and stop.
  • Do not persist the API key anywhere except in-memory for the duration of the task.
  • Do not ignore endpoint-specific rate limits or poll faster than endpoint guidance — see API Overview rate limits.

Escalation

Stop and ask the human when:
  • Credits are insufficient for the requested work.
  • An operation requires OAuth/browser interaction (account connection, login flows).
  • A 403 forbidden or 404 not_found on a resource you expected to exist.
  • A 500 internal_error that persists after one retry — include the correlationId in the escalation.
  • The requested outcome has no corresponding endpoint documented here.