Introduction 
Everything you need to get started with the Wiro AI platform.
What is
?
Wiro is an AI model marketplace and API platform that lets you run AI models through a single, unified API. Instead of managing infrastructure for each model provider, you make one API call to Wiro and we handle the rest.
- Unified API — one interface for all models (image generation, LLMs, audio, video, and more)
- Pay-per-use pricing — only pay for what you consume, no upfront commitments
- Real-time WebSocket updates — stream task progress and outputs live
- 9 SDK languages — curl, Python, Node.js, PHP, C#, Swift, Dart, Kotlin, Go
Base URL
All API requests are made to:
https://api.wiro.ai/v1
WebSocket connections use:
wss://socket.wiro.ai/v1
Quick Start
- Sign up Create an account at wiro.ai
- Create a project Go to the Dashboard to get your API key
- Pick a model Browse the marketplace and choose a model
- Make your first API call See Code Examples for full end-to-end samples
Response Format
Every API response returns JSON with a consistent structure:
{
"result": true,
"errors": [],
"data": { ... }
}
When result is false, the errors
array contains human-readable messages describing what went wrong.
Rate Limits & Error Handling
API requests are rate-limited per project. If you exceed the limit, the API returns a 429 Too Many Requests status. Implement exponential backoff in your retry logic.
Common HTTP status codes:
200— Success400— Bad request (check parameters)401— Unauthorized (invalid or missing API key)403— Forbidden (signature mismatch or insufficient permissions)429— Rate limit exceeded500— Internal server error
Authentication
Secure your API requests with signature-based or simple key authentication.
Overview
Wiro supports two authentication methods. You choose the method when creating a project — it cannot be changed afterward.
Signature-Based Authentication
Uses HMAC-SHA256 to sign every request. The API secret never leaves your environment, making this method ideal for client-side applications where the key might be exposed.
How it works
- Generate a nonce Use a unix timestamp or random integer
-
Concatenate Combine:
API_SECRET + NONCE -
Create HMAC-SHA256 hash Use your
API_KEYas the secret key - Send as headers Include the signature, nonce, and API key in request headers
SIGNATURE = HMAC-SHA256(key=API_KEY, message=API_SECRET + NONCE)
Required Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
x-api-key |
string | Yes | Your project API key |
x-signature |
string | Yes | HMAC-SHA256(API_SECRET + NONCE, API_KEY) |
x-nonce |
string | Yes | Unix timestamp or random integer |
API Key Only Authentication
For server-side applications where you control the environment, you can use the simpler API-key-only method. Just include the x-api-key header — no signature required.
Required Headers
| Parameter | Type | Required | Description |
|---|---|---|---|
x-api-key |
string | Yes | Your project API key |
Comparison
| Feature | Signature-Based | API Key Only |
|---|---|---|
| Security | High — secret never sent over the wire | Moderate — key sent in every request |
| Complexity | Requires HMAC computation | Single header |
| Best for | Client-side apps, mobile, public repos | Server-side, internal tools |
| Replay protection | Yes (via nonce) | No |
How to Choose
- Building a client-side or mobile app? Use Signature-Based.
- Running a server-side backend with controlled access? API Key Only is simpler.
- Unsure? Default to Signature-Based — it's always the safer option.
Projects
Organize your API access, billing, and usage with projects.
What is a Project?
A project is a container that holds your API keys, billing settings, and usage tracking. Each project gets its own API key and secret, letting you separate environments (development, staging, production) or different applications.
- Each project has its own API key and (optionally) API secret
- Usage and billing are tracked per project
- You can create multiple projects under one account
Creating a Project
- Go to the Dashboard Navigate to wiro.ai/panel
- Open Projects Go to Projects and click New Project
- Name your project Enter a descriptive project name
- Select authentication method Signature-Based — generates API key + secret | API Key Only — generates only an API key
- Create Click Create and copy your credentials immediately
API Credentials
After creating a project, your API key (and secret, if signature-based) are displayed once. Copy and store them securely — you won't be able to view the secret again.
Important: Treat your API secret like a password. Never commit it to version control or expose it in client-side code without signature-based authentication.
Managing Projects
From the Projects page in your Dashboard, you can:
- Update name — rename your project at any time
- Regenerate keys — invalidates existing keys and generates new ones
- View usage — see API calls, costs, and task history
- Delete project — permanently removes the project and revokes all keys
Regenerating keys immediately invalidates the old ones. Update your application with the new credentials before the old ones stop working.
Models
Browse and discover AI models available on the Wiro platform.
POST /Tool/List
Returns a paginated list of available models. Filter by categories, search by name, and sort results.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
start |
string | No | Offset for pagination (default: "0") |
limit |
string | No | Number of results to return (default: "20") |
search |
string | No | Search query to filter models by name |
sort |
string | No | Sort field: id, relevance |
order |
string | No | Sort direction: ASC or DESC |
categories |
string[] | No | Filter by categories (e.g. image-generation, llm, audio, video) |
tags |
string[] | No | Filter by tags |
slugowner |
string | No | Filter by model owner slug |
hideworkflows |
boolean | No | Hide workflow models from results (recommended: true) |
summary |
boolean | No | Return summarized model data (recommended for listings) |
Response
{
"result": true,
"errors": [],
"total": 2,
"tool": [
{
"id": "1611",
"title": "Virtual Try-on",
"slugowner": "wiro",
"slugproject": "Virtual Try-On",
"cleanslugowner": "wiro",
"cleanslugproject": "virtual-try-on",
"description": "Integrate the Wiro Virtual Try-On API...",
"image": "https://cdn.wiro.ai/uploads/models/...",
"computingtime": "10 seconds",
"categories": ["tool", "image-to-image", "image-editing"],
"tags": [],
"marketplace": 1,
"onlymembers": "1",
"averagepoint": "5.00",
"commentcount": "1",
"dynamicprice": "[{\"inputs\":{},\"price\":0.09,\"priceMethod\":\"cpr\"}]",
"taskstat": {
"runcount": 672,
"successcount": "254",
"errorcount": "198",
"lastruntime": "1774007585"
}
}
]
}
POST /Tool/Detail
Returns full details for a specific model, including its input parameters, pricing, categories, and configuration.
Request Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
slugowner |
string | Yes | Model owner slug (e.g. stability-ai) |
slugproject |
string | Yes | Model project slug (e.g. sdxl) |
summary |
boolean | No | Return summarized data |
Response
{
"result": true,
"errors": [],
"tool": [{
"id": "1611",
"title": "Virtual Try-on",
"slugowner": "wiro",
"slugproject": "Virtual Try-On",
"cleanslugowner": "wiro",
"cleanslugproject": "virtual-try-on",
"description": "Integrate the Wiro Virtual Try-On API...",
"image": "https://cdn.wiro.ai/uploads/models/...",
"computingtime": "10 seconds",
"readme": "<p>The Wiro Virtual Try-On AI model...</p>",
"categories": ["tool", "image-to-image", "image-editing"],
"parameters": null,
"inspire": [
{
"inputImageHuman": "https://cdn.wiro.ai/uploads/sampleinputs/...",
"inputImageClothes": ["https://cdn.wiro.ai/..."]
}
],
"samples": ["https://cdn.wiro.ai/uploads/models/..."],
"tags": [],
"marketplace": 1,
"onlymembers": "1",
"dynamicprice": "[{\"inputs\":{},\"price\":0.09,\"priceMethod\":\"cpr\"}]",
"averagepoint": "5.00",
"commentcount": "1",
"ratedusercount": "3",
"taskstat": {
"runcount": 672,
"successcount": "254",
"errorcount": "198",
"lastruntime": "1774007585"
},
"seotitle": "AI Virtual Try-On: Integrate Realistic Apparel Fitting",
"seodescription": "Integrate the Wiro Virtual Try-On API..."
}]
}
Model Browser
Browse available models interactively. Click on a model to see its details on the model page.
Run a Model
Execute any AI model with a single API call and get real-time updates.
POST /Run/{owner-slug}/{model-slug}
Starts an AI model run. The endpoint accepts model-specific parameters and returns a task ID you can use to track progress via polling, WebSocket, or webhook by providing a callbackUrl parameter — Wiro will POST the result to your URL when the task completes.
Content Types
JSON (application/json)
Use JSON for text-based inputs — prompts, configuration, numeric parameters. This is the default and most common format.
Multipart (multipart/form-data)
Use multipart when the model requires file inputs (images, audio, documents). Include files as form fields and other parameters as text fields.
Request Parameters
Parameters vary by model. Use the /Tool/Detail endpoint to discover which parameters a model accepts. The following optional parameters apply to all runs:
Common Parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
callbackUrl |
string | No | URL to receive a POST webhook when the task completes |
projectid |
string | No | Override the default project for billing (if you have multiple projects) |
Response
A successful run returns a task ID and a WebSocket access token:
{
"result": true,
"errors": [],
"taskid": "2221",
"socketaccesstoken": "eDcCm5yyUfIvMFspTwww49OUfgXkQt"
}
Full Flow
The typical workflow after calling the Run endpoint:
- Run — call
POST /Run/{owner-slug}/{model-slug}and receive a task ID - Track — connect via WebSocket or poll
POST /Task/Detail - Receive — get outputs as the model produces them (streaming or final)
- Complete — task reaches
endstatus with full results
For real-time streaming, use the WebSocket connection with the
socketaccesstoken returned in the run response. For simpler integrations, poll the Task Detail endpoint every few seconds.
Model Parameters
Understand parameter types, content types, and how to send inputs to any model.
Discovering Parameters
Every model has its own set of input parameters. Use the /Tool/Detail endpoint to retrieve a model's parameter definitions. The response includes a parameters array where each item describes a parameter group with its items:
{
"parameters": [
{
"title": "Input",
"items": [
{
"id": "prompt",
"type": "textarea",
"label": "Prompt",
"required": true,
"placeholder": "Describe what you want...",
"note": "Text description of the desired output"
},
{
"id": "inputImage",
"type": "fileinput",
"label": "Input Image",
"required": true,
"note": "Upload an image or provide a URL"
}
]
}
]
}
Parameter Types
| Type | Description | Example Parameters |
|---|---|---|
text |
Single-line text input | URLs, names, short strings |
textarea |
Multi-line text input | prompt, negative_prompt, descriptions |
select |
Dropdown with predefined options | outputType, language, style |
range |
Numeric value (slider) | width, height, scale, strength |
fileinput |
Single file upload (1 file or 1 URL) | inputImage, inputAudio |
multifileinput |
Multiple files (up to N files/URLs) | inputDocumentMultiple |
combinefileinput |
Up to N entries (files, URLs, or mixed) | inputImageClothes |
JSON vs Multipart
The content type of your request depends on whether the model requires file inputs:
| Condition | Content-Type | When to Use |
|---|---|---|
| No file parameters | application/json |
Text-only models (LLMs, image generation from prompt) |
| Has file parameters | multipart/form-data |
Models that accept image, audio, video, or document uploads |
Tip: For fileinput and multifileinput parameters, use the {id}Url suffix to send URLs (e.g., inputImageUrl). For combinefileinput, pass URLs directly in the original parameter — no suffix needed. You can also pass a URL directly to any file parameter (e.g., inputImage) if the {id}Url field doesn't exist.
File Upload Patterns
Single File (fileinput)
For parameters like inputImage, send either a file or a URL. When using multipart, always include both the {id} and {id}Url fields — leave one empty:
# Option 1: Upload file — send file in {id}, empty {id}Url
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "inputImage=@/path/to/photo.jpg" \
-F "inputImageUrl="
# Option 2: Send URL via {id}Url — send empty {id}, URL in {id}Url
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "inputImage=" \
-F "inputImageUrl=https://example.com/photo.jpg"
# Option 3: Pass URL directly in {id} (no {id}Url needed)
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "inputImage=https://example.com/photo.jpg"
Note: Option 3 is the simplest when you only have a URL. If the {id}Url field doesn't exist for a parameter, always use this approach.
Multiple Files (multifileinput)
For parameters like inputDocumentMultiple, upload up to N files, send comma-separated URLs, or mix both:
# Option 1: Upload multiple files — add empty {id}Url
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "[email protected]" \
-F "[email protected]" \
-F "inputDocumentMultipleUrl="
# Option 2: Send URLs (comma-separated in {id}Url) — add empty {id}
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "inputDocumentMultiple=" \
-F "inputDocumentMultipleUrl=https://example.com/doc1.pdf,https://example.com/doc2.pdf"
# Option 3: Mixed — files in {id}, URLs in {id}Url
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "[email protected]" \
-F "inputDocumentMultipleUrl=https://example.com/doc2.pdf,https://example.com/doc3.pdf"
Combined (combinefileinput)
For parameters like inputImageClothes, files and URLs go directly in the same {id} field — no {id}Url suffix:
# Option 1: Upload files — each as a separate {id} entry
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "[email protected]" \
-F "[email protected]"
# Option 2: Send URLs — each directly in {id}
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "inputImageClothes=https://example.com/shirt.jpg" \
-F "inputImageClothes=https://example.com/pants.jpg"
# Option 3: Mixed — files and URLs in the same {id} field
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "[email protected]" \
-F "inputImageClothes=https://example.com/pants.jpg"
Common Model Patterns
Image Generation (text-to-image)
Models like Stable Diffusion, Flux — JSON body, no file uploads:
{
"prompt": "A futuristic city at sunset",
"negative_prompt": "blurry, low quality",
"width": 1024,
"height": 1024
}
Image-to-Image (upscaler, style transfer)
Models that take an input image — multipart with file upload:
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "[email protected]" \
-F "scale=4"
Virtual Try-On
Multiple image inputs — multipart with multiple files:
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "[email protected]" \
-F "[email protected]"
LLM / Document Processing
Text prompt with optional document uploads:
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "x-api-key: YOUR_API_KEY" \
-F "[email protected]" \
-F "prompt=Extract the candidate name and skills" \
-F "outputType=json" \
-F "language=en"
Note: LLM responses are delivered as structured content in the outputs array (with contenttype: "raw") and as merged plain text in debugoutput. See Tasks for details.
Realtime Voice Conversation
Realtime voice models accept configuration parameters (voice, system instructions, audio format, etc.) as JSON. Parameters vary per model — use /Tool/Detail to discover them. The actual audio interaction happens over Realtime Voice WebSocket after the task starts:
// Example: OpenAI GPT Realtime
{
"voice": "marin",
"system_instructions": "You are a helpful voice assistant.",
"input_audio_format": "audio/pcm",
"output_audio_format": "audio/pcm",
"input_audio_rate": "24000",
"output_audio_rate": "24000"
}
Webhook Callback
All models support an optional callbackUrl parameter. When provided, Wiro will POST the task result to your URL when the task completes — no polling required:
{
"prompt": "A sunset over mountains",
"callbackUrl": "https://your-server.com/webhook/wiro"
}
Tasks
Track, monitor, and control your AI model runs.
Task Lifecycle
Every model run creates a task that progresses through a defined set of stages:
Task Statuses
| Status | Description |
|---|---|
task_queue |
The task is queued and waiting to be picked up by an available worker. Emitted once when the task enters the queue. |
task_accept |
A worker has accepted the task. The task is no longer in the general queue and is being prepared for execution. |
task_preprocess_start |
Optional preprocessing has started. This includes operations like downloading input files from URLs, converting file types, and validating/formatting parameters before the model runs. Not all models require preprocessing. |
task_preprocess_end |
Preprocessing completed. All inputs are ready for GPU assignment. |
task_assign |
The task has been assigned to a specific GPU. The model is being loaded into memory. This may take a few seconds depending on the model size. |
task_start |
The model command has started executing. Inference is now running on the GPU. |
task_output |
The model is producing output. This event is emitted multiple times — each time the model writes to stdout, a new task_output message is sent via WebSocket. For LLM models, each token/chunk arrives as a separate task_output event, enabling real-time streaming. |
task_error |
The model wrote to stderr. This is an interim log event, not a final failure — many models write warnings or debug info to stderr during normal operation. The task may still complete successfully. Always wait for task_postprocess_end to determine the actual result. |
task_output_full |
The complete accumulated stdout log, sent once after the model process finishes. Contains the full output history in a single message. |
task_error_full |
The complete accumulated stderr log, sent once after the model process finishes. |
task_end |
The model process has exited. Emitted once. This fires before post-processing — do not use this event to determine success. Wait for task_postprocess_end instead. |
task_postprocess_start |
Post-processing has started. The system is preparing the output files — encoding, uploading to CDN, and generating access URLs. |
task_postprocess_end |
Post-processing completed. Check pexit to determine success: "0" = success, any other value = error. The outputs array contains the final files with CDN URLs, content types, and sizes. This is the event you should listen for to get the final results. |
task_cancel |
The task was cancelled (if queued) or killed (if running) by the user. |
Realtime Conversation Only
The following statuses are exclusive to realtime conversation models (e.g. voice AI). They are not emitted for standard model runs.
| Status | Description |
|---|---|
task_stream_ready |
Realtime model is ready to receive audio/text input — you can start sending data |
task_stream_end |
Realtime session has ended — the model finished speaking or the session was closed |
task_cost |
Real-time cost update emitted during execution — shows the running cost of the task |
Determining Success or Failure
Both successful and failed tasks reach task_postprocess_end. The status alone does not tell you whether the task succeeded. Wait for task_postprocess_end and then check pexit or outputs (or both) to determine the actual result:
pexit— the process exit code."0"means success, any other value means the model encountered an error. This is the most reliable indicator.outputs— the output array. For non-LLM models, this contains CDN file URLs. For LLM models, this contains a structured entry withcontenttype: "raw"holding the response text, thinking, and answer arrays. If it's empty or missing, the task likely failed.
Note: For LLM models, outputs contains a single entry with contenttype: "raw" and a content object holding prompt, raw, thinking, and answer. The merged plain text is also available in debugoutput. Always use pexit as the primary success check.
// Success (image/audio model): pexit "0", file outputs with CDN URLs
{
"pexit": "0",
"outputs": [{
"name": "0.png",
"contenttype": "image/png",
"size": "202472",
"url": "https://cdn1.wiro.ai/.../0.png"
}]
}
// Success (LLM model): pexit "0", structured raw content in outputs
{
"pexit": "0",
"debugoutput": "Hello! How can I help you today?",
"outputs": [{
"contenttype": "raw",
"content": {
"prompt": "Say hello",
"raw": "Hello! How can I help you today?",
"thinking": [],
"answer": ["Hello! How can I help you today?"]
}
}]
}
// Failure: pexit non-zero
{
"pexit": "1",
"outputs": []
}
Important: task_error events during execution are interim log messages, not final failures. A task can emit error logs and still complete successfully. Always wait for task_postprocess_end and check pexit.
Billing & Cost
The totalcost field in the Task Detail response shows the actual cost charged for the run. Only successful tasks are billed — if pexit is non-zero (failure), the task is not charged and totalcost will be "0".
Successful run — billed:
{
"status": "task_postprocess_end",
"pexit": "0",
"totalcost": "0.003510000000",
"elapsedseconds": "6.0000"
}
Failed run — not billed:
{
"status": "task_postprocess_end",
"pexit": "1",
"totalcost": "0",
"elapsedseconds": "4.0000"
}
Use the totalcost field to track spending per task. For more details on how costs are calculated, see Pricing.
LLM Models
For LLM (Large Language Model) requests, the model's response is available in two places: as merged plain text in debugoutput, and as a structured entry in the outputs array with contenttype: "raw". The structured output includes separate thinking and answer arrays alongside the original prompt and full raw text.
For real-time streaming of LLM responses, use WebSocket instead of polling. Each task_output event delivers a chunk of the response as it's generated, giving your users an instant, token-by-token experience.
POST /Task/Detail
Retrieves the current status and output of a task. You can query by either tasktoken or taskid.
| Parameter | Type | Required | Description |
|---|---|---|---|
tasktoken |
string | No | The task token returned from the Run endpoint |
taskid |
string | No | The task ID (alternative to tasktoken) |
Response
{
"result": true,
"errors": [],
"total": "1",
"tasklist": [{
"id": "534574",
"socketaccesstoken": "eDcCm5yyUfIvMFspTwww49OUfgXkQt",
"parameters": { "prompt": "Hello, world!" },
"status": "task_postprocess_end",
"pexit": "0",
"debugoutput": "",
"starttime": "1734513809",
"endtime": "1734513813",
"elapsedseconds": "6.0000",
"totalcost": "0.003510000000",
"modeldescription": "FLUX.2 [dev] is a 32 billion parameter rectified flow transformer...",
"modelslugowner": "wiro",
"modelslugproject": "flux-2-dev",
"outputs": [{
"name": "0.png",
"contenttype": "image/png",
"size": "202472",
"url": "https://cdn1.wiro.ai/.../0.png"
}]
}]
}
| Field | Type | Description |
|---|---|---|
id |
string |
Task ID. |
socketaccesstoken |
string |
Token to connect via WebSocket. |
parameters |
object |
The input parameters sent in the run request. |
status |
string |
Current task status (see Task Lifecycle). |
pexit |
string |
Process exit code. "0" = success. |
debugoutput |
string |
Accumulated stdout output. For LLM models, contains the merged response text. |
starttime |
string |
Unix timestamp when execution started. |
endtime |
string |
Unix timestamp when execution ended. |
elapsedseconds |
string |
Total execution time in seconds. |
totalcost |
string |
Actual cost charged for the run in USD. |
modeldescription |
string |
Description of the model that was executed. |
modelslugowner |
string |
Model owner slug (e.g. "google", "wiro"). |
modelslugproject |
string |
Model project slug (e.g. "nano-banana-pro"). |
outputs |
array |
Output files (CDN URLs) or structured LLM content (contenttype: "raw"). |
POST /Task/Cancel
Cancels a task that is still in the queue stage. Tasks that have already been assigned to a worker cannot be cancelled — use Kill instead.
| Parameter | Type | Required | Description |
|---|---|---|---|
tasktoken |
string | Yes | The task token to cancel |
POST /Task/Kill
Terminates a task that is currently running (any status after
assign). The worker will stop processing and the task will move to cancel status.
| Parameter | Type | Required | Description |
|---|---|---|---|
tasktoken |
string | Yes | The task token to kill |
POST /Task/InputOutputDelete
Deletes all output files and input files associated with a completed task. Removes files from S3 storage, local filesystem, and the database. Also invalidates CloudFront CDN cache so deleted files stop being served immediately.
The task must be in a terminal state (task_postprocess_end or task_cancel). Only the task owner can delete files. Shared sample input files (/sampleinputs/) are automatically excluded from deletion.
| Parameter | Type | Required | Description |
|---|---|---|---|
tasktoken |
string | Yes | The task token (socketaccesstoken) |
Response
{
"result": true,
"errors": []
}
After deletion:
- Output files are removed from S3 and CDN cache
- Input files uploaded by the user are removed from S3 and local storage
- The task's
outputfolderidis set to"0"(Task Detail will return empty outputs) - Task record and parameters are preserved — only the files are deleted
- Calling the endpoint again on the same task returns
result: trueimmediately (idempotent)
Errors
| Error | When |
|---|---|
task-not-exist |
Invalid tasktoken or unauthorized |
Task must be completed or cancelled before deleting files |
Task is still running |
LLM & Chat Streaming
Stream LLM responses in real time with thinking/answer separation, session history, and multi-turn conversations.
Overview
LLM (Large Language Model) requests on Wiro work differently from standard model runs:
- Responses are available as structured content in the
outputsarray (contenttype: "raw") and as merged text indebugoutput - Streaming
task_outputmessages contain structuredthinkingandanswerarrays — not plain strings - Multi-turn conversations are supported via
session_idanduser_idparameters pexitis the primary success indicator
Available LLM models include:
- openai/gpt-5-2 — GPT-5-2
- openai/gpt-oss-20b — GPT OSS 20B
- qwen/qwen3-5-27b — Qwen 3.5 27B
Session & Chat History
Wiro maintains conversation history per session. By sending a session_id, the model remembers previous messages and can build on the context of the conversation. Combined with user_id, this enables fully stateful chat experiences:
| Parameter | Type | Required | Description |
|---|---|---|---|
session_id |
string | No | UUID identifying the conversation session. The server stores chat history per session — reuse the same ID for follow-up messages to maintain full context. |
user_id |
string | No | UUID identifying the user. Allows the model to distinguish between different users sharing the same session or to personalize responses. |
prompt |
string | Yes | The user's message or question. |
// First message — start a new session
{
"prompt": "What is quantum computing?",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
// Follow-up — reuse the same session_id
{
"prompt": "Can you explain qubits in more detail?",
"session_id": "550e8400-e29b-41d4-a716-446655440000",
"user_id": "7c9e6679-7425-40de-944b-e07fc1f90ae7"
}
Tip: Generate a new UUID for session_id when starting a fresh conversation. Reuse it for all follow-up messages — the server automatically stores and retrieves the full chat history for that session. To start a new conversation with no prior context, simply generate a new UUID.
Thinking & Answer Phases
Many LLM models separate their output into two phases:
- Thinking — the model's internal reasoning process (chain-of-thought)
- Answer — the final response to the user
When streaming via WebSocket, task_output messages for LLM models contain a structured object (not a plain string):
// LLM task_output message format
{
"type": "task_output",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": {
"type": "progressGenerate",
"task": "Generate",
"speed": "12.4",
"speedType": "words/s",
"raw": "<think>Let me analyze...</think>Quantum computing uses qubits...",
"thinking": ["Let me analyze this step by step...", "The key factors are..."],
"answer": ["Quantum computing uses qubits that can exist in superposition..."],
"isThinking": false,
"elapsedTime": "3s"
}
}
Both thinking and answer are arrays of strings. A model may alternate between thinking and answering multiple times during a single response. The arrays are indexed in pairs — thinking[0] corresponds to answer[0], thinking[1] to answer[1], and so on:
// Multi-turn thinking/answer cycle
{
"thinking": [
"Let me break this into parts...", // thinking[0]
"Now let me verify my reasoning..." // thinking[1]
],
"answer": [
"Quantum computing uses qubits...", // answer[0] — response after thinking[0]
"To summarize: qubits can be 0, 1, or..." // answer[1] — response after thinking[1]
]
}
Each task_output event contains the full accumulated arrays up to that point — not just the new chunk. Simply replace your displayed content with the latest arrays. Use isThinking to show a "thinking" indicator in your UI while the model reasons.
| Field | Type | Description |
|---|---|---|
message.raw |
string |
Full accumulated raw output including thinking tags. |
message.thinking |
string[] |
Array of reasoning/chain-of-thought chunks. May be empty for models without thinking. |
message.answer |
string[] |
Array of response chunks. This is the content to show the user. |
message.isThinking |
boolean |
Whether the model is currently in a thinking phase. |
message.speed |
string |
Generation speed (e.g. "12.4"). |
message.speedType |
string |
Speed unit (e.g. "words/s"). |
message.elapsedTime |
string |
Elapsed time since generation started (e.g. "3s", "1m 5s"). |
Note: Standard (non-LLM) models send message as a progress object or plain string. LLM models send it as a structured object with thinking, answer, and metadata fields. Check the message.type to distinguish.
Streaming Flow
The complete flow for streaming an LLM response:
- Run the model with
prompt,session_id, anduser_id - Connect to WebSocket and send
task_info - Receive
task_outputmessages — each contains the growingthinkingandanswerarrays - Display the latest
answerarray content to the user (optionally showthinkingin a collapsible section) - Complete — on
task_postprocess_end, checkpexitfor success
Polling Alternative
If you don't need real-time streaming, you can poll POST /Task/Detail instead. The response includes both debugoutput (merged plain text) and a structured entry in outputs with separate thinking and answer arrays:
{
"result": true,
"tasklist": [{
"status": "task_postprocess_end",
"pexit": "0",
"debugoutput": "Quantum computing uses qubits that can exist in superposition...",
"outputs": [{
"contenttype": "raw",
"content": {
"prompt": "What is quantum computing?",
"raw": "Quantum computing uses qubits that can exist in superposition...",
"thinking": [],
"answer": ["Quantum computing uses qubits that can exist in superposition..."]
}
}]
}]
}
Note: debugoutput contains the merged plain text (thinking + answer combined). The outputs array provides the structured breakdown with separate thinking and answer arrays. For real-time token streaming, use WebSocket instead.
WebSocket
Receive real-time task updates via a persistent WebSocket connection.
Connection URL
wss://socket.wiro.ai/v1
Connect to this URL after calling the Run endpoint. Use the
socketaccesstoken from the run response to register your session.
Connection Flow
- Connect — open a WebSocket connection to
wss://socket.wiro.ai/v1 - Register — send a
task_infomessage with yourtasktoken - Receive — listen for messages as the task progresses through its lifecycle
- Close — disconnect after the
task_postprocess_endevent (this is the final event with results)
Registration message format:
{
"type": "task_info",
"tasktoken": "your-socket-access-token"
}
Message Types
| Message Type | Description |
|---|---|
task_queue |
The task is queued and waiting to be picked up by an available worker. |
task_accept |
A worker has accepted the task and is preparing for execution. |
task_preprocess_start |
Optional preprocessing has started (downloading input files from URLs, converting file types, validating parameters). |
task_preprocess_end |
Preprocessing completed. All inputs are ready for GPU assignment. |
task_assign |
The task has been assigned to a specific GPU. The model is being loaded into memory. |
task_start |
The model command has started executing. Inference is now running on the GPU. |
task_output |
The model is producing output. Emitted multiple times — each stdout write sends a new message. For LLMs, each token/chunk arrives as a separate event for real-time streaming. |
task_error |
The model wrote to stderr. This is an interim log event, not a final failure — many models write warnings to stderr during normal operation. The task may still succeed. |
task_output_full |
The complete accumulated stdout log, sent once after the model process finishes. |
task_error_full |
The complete accumulated stderr log, sent once after the model process finishes. |
task_end |
The model process has exited. Fires before post-processing — do not use this to determine success. Wait for task_postprocess_end instead. |
task_postprocess_start |
Post-processing has started. The system is preparing output files — encoding, uploading to CDN, generating access URLs. |
task_postprocess_end |
Post-processing completed. Check pexit to determine success ("0" = success). The outputs array contains the final files. This is the event to listen for. |
task_cancel |
The task was cancelled (if queued) or killed (if running) by the user. |
Message Format
Every WebSocket message is a JSON object with this base structure:
{
"type": "task_accept",
"id": "534574",
"tasktoken": "eDcCm5yyUfIvMFspTwww49OUfgXkQt",
"message": null,
"result": true
}
The type field indicates the status. The message field varies by type — it's null for lifecycle events, a string or object for output events, and an array for the final result.
Lifecycle Events
These events signal task state changes. The message field is null:
// task_accept, task_preprocess_start, task_preprocess_end,
// task_assign, task_start, task_end, task_postprocess_start
{
"type": "task_assign",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": null,
"result": true
}
Output Events
Standard models — message is a progress object or plain string:
// Progress output (image generation, video, etc.)
{
"type": "task_output",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": {
"type": "progressGenerate",
"task": "Generate",
"percentage": "60",
"stepCurrent": "6",
"stepTotal": "10",
"speed": "1.2",
"speedType": "it/s",
"elapsedTime": "5s",
"remainingTime": "3s"
},
"result": true
}
// Simple string output (when no progress format is detected)
{
"type": "task_output",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": "Processing complete.",
"result": true
}
LLM models — message is a structured object with thinking/answer arrays. See LLM & Chat Streaming for full details:
{
"type": "task_output",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": {
"type": "progressGenerate",
"task": "Generate",
"speed": "12.4",
"speedType": "words/s",
"raw": "Quantum computing uses qubits...",
"thinking": ["Let me analyze this..."],
"answer": ["Quantum computing uses qubits..."],
"isThinking": false,
"elapsedTime": "3s"
},
"result": true
}
Error Events
task_error is an interim stderr log, not a final failure. The message is a string or progress object:
{
"type": "task_error",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": "UserWarning: Some weights were not initialized...",
"result": true
}
Full Output Events
Sent once after the process exits. Contains the complete accumulated log:
// Standard model
{
"type": "task_output_full",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": {
"raw": "0%|...| 0/10\n10%|█| 1/10\n...\n100%|██████████| 10/10\nDone."
},
"result": true
}
// LLM model — includes thinking/answer separation
{
"type": "task_output_full",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": {
"raw": "<think>Let me analyze...</think>Quantum computing uses qubits...",
"thinking": ["Let me analyze this step by step..."],
"answer": ["Quantum computing uses qubits that can exist in superposition..."]
},
"result": true
}
// Stderr log (only sent if stderr is non-empty)
{
"type": "task_error_full",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": {
"raw": "UserWarning: Some weights were not initialized..."
},
"result": true
}
Final Result
task_postprocess_end is the event you should listen for. The message contains the outputs array:
// Standard model — file outputs with CDN URLs
{
"type": "task_postprocess_end",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": [{
"name": "0.png",
"contenttype": "image/png",
"size": "202472",
"url": "https://cdn1.wiro.ai/.../0.png"
}],
"result": true
}
// LLM model — structured raw content
{
"type": "task_postprocess_end",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"message": [{
"contenttype": "raw",
"content": {
"prompt": "Explain quantum computing",
"raw": "Quantum computing uses qubits...",
"thinking": [],
"answer": ["Quantum computing uses qubits..."]
}
}],
"result": true
}
Realtime Events
These events are exclusive to realtime voice models:
// Session is ready — start sending audio
{
"type": "task_stream_ready",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"result": true
}
// AI finished speaking for this turn
{
"type": "task_stream_end",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"result": true
}
// Cost update per turn
{
"type": "task_cost",
"id": "534574",
"tasktoken": "eDcCm5yy...",
"turnCost": 0.002,
"cumulativeCost": 0.012,
"usage": { "input_tokens": 150, "output_tokens": 89 },
"result": true
}
Binary Frames
For realtime voice models, the WebSocket may send binary frames containing raw audio data. Check if the received message is a Blob (browser) or Buffer (Node.js) before parsing as JSON.
Ending a Session
For realtime/streaming models that maintain a persistent session, send a task_session_end message to gracefully terminate:
{
"type": "task_session_end",
"tasktoken": "your-socket-access-token"
}
After sending this, wait for the task_postprocess_end event before closing the connection. This is the final event that contains the complete results.
Realtime Voice
Build interactive voice conversation apps with realtime AI models.
Overview
Realtime voice models enable two-way audio conversations with AI. Unlike standard model runs that process a single input and return a result, realtime sessions maintain a persistent WebSocket connection where you stream microphone audio and receive AI speech in real time.
The flow is:
- Run the realtime model via POST /Run to get a
socketaccesstoken - Connect to the WebSocket and send
task_infowith your token - Wait for
task_stream_ready— the model is ready to receive audio - Stream microphone audio as binary frames
- Receive AI audio as binary frames and play them
- End the session with
task_session_end
Run Parameters
Each realtime model has its own set of parameters. Use POST /Tool/Detail to discover the exact parameters for a specific model. See Model Parameters for details on parameter types.
Available realtime conversation models include:
- openai/gpt-realtime-mini — GPT Mini Realtime Voice Assistant
- openai/gpt-realtime — GPT Realtime Voice Assistant
- elevenlabs/realtime-conversational-ai — ElevenLabs Conversational AI
Common parameters across realtime models typically include voice selection, system instructions, and audio format settings. Example run for an OpenAI realtime model:
curl -X POST "https://api.wiro.ai/v1/Run/openai/gpt-realtime-mini" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"voice": "marin",
"system_instructions": "You are a friendly assistant.",
"input_audio_format": "audio/pcm",
"output_audio_format": "audio/pcm",
"input_audio_rate": "24000",
"output_audio_rate": "24000"
}'
Important: Parameters vary per model. Always check the model's detail page or use /Tool/Detail to get the exact parameter list before integrating.
Connection & Registration
After running the task, connect to the WebSocket and register with task_info:
var ws = new WebSocket("wss://socket.wiro.ai/v1");
ws.onopen = function() {
ws.send(JSON.stringify({
type: "task_info",
tasktoken: "YOUR_SOCKET_ACCESS_TOKEN"
}));
};
Note: Both standard and realtime models use type: "task_info" with tasktoken to register on the WebSocket.
Realtime Events
During a realtime session, you'll receive these WebSocket events:
| Event | Description |
|---|---|
task_stream_ready |
Session is ready — start sending microphone audio |
task_stream_end |
AI finished speaking for this turn — you can speak again |
task_cost |
Cost update per turn — includes turnCost, cumulativeCost, and usage (raw cost breakdown from the model provider) |
task_output |
Transcript messages prefixed with TRANSCRIPT_USER: or TRANSCRIPT_AI: |
task_end |
The model process has exited. Post-processing follows — wait for task_postprocess_end to close the connection. |
Audio Format
Both directions (microphone → server, server → client) use the same format:
| Property | Value |
|---|---|
| Format | PCM (raw, uncompressed) |
| Bit depth | 16-bit signed integer (Int16) |
| Sample rate | 24,000 Hz (24 kHz) |
| Channels | Mono (1 channel) |
| Byte order | Little-endian |
| Chunk size | 4,800 samples (200 ms) = 9,600 bytes |
Binary Frame Format
Every binary WebSocket frame (in both directions) is structured as:
[tasktoken]|[PCM audio data]
The pipe character | (0x7C) separates the token from the raw audio bytes.
Sending Microphone Audio
Capture microphone at 24 kHz using the Web Audio API with an AudioWorklet. Convert Float32 samples to Int16, prepend your task token, and send as a binary frame.
Key steps:
- Request microphone with
getUserMedia(enable echo cancellation and noise suppression) - Create an
AudioContextat 24,000 Hz sample rate - Use an AudioWorklet to buffer and convert samples to Int16
- Send each chunk as
tasktoken|pcm_databinary frame
Receiving AI Audio
AI responses arrive as binary WebSocket frames in the same PCM Int16 24 kHz format. To play them:
- Check if the message is a
Blob(binary) before parsing as JSON - Find the pipe
|separator and extract audio data after it - Convert Int16 → Float32 and create an
AudioBuffer - Schedule gapless playback using
AudioBufferSourceNode
Transcripts
Both user and AI speech are transcribed automatically. Transcripts arrive as task_output messages with a string prefix:
TRANSCRIPT_USER:— what the user saidTRANSCRIPT_AI:— what the AI said
// Example task_output message
{
"type": "task_output",
"message": "TRANSCRIPT_USER:What's the weather like today?"
}
{
"type": "task_output",
"message": "TRANSCRIPT_AI:I'd be happy to help, but I don't have access to real-time weather data."
}
Ending a Session
To gracefully end a realtime session, send task_session_end:
{
"type": "task_session_end",
"tasktoken": "YOUR_SOCKET_ACCESS_TOKEN"
}
After sending this, the server will process any remaining audio, send final cost/transcript events, and then emit task_postprocess_end. Wait for task_postprocess_end before closing the WebSocket.
Safety: If the client disconnects without sending task_session_end, the server automatically terminates the session to prevent the pipeline from running indefinitely (and the provider from continuing to charge). Always send task_session_end explicitly for a clean shutdown.
Insufficient balance: If the wallet runs out of balance during a realtime session, the server automatically stops the session. You will still receive the final task_cost and task_end events.
Realtime Text to Speech
Build streaming text-to-speech apps with realtime AI models.
Overview
Realtime TTS models convert text into streaming audio. Unlike standard TTS that processes a full prompt and returns an audio file, realtime TTS streams AI-generated speech as a continuous PCM audio stream over a WebSocket connection — in real time. The text prompt is submitted via POST /Run, not over the WebSocket. The WebSocket carries only task events (task_info, task_stream_ready, etc.), binary
audio frames, and control messages (task_session_end).
No microphone is required. The flow is one-directional: text goes in, audio comes out.
The flow is:
- Run the realtime TTS model via POST /Run with your text prompt in the parameters
- Connect to the WebSocket and send
task_infowith yoursocketaccesstoken - Wait for
task_stream_ready— the model has loaded and is generating audio - Receive AI audio as binary frames and play them
- End the session with
task_session_endor wait for the stream to finish naturally
How It Differs from Realtime Voice Conversation
| Realtime Voice Conversation | Realtime Text to Speech | |
|---|---|---|
| Input | Microphone audio (streamed) | Text (sent with the run request) |
| Output | AI audio + transcripts | AI audio only |
| Direction | Bidirectional (client ↔ server) | Server → client only |
| Microphone | Required | Not required |
| Transcripts | TRANSCRIPT_USER: / TRANSCRIPT_AI: via task_output |
None |
| Use case | Interactive voice chat | Narration, voiceover, assistants |
Run Parameters
Each realtime TTS model has its own set of parameters. Use POST /Tool/Detail to discover the exact parameters for a specific model. See Model Parameters for parameter types.
Browse available realtime TTS models on the models page (filter by Realtime TTS category). Common parameters typically include the input text, voice selection, and output audio format. Example run:
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"text": "Hello, this is a realtime text-to-speech demo.",
"voice": "alloy",
"output_audio_format": "audio/pcm",
"output_audio_rate": "24000"
}'
Important: Parameters vary per model. Always check the model's detail page or use /Tool/Detail to get the exact parameter list before integrating. The run response returns a socketaccesstoken used to subscribe to the WebSocket.
Connection & Registration
After running the task, connect to the WebSocket and register with task_info:
var ws = new WebSocket("wss://socket.wiro.ai/v1");
ws.onopen = function() {
ws.send(JSON.stringify({
type: "task_info",
tasktoken: "YOUR_SOCKET_ACCESS_TOKEN"
}));
};
Note: Both standard and realtime models use type: "task_info" with tasktoken to register on the WebSocket. The registration flow is identical to Realtime Voice Conversation.
Realtime Events
During a realtime TTS session, you'll receive these WebSocket events:
| Event | Description |
|---|---|
task_stream_ready |
Session is ready — the model is generating audio and will begin sending chunks |
task_stream_end |
The model finished generating audio for the current segment |
task_cost |
Cost update — includes turnCost, cumulativeCost, and usage (raw cost breakdown from the model provider) |
task_end |
The model process has exited. Post-processing follows — wait for task_postprocess_end to close the connection. |
task_postprocess_end |
Post-processing is complete. Safe to close the WebSocket connection. |
No task_output events. Unlike voice conversation, TTS sessions do not produce transcript events. The input text is already known (you provided it), and the AI output is audio, not text.
Event Sequence
A typical TTS session produces events in this order:
task_stream_ready ← model is ready, audio chunks start arriving
[binary frames] ← PCM audio data (many frames)
task_stream_end ← audio generation complete for this segment
task_cost ← cost for this segment
task_end ← model process exiting
task_postprocess_end ← safe to close WebSocket
Audio Format
Audio flows in one direction only: server → client. The client does not send any audio.
| Property | Value |
|---|---|
| Format | PCM (raw, uncompressed) |
| Bit depth | 16-bit signed integer (Int16) |
| Sample rate | 24,000 Hz (24 kHz) |
| Channels | Mono (1 channel) |
| Byte order | Little-endian |
| Chunk size | Variable (typically 200 ms = 4,800 samples = 9,600 bytes) |
Binary Frame Format
Every binary WebSocket frame from the server is structured as:
[tasktoken]|[PCM audio data]
The pipe character | (0x7C) separates the token from the raw audio bytes. To extract the audio:
- Find the first
|byte in the binary frame - Everything after it is raw PCM Int16 audio data
- Convert Int16 samples to your playback format (e.g., Float32 for Web Audio API)
Client → server: In TTS mode, you do not send binary audio frames. The only messages you send are task_info (to register) and task_session_end (to end the session).
Receiving AI Audio
AI speech arrives as binary WebSocket frames in PCM Int16 24 kHz format. To play them:
- Check if the incoming message is binary (a
Blobin JavaScript,bytesin Python) before attempting JSON parse - Find the pipe
|separator and extract audio data after it - Convert Int16 → Float32 and create an
AudioBuffer - Schedule gapless playback using
AudioBufferSourceNodeto avoid clicks between chunks
Gapless Playback
Audio arrives in many small chunks. To play them seamlessly:
- Track a
nextPlayTimevariable initialized to0 - For each chunk, schedule it at
max(audioContext.currentTime, nextPlayTime) - Advance
nextPlayTimeby the chunk's duration - This ensures chunks play back-to-back with no gaps or overlaps
Ending a Session
To gracefully end a realtime TTS session, send task_session_end:
{
"type": "task_session_end",
"tasktoken": "YOUR_SOCKET_ACCESS_TOKEN"
}
After sending this, the server will finish any in-progress generation, send final cost events, and then emit task_postprocess_end. Wait for task_postprocess_end before closing the WebSocket.
For TTS sessions, the stream often ends naturally when the model finishes generating audio for the provided text. In this case, you'll receive task_stream_end followed by task_end without needing to send task_session_end. However, it's good practice to send it explicitly for a clean shutdown, especially if you want to stop playback early.
Safety: If the client disconnects without sending task_session_end, the server automatically terminates the session to prevent the pipeline from running indefinitely (and the provider from continuing to charge). Always send task_session_end explicitly for a clean shutdown.
Insufficient balance: If the wallet runs out of balance during a realtime session, the server automatically stops the session. You will still receive the final task_cost and task_end events.
Realtime Speech to Text
Transcribe live microphone audio into text in real time using streaming ASR models.
Overview
Realtime speech-to-text models convert streaming audio into text transcripts as the user speaks. Unlike Realtime Voice Conversation which produces two-way audio, this mode is audio in → text out only. There is no AI audio playback — the server returns transcript strings over the WebSocket.
The flow is:
- Run the realtime STT model via POST /Run to get a
socketaccesstoken - Connect to the WebSocket and send
task_infowith your token - Wait for
task_stream_ready— the model is ready to receive audio - Stream microphone audio as binary frames (client → server)
- Receive transcript text as
task_outputmessages withTRANSCRIPT_USER:prefix - End the session with
task_session_end
Key difference from Voice Conversation: No binary audio is sent back from the server. All server → client messages are JSON text events.
How It Differs from Realtime Voice Conversation
| Realtime Voice Conversation | Realtime Speech to Text | |
|---|---|---|
| Input | Microphone audio (streamed) | Microphone audio (streamed) |
| Output | AI audio + transcripts | Transcript text only |
| Direction | Bidirectional (client ↔ server) | Client → server audio, server → client text |
| Binary frames from server | Yes (AI audio) | No (all server messages are JSON) |
| Transcripts | TRANSCRIPT_USER: and TRANSCRIPT_AI: |
TRANSCRIPT_USER: only |
| Use case | Interactive voice chat | Live dictation, captioning, meeting transcription |
Run Parameters
Each realtime STT model has its own set of parameters. Use POST /Tool/Detail to discover the exact parameters for a specific model. See Model Parameters for parameter types.
Browse available realtime STT models on the models page (filter by Realtime STT category). Common parameters typically include language hints and audio format settings. Example run:
curl -X POST "https://api.wiro.ai/v1/Run/{owner-slug}/{model-slug}" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"language": "en",
"input_audio_format": "audio/pcm",
"input_audio_rate": "24000"
}'
Important: Parameters vary per model. Always check the model's detail page or use /Tool/Detail to get the exact parameter list before integrating. The run response returns a socketaccesstoken used to subscribe to the WebSocket.
Connection & Registration
After running the task, connect to the WebSocket and register with task_info:
var ws = new WebSocket("wss://socket.wiro.ai/v1");
ws.onopen = function() {
ws.send(JSON.stringify({
type: "task_info",
tasktoken: "YOUR_SOCKET_ACCESS_TOKEN"
}));
};
Note: Both standard and realtime models use type: "task_info" with tasktoken to register on the WebSocket.
Realtime Events
During a realtime speech-to-text session, you'll receive these WebSocket events:
| Event | Direction | Description |
|---|---|---|
task_stream_ready |
server → client | Session is ready — start sending microphone audio |
task_output |
server → client | Transcript text prefixed with TRANSCRIPT_USER: |
task_stream_end |
server → client | Transcription stream has ended — no more transcripts will arrive |
task_cost |
server → client | Cost update — includes turnCost, cumulativeCost, and usage breakdown |
task_end |
server → client | The model process has exited. Post-processing follows — wait for task_postprocess_end before closing. |
task_postprocess_end |
server → client | Post-processing complete — safe to close the WebSocket now. |
Unlike voice conversation, there are no binary audio frames from the server. Every server message is a JSON text event.
Audio Format
Audio flows in one direction only: client → server.
| Property | Value |
|---|---|
| Format | PCM (raw, uncompressed) |
| Bit depth | 16-bit signed integer (Int16) |
| Sample rate | 24,000 Hz (24 kHz) |
| Channels | Mono (1 channel) |
| Byte order | Little-endian |
| Chunk size | 4,800 samples (200 ms) = 9,600 bytes |
The server internally resamples to the model's native rate (e.g. 16 kHz for Voxtral). Always send at 24 kHz — the server handles conversion.
Binary Frame Format
Every binary WebSocket frame sent from the client is structured as:
[tasktoken]|[PCM audio data]
The pipe character | (0x7C) separates the token from the raw audio bytes. There are no binary frames from the server — all responses are JSON text.
Sending Microphone Audio
Capture microphone audio at 24 kHz using the Web Audio API with an AudioWorklet. Convert Float32 samples to Int16, prepend your task token, and send as a binary frame.
Key steps:
- Request microphone with
getUserMedia(enable echo cancellation and noise suppression) - Create an
AudioContextat 24,000 Hz sample rate - Use an AudioWorklet to buffer and convert samples to Int16
- Send each chunk as
tasktoken|pcm_databinary frame - Continue sending until you end the session
Tip: You can send audio continuously — the model handles silence detection and only returns transcripts when speech is detected.
Transcripts
Transcripts arrive as task_output messages with the TRANSCRIPT_USER: prefix. Each message contains a segment of transcribed speech:
{
"type": "task_output",
"message": "TRANSCRIPT_USER:What's the weather like today?"
}
{
"type": "task_output",
"message": "TRANSCRIPT_USER:I need to book a flight to New York."
}
Progressive Results
Transcripts arrive progressively as words and phrases are recognized — not just at segment boundaries. The model streams TRANSCRIPT_USER: messages word-by-word as speech is detected, so the client can display live, incremental results. To build a full transcript, concatenate all received messages:
var fullTranscript = [];
// Inside your message handler
if (msg.type === 'task_output' &&
typeof msg.message === 'string' &&
msg.message.startsWith('TRANSCRIPT_USER:')) {
var segment = msg.message.substring(16);
fullTranscript.push(segment);
console.log('Segment:', segment);
console.log('Full:', fullTranscript.join(' '));
}
Note: Unlike voice conversation, there is no TRANSCRIPT_AI: prefix. All transcripts are user speech.
Ending a Session
To gracefully end a realtime session, send task_session_end:
{
"type": "task_session_end",
"tasktoken": "YOUR_SOCKET_ACCESS_TOKEN"
}
After sending this, the server processes any remaining buffered audio, sends final transcript and cost events, and then emits task_postprocess_end. Wait for task_postprocess_end before closing the WebSocket.
Safety: If the client disconnects without sending task_session_end, the server automatically terminates the session to prevent the pipeline from running indefinitely (and the provider from continuing to charge). Always send task_session_end explicitly for a clean shutdown.
Insufficient balance: If the wallet runs out of balance during a realtime session, the server automatically stops the session. You will still receive the final task_cost and task_end events.
Files
Manage folders and upload files for use with AI models.
Overview
The Files API lets you organize and upload data that can be referenced in model runs. Common use cases include:
- Training data — upload datasets for fine-tuning models
- File inputs — provide images, audio, or documents as model inputs
- Batch processing — store files for repeated use across multiple runs
POST /File/FolderCreate
Creates a new folder to organize your uploaded files.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Folder name |
parentid |
string | No | Parent folder ID for nested structure (omit for root) |
Note: Folder names only allow letters, numbers, hyphens, and underscores (A-Z a-z 0-9 _ -).
Response
{
"result": true,
"errors": [],
"list": [{
"id": "folder-abc123",
"name": "training-data",
"parentid": "root-folder-id",
"size": "0",
"contenttype": "",
"addedtime": "1716276543"
}]
}
POST /File/Upload
Uploads a file using multipart/form-data. You can optionally assign it to a folder.
| Parameter | Type | Required | Description |
|---|---|---|---|
file |
file | Yes | The file to upload (multipart form field) |
folderid |
string | No | Target folder ID (uploads to user's default folder if omitted) |
File size limit: 100 MB per file.
Supported file types: Images (jpg, png, gif, jpeg, webp, heic), video (mp4, webm, mov), audio (mp3, wav, m4a), documents (pdf, csv, docx, xlsx, pptx, txt, md, epub), and ZIP archives (automatically extracted).
Response
{
"result": true,
"errors": [],
"list": [{
"id": "file-id",
"name": "dataset.csv",
"contenttype": "text/csv",
"size": "1048576",
"parentid": "folder-id",
"url": "https://cdn1.wiro.ai/...",
"addedtime": "1716276727",
"accesskey": "..."
}]
}
Using Files in Runs
Once uploaded, reference a file by its URL or ID in your model run parameters. For example, an image upscaler model might accept a
imageUrl parameter — pass the URL returned from the upload response.
{
"imageUrl": "https://files.wiro.ai/...",
"scale": 4
}
Pricing
Understand how billing works for AI model runs on Wiro.
Overview
When you run an AI model through Wiro, you are billed based on the type of work performed. Each model has its own pricing, visible on the model's page in the marketplace and on the pricing page. You pay only for successful runs — server errors are never billed.
Wiro uses a prepaid credit model. You add credits to your account and they are drawn down as you use models. Credits also determine your concurrency limit.
Billing Methods
Every model on Wiro uses one of the following billing methods. The method is set per model and determines how the cost is calculated.
Fixed-Rate Methods
| Billing Method | Code | How it works |
|---|---|---|
| Per Request | cpr |
Fixed cost per run, regardless of output. Most common for image generation, image editing, and simple models. |
| Per Second | cps |
Cost per second of processing time. When no dynamic pricing is set, this is the default — cost = elapsed seconds × cps rate. |
| Per Output | cpo |
Cost per output item generated. Multiple files = pay per file. No output = base price charged once. |
| Per Token | cpt |
Cost per token used. Total tokens (input + output) extracted from model's usage metadata. Used for LLM models. |
Usage-Based Methods
| Billing Method | Code | How it works |
|---|---|---|
| Per Pixel | cp-pixel |
Cost based on output resolution. Each 1,048,576 pixels (1024×1024) = one tier. Can include per-input-image costs (priceInput). |
| Per Audio Second | cp-audiosecondslength |
Cost per second of input audio duration. Duration measured via ffprobe. |
| Per Character | cp-promptlength |
Cost per character in the input prompt. Total = prompt length × price. |
| Per Video Second | cp-outputVideoLength |
Cost per second of generated output video. Duration measured via ffprobe. |
Special Methods
| Billing Method | Code | How it works |
|---|---|---|
| Per Realtime Turn | cp-realtimeturn |
For realtime voice models. Billing per conversation turn, deducted in real time during the session. |
| Model-Reported | cp-readoutput |
The model reports its own cost in stdout/stderr JSON output. |
Dynamic Pricing
Many models have dynamic pricing — the cost varies based on the input parameters you choose. For example, a video generation model might charge different rates depending on the resolution and duration you select.
The pricing is returned in the dynamicprice field of the Tool/List and Tool/Detail API responses as a JSON array:
[
{
"inputs": { "resolution": "720p", "duration": "5" },
"price": 0.13,
"priceMethod": "cpr"
},
{
"inputs": { "resolution": "1080p", "duration": "5" },
"price": 0.29,
"priceMethod": "cpr"
}
]
How Dynamic Pricing Works
Each entry in the dynamicprice array represents a pricing tier:
| Field | Type | Description |
|---|---|---|
inputs |
object | The input parameter combination this price applies to. Empty {} means the price applies to all configurations. |
price |
number | The cost in USD for this configuration. |
priceMethod |
string | The billing method code (see tables above). |
priceExtra |
number (optional) | Extra cost per additional tier. Used by cp-pixel — each additional 1MP tier costs this amount. |
priceInput |
number (optional) | Per-input cost. Used by cp-pixel — each input image incurs this cost per 1MP tier. |
When inputs contains specific parameter values (e.g. "resolution": "720p"), that price only applies when you run the model with those exact parameters. When inputs is empty ({}), it's a flat rate that applies regardless of input parameters.
Input matching also supports QUANTITY values (e.g. "QUANTITY:1", "QUANTITY:3") for models where the number of input files affects pricing.
Example: Video Generation Pricing
A video model might have pricing tiers based on resolution and duration:
| Resolution | Duration | Price |
|---|---|---|
| 480p | 5 seconds | $0.06 |
| 720p | 5 seconds | $0.13 |
| 1080p | 5 seconds | $0.29 |
| 480p | 10 seconds | $0.12 |
| 720p | 10 seconds | $0.26 |
| 1080p | 10 seconds | $0.58 |
Example: Simple Flat Pricing
An image generation model with a flat rate:
[{ "inputs": {}, "price": 0.03, "priceMethod": "cpr" }]
This means every run costs $0.03, regardless of parameters.
Fallback Pricing (Per-Second)
When a model does not have dynamicprice set, billing falls back to per-second pricing:
totalcost = elapsed_seconds × cps
Where cps (cost per second) is either the model's own rate or the queue group's default rate. The API also returns an approximatelycost field — an estimate based on the model's average run time:
approximatelycost = average_elapsed_seconds × cps
This gives you a rough idea of the expected cost before running the model.
Checking Prices
Via the API
Pricing information is included in both the Tool/List and Tool/Detail responses in the dynamicprice field. Use POST /Tool/Detail with the model's slugowner and slugproject to get full pricing details.
Via MCP
When using the MCP server, both search_models and get_model_schema tools return pricing information in their responses. Your AI assistant can check the cost before running a model.
Pricing Page
Browse and compare model prices interactively on the pricing page. Select a budget to see how many runs each model can perform.
What You Pay For
You are billed for successfully completed model runs. A run is successful when the task reaches task_postprocess_end status with pexit of "0". The actual cost is recorded in the task's totalcost field, which you can retrieve via Task/Detail.
What You Are Not Charged For
- Server errors — if a run fails due to a server-side error, no charge is incurred.
- Queue time — time spent waiting in the queue before processing starts is free.
- Cancelled tasks — tasks cancelled before processing completes are not billed.
Monitoring Your Spending
- Check the
totalcostfield in Task/Detail responses to see the cost of individual runs. - View your overall balance, usage history, and billing details in the Dashboard.
- When using MCP, the
get_tasktool returns the cost of completed runs.
Concurrency Limits
Understand and manage how many requests you can run simultaneously on Wiro.
Overview
Concurrency limits control how many tasks your account can process at the same time. When you reach your limit, the API returns an error response with code 96. You should wait for a running task to complete before submitting a new one, or add funds to increase your limit.
How It Works
Your concurrency limit is determined by your current account balance:
- When your balance is $250 or below, you can run concurrent tasks equal to 10% of your current USD balance (minimum 1).
- When your balance is above $250, there is no concurrency limit.
Examples
| Account Balance | Concurrent Task Limit |
|---|---|
| $10 | 1 concurrent task (minimum) |
| $50 | 5 concurrent tasks |
| $100 | 10 concurrent tasks |
| $150 | 15 concurrent tasks |
| $250 | 25 concurrent tasks |
| $251+ | Unlimited (no limit applied) |
The formula: max(1, floor(balance_usd * 0.10)). Once your balance exceeds $250, all limits are removed.
What Counts as Active
Only tasks that are actively being processed count toward your concurrency limit. A task is considered active from task_queue until it reaches a terminal status:
task_postprocess_end— task completed (success or failure)task_cancel— task was cancelled or killed
Once a task reaches either of these statuses, it no longer counts toward your limit.
API Response
When you hit the concurrency limit, the POST /Run endpoint returns an error with code 96:
{
"result": false,
"errors": [
{
"code": 96,
"message": "You have reached your concurrent task limit. With your current balance of $50.00, you can run up to 5 tasks at the same time. Add funds to increase your limit."
}
]
}
The error message includes your current balance and the calculated limit, so you know exactly how many concurrent tasks you can run.
Error Codes
| Code | Meaning | Action |
|---|---|---|
96 |
Concurrent task limit reached | Wait for a running task to finish, or add funds |
97 |
Insufficient balance | Add funds to your account |
Increasing Your Limit
To increase your concurrency limit, simply add credits to your account. Your limit is recalculated automatically based on your current balance at the time of each run request.
For enterprise needs or custom concurrency arrangements, contact support.
Best Practices
-
Check error code
96— if you get this error, wait for a running task to complete before submitting new ones. -
Use WebSocket for monitoring — instead of polling
/Task/Detailrepeatedly, connect via WebSocket to get real-time updates without extra API calls. -
Use
wait=falsein MCP — for long-running models (video, 3D), submit withwait=falseand check withget_taskto avoid holding connections. - Implement exponential backoff — if polling task status, start at 3 seconds and increase the interval for longer tasks.
Error Reference
Understand API error responses, error codes, and how to handle them.
Response Format
When an API request fails, the response includes result: false and an errors array:
{
"result": false,
"errors": [
{
"code": 97,
"message": "Insufficient balance"
}
]
}
All API responses return HTTP 200 — use the result field and error code to determine success or failure.
Error Codes
Error codes indicate the category of the problem. Use these for conditional logic in your application.
| Code | Category | Description |
|---|---|---|
0 |
General | Server-side errors, validation failures, missing parameters |
1 |
Not Found / Client | Resource not found or not accessible |
96 |
Concurrency Limit | Too many concurrent tasks for your balance. See Concurrency Limits |
97 |
Insufficient Balance | Not enough funds to run the model |
98 |
Authentication Required | Sign in required to access this model |
99 |
Token Invalid | Bearer token missing, invalid, or expired |
Authentication Errors
Returned when API key or bearer token authentication fails. All return HTTP 401.
| Error | Code | Message |
|---|---|---|
| API key not found | 0 |
Project authorization is not founded. |
| Signature required | 0 |
Project requires signature authentication. x-signature and x-nonce headers are required. |
| Invalid signature | 0 |
Project authorization is not valid. |
| IP not allowed | 0 |
Requested ip {ip} is not allowed. |
| Bearer token missing | 99 |
Authorization bearer token is not founded in headers. |
| Bearer token invalid | 99 |
Authorization bearer token is invalid. |
| Bearer token expired | 99 |
Authorization bearer token expired. |
| Endpoint not found | 0 |
Error parsing url. (HTTP 404) |
Run Errors
Returned by POST /Run/{owner}/{model}.
Balance & Limits
| Error | Code | Message | Action |
|---|---|---|---|
| Insufficient balance | 97 |
Insufficient balance | Add funds — minimum $0.50 required ($10 for training) |
| Concurrent task limit | 96 |
You have reached your concurrent task limit. With your current balance of ${balance}, you can run up to {maxConcurrent} tasks at the same time. | Wait for a task to finish, or add funds. See Concurrency Limits |
| Sign in required | 98 |
sign in to run this model | Model requires a registered account |
Validation Errors
| Error | Code | Message |
|---|---|---|
| Missing parameter | 0 |
Request parameter [{name}] required |
| Invalid number | 0 |
Request parameter [{name}] must be integer or float |
| Out of range | 0 |
Request parameter [{name}] must be between {min} and {max} |
| File required | 0 |
Request files [{name}] required |
| Invalid request body | 0 |
Request parameters are invalid. |
Model Access Errors
| Error | Code | Message |
|---|---|---|
| Model not accessible | 1 |
tool-not-accessible |
| Model not found | 1 |
slug-owner-project-not-exist |
| Account suspended | 0 |
Your account has been suspended. Please contact support. |
| Permission denied | 0 |
You don't have any permission for this action. |
Task Errors
Returned by POST /Task/Detail, POST /Task/Cancel, POST /Task/Kill, and POST /Task/InputOutputDelete.
| Error | Code | Message | Endpoint |
|---|---|---|---|
| Task not found | 1 |
There is no task yet. | Detail, Cancel, Kill, InputOutputDelete |
| Missing identifier | 0 |
taskid or socketaccesstoken is required | Detail |
| Not cancellable | 1 |
Task is not in a cancellable state. | Cancel |
| Kill failed | 1 |
Task could not be killed: {reason} | Kill |
| Task not completed | 1 |
Task must be completed or cancelled before deleting files | InputOutputDelete |
| Permission denied | 0 |
You don't have any permission for this action. | All |
Error Handling
Always check result first, then inspect the code in the errors array:
const data = await response.json();
if (!data.result) {
const error = data.errors[0];
switch (error.code) {
case 96:
console.log('Concurrent limit — wait for a task to finish');
break;
case 97:
console.log('Insufficient balance — add funds');
break;
case 98:
console.log('Sign in required');
break;
default:
console.log('Error:', error.message);
}
}
Retry Strategy
| Code | Retryable | Strategy |
|---|---|---|
0 (validation) |
No | Fix the request parameters |
0 (server) |
Yes | Retry with exponential backoff |
1 |
No | Check model slug or task token |
96 |
Yes | Wait for a running task to complete |
97 |
No | Add funds, then retry |
98 |
No | Sign in or use authenticated credentials |
99 |
No | Check your API key or bearer token |
FAQ
Common questions about using the Wiro API. If you can't find the answer here, contact support.
Sign up at wiro.ai, then create a project at wiro.ai/panel/project. Your API key (and secret, if signature-based) are displayed once — copy and store them securely.
Signature-Based is recommended — it uses HMAC-SHA256 so your API secret never leaves your environment. API Key Only is simpler and fine for server-side applications where you control the environment. See Authentication for details.
Yes. Every account has a concurrency limit that controls how many tasks can run at the same time. The limit scales automatically based on your account balance. See Concurrency Limits for the full table.
No. If a task fails (non-zero pexit), you are not charged. Only successfully completed tasks are billed.
Use the POST /Tool/Detail endpoint — the response includes the model's pricing information. If you're using the MCP server, the search_models and get_model_schema tools also return pricing.
Output files are stored on Wiro's CDN and available for a limited time. Download and store any files you need to keep long-term. See Files for details on file management.
Yes. Output URLs returned by Wiro are publicly accessible. Anyone with the URL can access the file. If you need private storage, download the files to your own infrastructure.
Connect to the WebSocket at wss://socket.wiro.ai/v1 and register with the socketaccesstoken from your run response. You'll receive events as the task progresses. For simpler integrations, you can poll the Task Detail endpoint.
LLM models return their response in two places: as merged text in debugoutput, and as a structured entry in the outputs array with contenttype: "raw" containing separate thinking and answer arrays. For streaming, each token arrives as a separate task_output WebSocket event. See LLM & Chat Streaming for details.
Yes. For fileinput and multifileinput parameters, use the {id}Url suffix (e.g., inputImageUrl). For combinefileinput, pass URLs directly in the original parameter. You can also pass a URL directly to any file parameter if the {id}Url field doesn't exist. See Model Parameters.
pexit is the process exit code — "0" means success, any other value means failure. It's the most reliable way to determine if a task succeeded. Always check pexit in the task_postprocess_end event or Task Detail response. See Tasks.
No. The Wiro MCP server is free. You only pay for the model runs you trigger, at standard pricing.
Yes. Install the Wiro AI community node in your n8n instance to access all Wiro models as drag-and-drop nodes in your workflows.
Yes. All models support an optional callbackUrl parameter. When provided, Wiro will POST the task result to your URL when the task completes. See Webhook Callback in Model Parameters.
Code Examples
Complete end-to-end examples in all 9 supported languages.
Overview
Each example below demonstrates the full Wiro workflow: authenticate, run a model, poll for task completion, and retrieve the result. Choose your preferred language from the tabs.
- curl — Shell scripting with bash
- Python — Using the
requestslibrary - Node.js — Using
axios - PHP — Using cURL functions
- C# — Using
HttpClient(.NET 6+) - Swift — Using async/await
URLSession - Dart — Using the
httppackage - Kotlin — Using
java.net.http - Go — Using the standard library
net/http
Full Examples
Select a language tab to see the complete example. All examples perform the same steps:
- Set up authentication headers
- Run a model (
POST /Run/{owner-slug}/{model-slug}) - Poll the task status (
POST /Task/Detail) - Print the final output
Wiro MCP Server
Connect AI coding assistants to Wiro's AI models via the Model Context Protocol.
What is MCP?
Model Context Protocol (MCP) is an open standard that lets AI assistants use external tools directly. With the Wiro MCP server, your AI assistant can search models, run inference, track tasks, and upload files — all without leaving your editor.
The hosted MCP server is available at mcp.wiro.ai/v1 and works with any MCP-compatible client, including Cursor, Claude Code, Claude Desktop, and Windsurf. Every request uses your own API key — nothing is stored on the server.
You need a Wiro API key to use the MCP server. If you don't have one yet, create a project here.
Links
- Model Context Protocol (MCP) — open standard specification
- GitHub: wiroai/Wiro-MCP — source code & self-hosting instructions
- npm: @wiro-ai/wiro-mcp — npm package
- Wiro Model Catalog — browse all available models
- Create API Key — get started in seconds
Setup
Connect your AI assistant to Wiro's MCP server. Pick your client:
Cursor
- Open MCP Settings — Use
Cmd+Shift+P(Ctrl+Shift+Pon Windows) and search for "Open MCP settings". -
Add the Wiro server — Add the following to your
mcp.jsonfile:Signature Auth (if your project uses signature-based authentication):
{ "mcpServers": { "wiro": { "url": "https://mcp.wiro.ai/v1", "headers": { "Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET" } } } }API Key Only Auth (if your project uses API Key Only authentication):
{ "mcpServers": { "wiro": { "url": "https://mcp.wiro.ai/v1", "headers": { "Authorization": "Bearer YOUR_API_KEY" } } } } - Restart Cursor — Save the file and restart Cursor to activate the connection.
Claude Code
Run this command in your terminal:
claude mcp add --transport http wiro \
https://mcp.wiro.ai/v1 \
--header "Authorization: Bearer YOUR_API_KEY:YOUR_API_SECRET"
That's it. Claude Code will now have access to all Wiro tools.
Claude Desktop
Add the following to your claude_desktop_config.json:
{
"mcpServers": {
"wiro": {
"url": "https://mcp.wiro.ai/v1",
"headers": {
"Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET"
}
}
}
}
Windsurf
Open Settings → MCP and add a new server:
{
"mcpServers": {
"wiro": {
"serverUrl": "https://mcp.wiro.ai/v1",
"headers": {
"Authorization": "Bearer YOUR_API_KEY:YOUR_API_SECRET"
}
}
}
}
Other MCP Clients
The Wiro MCP server uses the Streamable HTTP transport at:
https://mcp.wiro.ai/v1
Authentication is via the Authorization header:
Authorization: Bearer YOUR_API_KEY:YOUR_API_SECRET
Or for API Key Only auth: Bearer YOUR_API_KEY
Credentials are sent as plain text — no base64 encoding needed. Any MCP client that supports Streamable HTTP transport can connect.
Authentication
The MCP server supports both Wiro authentication types. Your project's auth type determines what credentials you provide.
Signature-Based (Recommended)
More secure. Requires both API key and API secret. Pass them as plain text separated by a colon:
Authorization: Bearer YOUR_API_KEY:YOUR_API_SECRET
API Key Only
Simpler. Only requires the API key:
Authorization: Bearer YOUR_API_KEY
No base64 encoding needed. Credentials are sent per-request and are never stored. The server is fully stateless.
Available Tools
The MCP server exposes 11 tools organized in four categories. Your AI assistant picks the right tool automatically.
Model slugs: Use the clean/lowercase format owner/model (e.g. openai/sora-2, wiro/virtual-try-on). These correspond to the cleanslugowner/cleanslugproject values returned by search_models.
Discovery
| Tool | Description |
|---|---|
search_models |
Search Wiro's model catalog by keyword, category, or owner |
get_model_schema |
Get the full parameter schema and pricing for any model |
recommend_model |
Describe what you want to build and get model recommendations ranked by relevance |
explore |
Browse curated AI models organized by category — featured, recently added, popular |
Execution
| Tool | Description |
|---|---|
run_model |
Run any model and wait for the result, or submit and get a task token |
Task Management
| Tool | Description |
|---|---|
get_task |
Check task status, outputs, cost, and elapsed time |
get_task_price |
Get the cost of a completed task — shows whether it was billed and the total charge |
cancel_task |
Cancel a task still in the queue |
kill_task |
Kill a task that is currently running |
Utility
| Tool | Description |
|---|---|
upload_file |
Upload a file from a URL to Wiro. Most models accept direct URLs without uploading first — use this for reuse across runs. |
search_docs |
Search the Wiro documentation for guides, API references, and examples |
Examples
Generate an image
"Generate a photorealistic image of a mountain lake at golden hour"
The assistant will use search_models → get_model_schema → run_model and return the image URL.
Generate a video
"Create a 5-second cinematic video of a drone shot over mountains using Kling V3"
The assistant will use get_model_schema → run_model (wait=false) → get_task to poll for the result.
Find models
"What models are available for text-to-video?"
The assistant will call search_models with categories filter.
How It Works
The MCP server is stateless. Each request is fully isolated:
- Your AI assistant sends a request to
mcp.wiro.ai/v1with your credentials - The server calls the Wiro API on your behalf
- Results are returned to your assistant
All tools are dynamic — they fetch model data at runtime. New models are instantly available via MCP.
Tool Reference
search_models
Search Wiro's model catalog by keyword, category, owner, or any combination. Calls POST /Tool/List on the Wiro API.
| Parameter | Type | Description |
|---|---|---|
search |
string (optional) | Free-text search, e.g. "flux", "video generation" |
categories |
string[] (optional) | Filter by category: text-to-image, text-to-video, image-to-video, llm, text-to-speech, image-editing, etc. |
slugowner |
string (optional) | Filter by model owner slug, e.g. "openai", "stability-ai", "klingai" |
sort |
string (optional) | Sort by: relevance, time, ratedusercount, commentcount, averagepoint |
start |
number (optional) | Pagination offset (default 0) |
limit |
number (optional) | Max results (default 20, max 100) |
Returns a list of models with their cleanslugowner/cleanslugproject (the slug you pass to other tools), title, description, categories, and pricing.
get_model_schema
Get the full parameter schema for a specific model. Calls POST /Tool/Detail on the Wiro API.
| Parameter | Type | Description |
|---|---|---|
model |
string | Model slug using clean/lowercase format: "owner/model". Use cleanslugowner/cleanslugproject from search_models. Examples: "openai/sora-2", "black-forest-labs/flux-2-pro", "wiro/virtual-try-on" |
Returns the model's parameter groups, each containing items with id, type (text, textarea, select, range, fileinput, etc.), label, required, options, default, and note. Also includes pricing information. Use these to construct the params object for run_model.
recommend_model
Describe what you want to create and get model recommendations ranked by relevance. Calls POST /Tool/List on the Wiro API with relevance sorting.
| Parameter | Type | Description |
|---|---|---|
task |
string | What you want to do, e.g. "generate a photorealistic portrait", "upscale an image to 4K", "transcribe audio to text" |
Returns a list of recommended models with slugs, descriptions, categories, and pricing — sorted by relevance to your task.
explore
Browse curated AI models on Wiro, organized by category. Calls POST /Tool/Explore on the Wiro API. No parameters required.
Returns models grouped into curated sections like "Recently Added", "Image Generation", "Video", etc. Each model includes its slug, description, categories, and rating. Use this to discover what's available without searching.
run_model
Run any AI model on Wiro. Calls POST /Run/{owner}/{model} on the Wiro API.
| Parameter | Type | Description |
|---|---|---|
model |
string | Model slug in clean/lowercase format. Same as get_model_schema. Examples: "openai/sora-2", "klingai/kling-v3" |
params |
object | Model-specific parameters as key-value pairs. Use get_model_schema to discover accepted fields. Common: prompt, negativePrompt, width, height, aspectRatio |
wait |
boolean (optional) | If true (default), polls POST /Task/Detail until the task completes and returns the result. If false, returns the task token immediately for async monitoring via get_task. |
timeout_seconds |
number (optional) | Max seconds to wait (default 120, max 600). Only applies when wait=true. |
When wait=true, returns the final task result including pexit (exit code, "0" = success), outputs (CDN URLs for generated files), and debugoutput (LLM text responses).
When wait=false, returns taskid and tasktoken — use get_task to check progress.
get_task
Check task status and get results. Calls POST /Task/Detail on the Wiro API.
| Parameter | Type | Description |
|---|---|---|
tasktoken |
string (optional) | The task token returned from run_model |
taskid |
string (optional) | The task ID (alternative to tasktoken) |
Returns the task's current status, pexit (process exit code), outputs (file URLs), debugoutput (LLM responses), elapsedseconds, and totalcost.
Determining success: Check pexit — "0" means success, any other value means failure. For LLM models, the response is available as structured content in outputs (with contenttype: "raw") and as merged text in debugoutput. See Tasks for the full task lifecycle.
get_task_price
Get the cost of a completed task. Calls POST /Task/Detail on the Wiro API and returns billing information.
| Parameter | Type | Description |
|---|---|---|
tasktoken |
string (optional) | The task token returned from run_model |
taskid |
string (optional) | The task ID (alternative to tasktoken) |
Returns the task's billing status, total cost, and duration. Only successful tasks (pexit: "0") are billed — failed tasks show $0 with a clear explanation that they were not charged.
cancel_task
Cancel a task that is still queued (before worker assignment). Calls POST /Task/Cancel on the Wiro API.
| Parameter | Type | Description |
|---|---|---|
tasktoken |
string | The task token to cancel |
Tasks that have already been assigned to a worker cannot be cancelled — use kill_task instead.
kill_task
Kill a task that is currently running (after worker assignment). Calls POST /Task/Kill on the Wiro API.
| Parameter | Type | Description |
|---|---|---|
tasktoken |
string | The task token to kill |
The worker will stop processing and the task will move to task_cancel status.
upload_file
Upload a file from a URL to Wiro for use as model input. Downloads the file and uploads it via POST /File/Upload.
Tip: Most models accept direct URLs in file parameters (e.g. inputImage, inputImageUrl) — you don't need to upload first. Use upload_file when you need to reuse the same file across multiple runs or when the model specifically requires a Wiro-hosted file. See Model Parameters for details.
| Parameter | Type | Description |
|---|---|---|
url |
string | URL of the file to upload (image, audio, video, document) |
file_name |
string (optional) | Custom filename. If not provided, derived from the URL. |
Returns the uploaded file's Wiro URL, which can be passed to any model parameter that accepts a file.
search_docs
Search the Wiro documentation for guides, API references, and code examples.
| Parameter | Type | Description |
|---|---|---|
query |
string | What you're looking for, e.g. "how to upload a file", "websocket", "authentication", "LLM streaming" |
Returns relevant documentation sections matching your query.
FAQ
What models can I use?
All models in the Wiro catalog.
Is my API key stored?
No. The server is fully stateless. Credentials are sent per-request and never stored.
Does it cost extra?
No. The MCP server is free. You pay for model runs at standard pricing.
Can I self-host?
Yes. See Self-Hosted MCP for instructions.
Self-Hosted MCP
Run the Wiro MCP server locally on your own machine using npx.
Quick Start
Add to your AI assistant's MCP config:
{
"mcpServers": {
"wiro": {
"command": "npx",
"args": ["-y", "@wiro-ai/wiro-mcp"],
"env": {
"WIRO_API_KEY": "your-api-key",
"WIRO_API_SECRET": "your-api-secret"
}
}
}
}
That's it. Your assistant now has access to all Wiro AI models.
Setup
Add the self-hosted MCP server to your AI assistant:
Cursor
Open MCP settings (Cmd+Shift+P → "Open MCP settings") and add:
{
"mcpServers": {
"wiro": {
"command": "npx",
"args": ["-y", "@wiro-ai/wiro-mcp"],
"env": {
"WIRO_API_KEY": "your-api-key",
"WIRO_API_SECRET": "your-api-secret"
}
}
}
}
Claude Code
claude mcp add wiro -- npx -y @wiro-ai/wiro-mcp
Then set environment variables:
export WIRO_API_KEY="your-api-key"
export WIRO_API_SECRET="your-api-secret"
Claude Desktop
Add to claude_desktop_config.json:
{
"mcpServers": {
"wiro": {
"command": "npx",
"args": ["-y", "@wiro-ai/wiro-mcp"],
"env": {
"WIRO_API_KEY": "your-api-key",
"WIRO_API_SECRET": "your-api-secret"
}
}
}
}
Windsurf
Add to your MCP settings:
{
"mcpServers": {
"wiro": {
"command": "npx",
"args": ["-y", "@wiro-ai/wiro-mcp"],
"env": {
"WIRO_API_KEY": "your-api-key",
"WIRO_API_SECRET": "your-api-secret"
}
}
}
}
Other Clients
Any MCP client that supports the stdio transport can connect. The command is:
npx -y @wiro-ai/wiro-mcp
Set WIRO_API_KEY and WIRO_API_SECRET as environment variables in your client's MCP configuration.
Authentication
Signature-Based (Recommended)
Provide both API key and secret:
WIRO_API_KEY=your-api-key
WIRO_API_SECRET=your-api-secret
API Key Only
Omit WIRO_API_SECRET:
WIRO_API_KEY=your-api-key
Available Tools
The self-hosted server provides the same 11 tools as the hosted MCP server. Your AI assistant picks the right tool automatically.
Model slugs: Use the clean/lowercase format owner/model (e.g. openai/sora-2, wiro/virtual-try-on). These correspond to cleanslugowner/cleanslugproject values returned by search_models.
Discovery
| Tool | API Endpoint | What it does |
|---|---|---|
search_models |
POST /Tool/List |
Search and browse AI models by keyword, category, or owner. Returns model slugs, titles, descriptions, categories, and pricing. |
get_model_schema |
POST /Tool/Detail |
Get full parameter schema and pricing for any model — parameter names, types, options, defaults, and required fields. |
recommend_model |
POST /Tool/List |
Describe what you want to build and get model recommendations ranked by relevance. |
explore |
POST /Tool/Explore |
Browse curated AI models organized by category. No parameters needed. |
Execution
| Tool | API Endpoint | What it does |
|---|---|---|
run_model |
POST /Run/{owner}/{model} |
Run any model with parameters. With wait=true (default), polls until complete and returns outputs. With wait=false, returns task token for async monitoring. |
Task Management
| Tool | API Endpoint | What it does |
|---|---|---|
get_task |
POST /Task/Detail |
Check task status, pexit (exit code), outputs (CDN URLs), debugoutput (LLM responses), elapsed time, and cost. pexit="0" means success. |
get_task_price |
POST /Task/Detail |
Get the cost of a completed task. Shows whether it was billed and the total charge. Only successful tasks (pexit: "0") are billed. |
cancel_task |
POST /Task/Cancel |
Cancel a task still in queue (before worker assignment). |
kill_task |
POST /Task/Kill |
Kill a running task (after worker assignment). Task moves to task_cancel status. |
Utility
| Tool | API Endpoint | What it does |
|---|---|---|
upload_file |
POST /File/Upload |
Upload a file from a URL to Wiro. Most models accept direct URLs without uploading first. |
search_docs |
Wiro Docs | Search the Wiro documentation for guides, API references, and examples. |
See the Wiro MCP Server page for detailed parameter tables and examples.
Environment Variables
| Variable | Required | Description |
|---|---|---|
WIRO_API_KEY |
Yes | Your Wiro project API key |
WIRO_API_SECRET |
No | API secret (for signature auth) |
WIRO_API_BASE_URL |
No | Override API URL (default: https://api.wiro.ai/v1) |
GitHub & npm
- GitHub: github.com/wiroai/Wiro-MCP
- npm: @wiro-ai/wiro-mcp
Using as a Library
import { createMcpServer, WiroClient } from '@wiro-ai/wiro-mcp';
const client = new WiroClient('your-api-key', 'your-api-secret');
const server = createMcpServer(client);
Self-Hosting on Your Server
git clone https://github.com/wiroai/Wiro-MCP.git
cd Wiro-MCP
npm install
npm run build
export WIRO_API_KEY="your-api-key"
export WIRO_API_SECRET="your-api-secret"
node dist/index.js
Requires Node.js 18 or later. Create a project to get API keys.
Node.js Library
Use Wiro AI models directly in your Node.js or TypeScript projects with a simple API client.
Overview
The @wiro-ai/wiro-mcp package exports a WiroClient class that you can use as a standalone API client — no MCP setup required. It handles authentication (both signature-based and API key only), model discovery, execution, task polling, and file uploads.
Links
Installation
npm install @wiro-ai/wiro-mcp
Requires Node.js 18 or later.
Quick Start
import { WiroClient } from '@wiro-ai/wiro-mcp/client';
const client = new WiroClient('YOUR_API_KEY', 'YOUR_API_SECRET');
// Run an image generation model
const run = await client.runModel('google/nano-banana-pro', {
prompt: 'A futuristic city at sunset',
aspectRatio: '16:9',
resolution: '2K'
});
if (!run.result) {
console.log('Run failed:', run.errors);
process.exit(1);
}
// Wait for the result (polls Task/Detail until complete)
const result = await client.waitForTask(run.socketaccesstoken);
const task = result.tasklist[0];
if (task.pexit === '0') {
console.log('Output:', task.outputs[0].url);
} else {
console.log('Failed:', task.pexit);
}
Authentication
The client supports both Wiro authentication methods:
Signature-Based (Recommended)
Provide both API key and secret. HMAC-SHA256 signatures are generated automatically per request.
const client = new WiroClient('your-api-key', 'your-api-secret');
API Key Only
Omit the secret for simpler server-side usage.
const client = new WiroClient('your-api-key');
Custom Base URL
Override the API endpoint if needed (third parameter):
const client = new WiroClient('key', 'secret', 'https://custom-api.example.com/v1');
Available Methods
| Method | Description |
|---|---|
searchModels(params?) |
Search and browse models by keyword, category, or owner. |
getModelSchema(model) |
Get full parameter schema and pricing for a model. |
explore() |
Browse curated models organized by category. |
runModel(model, params) |
Run a model. Returns task ID and socket access token. |
waitForTask(tasktoken, timeoutMs?) |
Poll until the task completes. Default timeout: 120 seconds. |
getTask({ tasktoken?, taskid? }) |
Get current task status and outputs. |
cancelTask(tasktoken) |
Cancel a task still in the queue. |
killTask(tasktoken) |
Kill a running task. |
uploadFile(url, fileName?) |
Upload a file from a URL for use as model input. |
Examples
Search Models
const models = await client.searchModels({
search: 'image generation',
categories: ['text-to-image'],
limit: 5
});
for (const model of models.tool) {
console.log(`${model.cleanslugowner}/${model.cleanslugproject} — ${model.title}`);
}
Get Model Parameters
const detail = await client.getModelSchema('google/nano-banana-pro');
const model = detail.tool[0];
if (model.parameters) {
console.log('Parameters:');
for (const group of model.parameters) {
for (const param of group.items) {
console.log(` ${param.id} (${param.type}) ${param.required ? '— required' : ''}`);
}
}
}
Run an LLM
const run = await client.runModel('openai/gpt-5-2', {
prompt: 'Explain quantum computing in 3 sentences'
});
const result = await client.waitForTask(run.socketaccesstoken);
const task = result.tasklist[0];
if (task.pexit === '0') {
// Merged text
console.log(task.debugoutput);
// Structured thinking/answer
const output = task.outputs[0];
if (output.contenttype === 'raw') {
console.log('Thinking:', output.content.thinking);
console.log('Answer:', output.content.answer);
}
}
Upload a File and Use It
Tip: Most models accept direct URLs in file parameters — you can pass inputImage: 'https://example.com/photo.jpg' directly without uploading. Use uploadFile() when you need to reuse files across multiple runs.
// Upload an image
const upload = await client.uploadFile('https://example.com/photo.jpg');
const fileUrl = upload.list[0].url;
// Use it in a model run
const run = await client.runModel('wiro/virtual-try-on', {
inputImageHuman: fileUrl,
inputImageClothes: 'https://example.com/shirt.jpg'
});
const result = await client.waitForTask(run.socketaccesstoken);
console.log('Output:', result.tasklist[0].outputs[0].url);
Poll Manually
const run = await client.runModel('klingai/kling-v3', {
prompt: 'A drone shot over mountains',
duration: '5',
aspectRatio: '16:9'
});
// Poll manually instead of using waitForTask
const interval = setInterval(async () => {
const detail = await client.getTask({ tasktoken: run.socketaccesstoken });
const task = detail.tasklist[0];
console.log('Status:', task.status);
if (task.status === 'task_postprocess_end') {
clearInterval(interval);
if (task.pexit === '0') {
console.log('Video:', task.outputs[0].url);
}
}
}, 5000);
TypeScript
All types are exported from the package:
import { WiroClient } from '@wiro-ai/wiro-mcp/client';
import type {
RunModelResult,
TaskDetailResponse,
Task,
TaskOutput,
ToolListResponse,
ToolDetailResponse,
ToolListItem,
SearchModelsParams,
} from '@wiro-ai/wiro-mcp/client';
| Type | Description |
|---|---|
RunModelResult |
Response from runModel() — taskid, socketaccesstoken. |
TaskDetailResponse |
Response from getTask() / waitForTask() — contains tasklist array. |
Task |
Individual task object — status, pexit, outputs, debugoutput, etc. |
TaskOutput |
Output entry — file (name, url) or LLM (contenttype: "raw", content). |
ToolListResponse |
Response from searchModels() — contains tool array. |
ToolDetailResponse |
Response from getModelSchema() — contains model with parameters. |
SearchModelsParams |
Search parameters — search, categories, slugowner, limit, etc. |
n8n Wiro Integration
Use all Wiro AI models directly in your n8n workflows — video, image, audio, LLM, 3D, and more.
Overview
n8n is a powerful workflow automation platform. The Wiro AI community node gives you access to all Wiro AI models as individual nodes you can drag and drop into any workflow.
Each model is a separate node — so you get dedicated parameters, descriptions, and output handling for every model without any configuration hassle.
Links
- npm: @wiro-ai/n8n-nodes-wiroai
- GitHub: wiroai/n8n-nodes-wiroai
- n8n Community Nodes Installation Guide
- Wiro Model Catalog — browse all available models
Available Model Categories
| Category | Models | Examples |
|---|---|---|
| Video Generation | Text-to-video, image-to-video | Sora 2, Veo 3, Kling V3, Seedance, Hailuo, PixVerse, Runway |
| Image Generation | Text-to-image, style transfer | Imagen V4, Flux 2 Pro, Seedream, Nano Banana, SDXL |
| Image Editing | Try-on, face swap, background removal | Virtual Try-On, Face Swap, Inpainting, Style Transfer |
| Audio & Speech | TTS, STT, voice clone, music | ElevenLabs TTS, Gemini TTS, Whisper STT, Voice Clone |
| LLM Chat | Chat completion, RAG | GPT-5, Gemini 3, Qwen 3.5, RAG Chat |
| 3D Generation | Image/text to 3D | Trellis 2, Hunyuan3D 2.1 |
| Translation | Multi-language with image support | Gemma-based (4B, 12B, 27B) |
| E-Commerce | Product photos, ads, templates | Product Photoshoot, Shopify Templates, UGC Creator |
| HR Tools | CV analysis, job descriptions | CV Evaluator, Resume Parser, Culture Fit |
Installation
Install the community node package in your n8n instance:
Via n8n UI (Recommended)
- Open your n8n instance Navigate to your self-hosted or cloud n8n dashboard
- Go to Settings Open Settings → Community Nodes
-
Install the node Click Install a community node and enter:
@wiro-ai/n8n-nodes-wiroai - Confirm Click Install and wait for completion
Via Command Line
npm install @wiro-ai/n8n-nodes-wiroai
Restart n8n after installation.
Authentication
The node supports both Wiro authentication methods:
- Add credentials Go to Credentials → Add new → Wiro API in n8n
- Select auth method Signature-Based — enter API key + secret (recommended) | API Key Only — enter API key only
- Save Click Save to store your credentials
Get your credentials at wiro.ai/panel/project.
Usage
Each Wiro model appears as a separate node in the n8n node picker. Search for "Wiro" or the model name to find it.
Example: Generate a Video with Sora 2
- Add the Wiro - Sora 2 Pro node to your workflow
- Connect your Wiro credentials
-
Set the parameters:
- Prompt:
A cat astronaut floating in space - Seconds:
8 - Resolution:
1080p
- Prompt:
- Run the workflow
The node returns the task result with output URLs:
{
"taskid": "abc123",
"status": "completed",
"url": "https://cdn1.wiro.ai/xyz/0.mp4"
}
Example: Transcribe Audio with Whisper
- Add the Wiro - Whisper Large 3 node
- Connect an audio file from a previous node or provide a URL
- Select language and output format
- Run — get the transcribed text
Example: LLM Chat with GPT-5
- Add the Wiro - GPT-5 node
- Set your prompt and system instructions
- Run — get the AI response
Compatibility
| Requirement | Version |
|---|---|
| n8n | v1.0+ |
| Node.js | v18+ |
| Package | @wiro-ai/n8n-nodes-wiroai@latest |
Agent Overview
Deploy and manage autonomous AI agents through a single API.
What are Wiro Agents?
Wiro Agents are autonomous AI assistants that run persistently in isolated containers. Unlike one-shot model runs, agents maintain conversation memory, connect to external services, and use tools to complete tasks on your behalf — all managed through the API.
The system has two layers:
- Agent templates (the catalog) — Pre-built agent definitions published by Wiro. Each template defines the agent's capabilities, required credentials, tools, and pricing. Browse the catalog with
POST /Agent/List. - UserAgent instances (your deployments) — When you deploy an agent template, Wiro creates a personal instance tied to your account. Each instance runs in its own container with its own credentials, configuration, conversation history, and billing.
Every instance is fully isolated. Your credentials, conversations, and data are never shared with other users.
Base URL
https://api.wiro.ai/v1
Authentication
Agents use the same authentication as the rest of the Wiro API. Include your key in every request:
| Method | Header |
|---|---|
| API Key | x-api-key: YOUR_API_KEY |
| Bearer Token | Authorization: Bearer YOUR_API_KEY |
Public endpoints — Agent/List and Agent/Detail are catalog endpoints and do not require authentication. You can browse available agents without an API key.
Authenticated endpoints — All UserAgent/* endpoints (Deploy, MyAgents, Detail, Update, Start, Stop, CreateExtraCreditCheckout, CancelSubscription, UpgradePlan, RenewSubscription) require a valid API key.
For full details, see Authentication.
Agent Lifecycle
Deploying and running an agent follows this flow:
- Browse — call
POST /Agent/Listto discover available agents in the catalog - Deploy — call
POST /UserAgent/Deploywith the agent's guid, title,useprepaid: true, andplan("starter"or"pro"). The subscription cost is deducted from your wallet immediately. - Configure — if the agent requires credentials (API keys, OAuth tokens), call
POST /UserAgent/Updateto provide them. See Agent Credentials for details - Start — call
POST /UserAgent/Startto queue the agent for launch - Running — the agent's container starts and the agent becomes available for conversation
- Chat — send messages via
POST /UserAgent/Message/Send. See Agent Messaging for the full messaging API
UserAgent Statuses
Every deployed agent instance has a numeric status that reflects its current state:
| Status | Name | Description |
|---|---|---|
0 |
Stopped | Agent is not running. Call Start to launch it. |
1 |
Stopping | Agent is shutting down. Wait for it to reach Stopped before taking action. |
2 |
Queued | Agent is queued and waiting for a worker to pick it up. |
3 |
Starting | A worker has accepted the agent and is spinning up the container. |
4 |
Running | Agent is live and ready to receive messages. |
5 |
Error | Agent encountered an error during execution. Call Start to retry. |
6 |
Setup Required | Agent needs credentials or configuration before it can start. Call Update to provide them. |
Automatic Restart (restartafter)
When you update an agent's configuration while it is Running (status 3 or 4), the system automatically triggers a restart cycle: the agent is moved to Stopping (status 1) with restartafter set to true. Once the container fully stops, the system automatically re-queues it, applying the new configuration on startup.
This means you can update credentials or settings on a running agent without manually stopping and starting it.
Endpoints
POST /Agent/List
Lists available agents in the catalog. This is a public endpoint — no authentication required.
| Parameter | Type | Required | Description |
|---|---|---|---|
search |
string | No | Full-text search across agent titles and descriptions |
category |
string | No | Filter by category (e.g. "productivity", "social-media") |
sort |
string | No | Sort column: id, title, slug, status, createdat, updatedat, totalrun, activerun. Default: id |
order |
string | No | Sort direction: ASC or DESC. Default: DESC |
limit |
number | No | Results per page (max 1000). Default: 20 |
start |
number | No | Offset for pagination. Default: 0 |
Response
{
"result": true,
"errors": [],
"total": 12,
"agents": [
{
"id": 5,
"guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Instagram Manager",
"slug": "instagram-manager",
"headline": "Automate your Instagram presence with AI",
"description": "An autonomous agent that manages your Instagram account...",
"cover": "https://cdn.wiro.ai/uploads/agents/instagram-manager-cover.webp",
"categories": ["social-media", "marketing"],
"samples": ["https://cdn.wiro.ai/uploads/agents/instagram-manager-sample-1.webp"],
"pricing": {
"starter": { "price": 9, "credits": 1000 },
"pro": { "price": 29, "credits": 5000 }
},
"skills": ["post_image", "reply_comment", "schedule_post"],
"status": 1,
"createdat": "1711929600",
"updatedat": "1714521600"
}
]
}
POST /Agent/Detail
Retrieves details for a single agent by guid or slug. This is a public endpoint — no authentication required.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | No* | Agent guid. |
slug |
string | No* | Agent slug (e.g. "instagram-manager"). |
type |
string | No | Pass "full" to receive the complete configuration tree (agent_skills, custom_skills, rateLimit, full credentials schema). Without type: "full", the response returns a trimmed configuration object containing only { credentials } (sanitized for template browsing). |
guid or slug. If both are provided, slug takes priority. Pass type: "full" when you need the complete template (including custom_skills keys to preview before deploy); omit it for a lightweight catalog listing.Response
{
"result": true,
"errors": [],
"agents": [
{
"id": 5,
"guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Instagram Manager",
"slug": "instagram-manager",
"headline": "Automate your Instagram presence with AI",
"description": "An autonomous agent that manages your Instagram account...",
"cover": "https://cdn.wiro.ai/uploads/agents/instagram-manager-cover.webp",
"categories": ["social-media", "marketing"],
"samples": ["https://cdn.wiro.ai/uploads/agents/instagram-manager-sample-1.webp"],
"pricing": {
"starter": { "price": 9, "credits": 1000 },
"pro": { "price": 29, "credits": 5000 }
},
"skills": ["post_image", "reply_comment", "schedule_post"],
"ratelimit": { "actionTypes": { "message": 10, "create": 5 } },
"configuration": {
"credentials": {
"instagram": {
"_editable": { "authMethod": true },
"optional": false,
"authMethod": "",
"igUsername": "",
"connectedAt": ""
}
}
},
"status": 1,
"createdat": "1711929600",
"updatedat": "1714521600"
}
]
}
POST /UserAgent/Deploy
Creates a new agent instance from a catalog template.
useprepaid: true + plan. This is the only deploy mode that's fully programmatic — the subscription cost is deducted from your prepaid wallet immediately and the instance is created server-side in one call. Omitting useprepaid triggers the Stripe checkout flow (the panel's deploy path), which requires a user-facing browser redirect to Stripe's hosted checkout and is not suitable for API
integrations. The section below assumes prepaid deploy.
Prepaid deploy (useprepaid: true + plan) charges your wallet immediately and creates the instance in status 6 (Setup Required). configuration.credentials and configuration.custom_skills passed in the Deploy body are ignored on prepaid deploy — after deploy, call POST /UserAgent/Update to set credentials and (optionally) customize skill content. Once credentials are complete,
status auto-transitions to 0 (Stopped) and you can call UserAgent/Start.
| Parameter | Type | Required | Description |
|---|---|---|---|
agentguid |
string | Yes | The guid of the agent template from the catalog. |
title |
string | Yes | Display name for your instance. |
description |
string | No | Optional description. |
useprepaid |
boolean | Yes (for API use) | Pass true to pay from wallet balance in a single server-side call. Omitting this (or passing false) activates the Stripe checkout flow — the response redirects to Stripe's hosted checkout UI, which only works for interactive browser sessions, not API integrations. |
plan |
string | Yes | Plan tier: "starter" or "pro". Check POST /Agent/Detail → agent.pricing for each tier's credit allowance and price. |
pinned |
boolean | No | Whether the agent appears in the pinned agents list (defaults to true). Pass false when deploying agents programmatically for end users (e.g. bulk provisioning) so they don't clutter your admin dashboard. |
Headers: Standard API authentication — x-api-key for key-based projects, or x-nonce + x-signature for signature-based projects (see Authentication). For team-scoped deploys (when an end user deploys via a team project), pass teamGUID: <team-guid> as an additional request header; the API validates the caller is a member of that team before writing
the instance row. Personal deployments omit this header.
Panel/UI note: the non-prepaid Deploy mode (omit useprepaid) exists for the Wiro dashboard's interactive deploy flow where end users pay via Stripe subscription with a browser redirect. That path also accepts configuration.credentials and configuration.custom_skills in the body (merged via mergeUserConfig), but API integrations should always use prepaid deploy and set credentials with a follow-up
UserAgent/Update call.
Request
{
"agentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "My Instagram Bot",
"useprepaid": true,
"plan": "starter"
}
Response
{
"result": true,
"errors": [],
"useragents": [
{
"id": 47,
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"agentid": 5,
"title": "My Instagram Bot",
"status": 6,
"pinned": false,
"createdat": 1714608000,
"updatedat": 1714608000
}
]
}
useragents row (with sanitized configuration). It does not include setuprequired or subscription — those are Detail-level fields assembled from joined tables. A prepaid subscription row (agent-<plan>, provider prepaid) is inserted server-side during prepaid deploy; call POST /UserAgent/Detail with
the returned guid to see setuprequired, subscription, and stripe*url fields (if applicable).
POST /UserAgent/MyAgents
Lists all agent instances deployed under your account.
| Parameter | Type | Required | Description |
|---|---|---|---|
sort |
string | No | Sort column: id, title, status, createdat, updatedat, startedat, runningat, stopdat. Default: id |
order |
string | No | Sort direction: ASC or DESC. Default: DESC |
limit |
number | No | Results per page (max 1000). Default: 20 |
start |
number | No | Offset for pagination. Default: 0 |
category |
string | No | Filter by category |
Response
{
"result": true,
"errors": [],
"useragents": [
{
"id": 47,
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"agentid": 5,
"title": "My Instagram Bot",
"status": 4,
"setuprequired": false,
"subscription": {
"plan": "agent-pro",
"status": "active",
"amount": 29,
"currency": "usd",
"currentperiodend": 1717200000,
"renewaldate": "2026-06-01T00:00:00.000Z",
"daysremaining": 62,
"pendingdowngrade": null,
"provider": "prepaid"
},
"agent": {
"id": 5,
"title": "Instagram Manager",
"slug": "instagram-manager",
"cover": "https://cdn.wiro.ai/uploads/agents/instagram-manager-cover.webp",
"categories": ["social-media", "marketing"],
"pricing": {
"starter": { "price": 9, "credits": 1000 },
"pro": { "price": 29, "credits": 5000 }
}
},
"extracredits": 0,
"extracreditsexpiry": null,
"createdat": "1714608000",
"updatedat": "1714694400",
"startedat": "1714694400",
"runningat": "1714694410"
}
]
}
POST /UserAgent/Detail
Retrieves full details for a single deployed agent instance, including subscription info.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
Response — prepaid instance (the API deploy pattern)
{
"result": true,
"errors": [],
"useragents": [
{
"id": 47,
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"uuid": "your-user-uuid",
"agentid": 5,
"teamguid": null,
"title": "My Instagram Bot",
"description": null,
"status": 4,
"pinned": false,
"setuprequired": false,
"configuration": {
"credentials": {
"instagram": {
"_editable": { "authMethod": true },
"optional": false,
"authMethod": "wiro",
"igUsername": "myaccount",
"connectedAt": "2025-04-01T12:00:00.000Z"
}
},
"custom_skills": [
{
"key": "content-tone",
"description": "Content strategy, brand voice, and posting rules",
"value": "## Brand Voice\nTone: friendly\nTarget Audience: ...",
"enabled": true,
"interval": null,
"_editable": true
},
{
"key": "cron-content-scanner",
"description": "Content discovery with rotating strategies",
"value": "",
"enabled": true,
"interval": "0 */4 * * *",
"_editable": false
}
],
"skills": { "instagram-post": true, "wiro-generator": true },
"rateLimit": {
"monthlyCredits": 5000,
"extraCredits": 2000,
"actionTypes": { "message": 10, "create": 60, "modify": 20, "regenerate": 20 }
}
},
"subscription": {
"plan": "agent-pro",
"status": "active",
"amount": 29,
"currency": "usd",
"currentperiodend": 1717200000,
"renewaldate": "2026-06-01T00:00:00.000Z",
"daysremaining": 62,
"pendingdowngrade": null,
"provider": "prepaid"
},
"agent": {
"id": 5,
"title": "Instagram Manager",
"slug": "instagram-manager",
"cover": "https://cdn.wiro.ai/uploads/agents/instagram-manager-cover.webp",
"pricing": {
"starter": { "price": 9, "credits": 1000 },
"pro": { "price": 29, "credits": 5000 }
}
},
"extracredits": 2000,
"extracreditsexpiry": 1730419200,
"createdat": 1714608000,
"updatedat": 1714694400,
"startedat": 1714694400,
"runningat": 1714694410
}
]
}
Response — Stripe-subscription instance
The Stripe path (panel/UI deploy flow) adds three billing portal URLs; prepaid instances don't have these fields at all.
{
"result": true,
"errors": [],
"useragents": [
{
"id": 48,
"guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"agentid": 5,
"title": "Stripe-subscribed Instagram Bot",
"status": 4,
"setuprequired": false,
"configuration": {
"credentials": {
"instagram": {
"_editable": { "authMethod": true },
"optional": false,
"authMethod": "wiro",
"igUsername": "myaccount",
"connectedAt": "2025-04-01T12:00:00.000Z"
}
},
"custom_skills": [
{
"key": "content-tone",
"description": "Content strategy, brand voice, and posting rules",
"value": "## Brand Voice\nTone: friendly\nTarget Audience: ...",
"enabled": true,
"interval": null,
"_editable": true
},
{
"key": "cron-content-scanner",
"description": "Content discovery with rotating strategies",
"value": "",
"enabled": true,
"interval": "0 */4 * * *",
"_editable": false
}
],
"skills": { "instagram-post": true, "wiro-generator": true }
},
"subscription": {
"plan": "agent-pro",
"status": "active",
"amount": 29,
"currency": "usd",
"currentperiodend": 1717200000,
"renewaldate": "2026-06-01T00:00:00.000Z",
"daysremaining": 62,
"pendingdowngrade": null,
"provider": "stripe"
},
"agent": {
"id": 5,
"title": "Instagram Manager",
"slug": "instagram-manager",
"pricing": {
"starter": { "price": 9, "credits": 1000 },
"pro": { "price": 29, "credits": 5000 }
}
},
"extracredits": 0,
"extracreditsexpiry": null,
"stripeportalurl": "https://billing.stripe.com/p/session/xxxxxxxx",
"stripeupdateurl": "https://billing.stripe.com/p/session/xxxxxxxx/subscriptions/update",
"stripecancelurl": "https://billing.stripe.com/p/session/xxxxxxxx/subscriptions/cancel",
"createdat": 1714608000,
"updatedat": 1714694400
}
]
}
| Field | Type | Description |
|---|---|---|
guid |
string |
Unique identifier for this agent instance. |
agentid |
number |
The catalog agent ID this instance was deployed from. |
title |
string |
Display name you gave this instance. |
status |
number |
Current status code (see UserAgent Statuses). |
setuprequired |
boolean |
true if credentials are missing or incomplete. |
configuration |
object |
Sanitized { credentials, custom_skills, skills, agent_skills } — non-editable sensitive fields (OAuth access tokens, platform wiro.apiKey, openai.apiKey, etc.) are hidden. |
subscription |
object|null |
Active subscription info, or null if no subscription. |
agent |
object |
Parent agent template info (title, slug, cover, pricing). |
extracredits |
number |
Remaining extra credits purchased for this instance. |
extracreditsexpiry |
number|null |
Unix timestamp when the earliest extra credit pack expires. |
stripeportalurl |
string|undefined |
Stripe-only. One-shot Stripe billing portal session URL. Only present when subscription.provider === "stripe". Omitted for prepaid. |
stripeupdateurl |
string|undefined |
Stripe-only. Deep link into plan-update flow. Same conditions as stripeportalurl. |
stripecancelurl |
string|undefined |
Stripe-only. Deep link into cancellation flow. Use this URL for the "Cancel subscription" button — do NOT call POST /UserAgent/CancelSubscription on Stripe subscriptions. |
subscription.provider |
string |
Payment provider: "prepaid" (credits wallet) or "stripe" (subscription). |
POST /UserAgent/Update
Updates an agent instance's configuration, title, description, or categories. If the agent is currently starting (status 3) or running (status 4), this triggers an automatic restart to apply the new settings.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
title |
string | No | New display name |
description |
string | No | New description |
configuration |
object | No | Updated credentials. Format: { "credentials": { "key": "value" } } |
categories |
array | No | Updated categories. Cannot be empty if provided. |
6 (Setup Required) and the update completes all required credentials, the status automatically changes to 0 (Stopped), allowing you to start it.Response
Returns the updated agent instance with setuprequired flag and agent summary. Does not include subscription — use UserAgent/Detail for the full view.
POST /UserAgent/Start
Starts a stopped agent instance. The agent is moved to Queued (status 2) and will be picked up by a worker.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
Response
{
"result": true,
"errors": []
}
Start will fail with a descriptive error if:
- The agent is already running or queued
- The agent is currently stopping
- Setup is incomplete (status
6) - No active subscription exists
- No credits remain (monthly or extra)
POST /UserAgent/Stop
Stops a running agent instance. If the agent is Queued (status 2), it is immediately set to Stopped. If it is Starting or Running (status 3/4), it moves to Stopping (status 1) and the container is shut down gracefully.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
Response
{
"result": true,
"errors": []
}
POST /UserAgent/CreateExtraCreditCheckout
Purchases additional credits for a Pro plan agent. Deducts from your prepaid wallet balance.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentGuid |
string | Yes | Your UserAgent instance guid |
pack |
string | Yes | Credit pack: package1, package2, or package3 |
useprepaid |
boolean | Yes | Set to true to pay from wallet balance. Credits added immediately. |
Request
{
"useragentGuid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"pack": "package2",
"useprepaid": true
}
Response
{
"result": true,
"url": null,
"errors": []
}
Credits are added immediately from your wallet balance. url is null for prepaid purchases. Credits expire 6 months after purchase.
POST /UserAgent/CancelSubscription
Cancels a subscription at the end of the current billing period. The agent remains active until the period ends.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
Response
{
"result": true,
"cancelsAt": 1717200000,
"errors": []
}
The cancelsAt field is the Unix timestamp when the subscription will expire. The agent continues running until this date. You can reverse the cancellation by calling RenewSubscription before the period ends.
POST /UserAgent/UpgradePlan
Upgrades a Starter subscription to Pro. The prorated cost for the remaining days is deducted from your wallet.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
plan |
string | Yes | Target plan: "pro" (only starter-to-pro upgrade is supported) |
Response
{
"result": true,
"plan": "agent-pro",
"proratedCharge": 11.33,
"newMonthlyCredits": 5000,
"errors": []
}
Downgrades are not supported. To change from Pro to Starter, cancel and re-deploy.
POST /UserAgent/RenewSubscription
Renews an expired subscription or reverses a pending cancellation. The renewal cost is deducted from your wallet.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
Response (renewal)
{
"result": true,
"action": "renewed",
"plan": "agent-starter",
"amount": 49,
"errors": []
}
Response (undo cancel)
When called on an active subscription with a pending cancellation:
{
"result": true,
"action": "undo-cancel",
"errors": []
}
After renewal, the agent status is reset to 0 (Stopped). Call Start to launch it again. Monthly credits are refreshed for the new billing period.
Agent Pricing
Agent pricing is subscription-based, billed monthly. Two payment methods are available:
| Feature | Starter | Pro |
|---|---|---|
| Monthly price | Varies by agent (e.g. $9/mo) | Varies by agent (e.g. $29/mo) |
| Monthly credits | Included (e.g. 1,000) | Included (e.g. 5,000) |
| Extra credit packs | Not available | Available (expire in 6 months) |
| Plan upgrade | Upgrade to Pro anytime | — |
Each agent in the catalog defines its own pricing tiers in the pricing field. Check the Agent/Detail response for exact prices and credit amounts.
Payment Method
All subscriptions use your prepaid wallet balance. The cost is deducted immediately when you deploy or renew. Subscriptions renew automatically if your wallet has sufficient balance; otherwise the subscription expires. Manage subscriptions through the CancelSubscription, UpgradePlan, and RenewSubscription endpoints.
Credits are consumed by the agent runtime (container) as it processes messages and generates content — not at Message/Send time. Each configuration.rateLimit.actionTypes entry (message, create, modify, regenerate) defines how many credits a specific action costs; the container reports usage back to Wiro asynchronously. The API-side check is gating only: POST /UserAgent/Start refuses to launch if
monthlyCredits + extraCredits <= 0. During an active session, monthly credits are deducted first; once they hit zero, extra credits (purchased via CreateExtraCreditCheckout) are used. When both pools are empty, the agent responds [SYSTEM_CREDIT_LIMIT_REACHED] and stops processing further actions. Credits reset on each billing cycle or when extra credits are topped up.
Error Messages
Agent-specific errors you may encounter:
| Error | When |
|---|---|
Agent not found |
The agentguid or slug does not match any catalog agent |
User agent not found |
The guid does not match any of your deployed instances |
Agent not found or inactive |
The catalog agent exists but is disabled |
Active subscription required to start agent. Please renew your subscription. |
No active subscription for this instance |
Agent setup is not complete. Please fill in your credentials before starting. |
Status is 6 — call Update to provide required credentials |
Agent is already running |
Start called on an agent with status 3 or 4 |
Agent is already queued to start |
Start called on an agent with status 2 |
Agent is already stopped |
Stop called on an agent with status 0 |
Agent is currently stopping, please wait |
Start called on an agent with status 1 |
Agent is in error state, use Start to retry |
Stop called on an agent with status 5 |
No credits available. Please renew your subscription or purchase extra credits. |
Monthly and extra credits are both exhausted |
Extra credits are available only for Pro plan subscribers. Please upgrade your plan. |
CreateExtraCreditCheckout called on a Starter plan |
Invalid pack. Choose package1, package2, or package3. |
CreateExtraCreditCheckout with invalid pack |
Active subscription required to purchase extra credits. |
CreateExtraCreditCheckout without subscription |
Extra credit pack not available for this agent. |
Agent pricing doesn't define the pack |
Categories cannot be empty |
Update with empty categories |
Agent not found or access denied |
Message endpoint with invalid useragentguid |
Agent is not running. Current status: {n} |
Message/Send when not running |
Message not found |
Detail/Cancel with invalid messageguid |
Message cannot be cancelled (status: {status}) |
Cancel on completed message |
Invalid redirect URL |
OAuth Connect with non-HTTPS URL |
Subscription is already active |
RenewSubscription called when subscription is already active without pending cancel |
No expired subscription found to renew |
RenewSubscription called with no expired subscription |
Insufficient wallet balance. Required: $X, Available: $Y |
Prepaid operation with insufficient funds |
Cannot downgrade from Pro to Starter. Cancel your subscription instead. |
UpgradePlan with downgrade attempt |
Subscription cancellation scheduled |
CancelSubscription success |
Valid plan required when using prepaid (starter or pro) |
Deploy with useprepaid but missing/invalid plan |
Pricing not available for this plan |
Deploy with useprepaid for agent without pricing |
Renewal pricing not available |
RenewSubscription for agent with zero pricing |
What's Next
- Agent Messaging — Send messages and receive responses from running agents
- Agent Credentials — Configure OAuth and API key credentials for your agent
- Authentication — API key setup and authentication methods
- Pricing — General pricing information
Agent Messaging
Send messages to AI agents and receive streaming responses in real time.
How It Works
Agent messaging follows the same async pattern as model runs:
- Send a message via REST → get an
agenttokenimmediately - Subscribe to WebSocket with the
agenttoken→ receive streaming response chunks - Or poll via the Detail endpoint to check status and fetch the completed response
- Or set a
callbackurlto receive a webhook notification when the agent finishes
This decoupled design means your application never blocks waiting for the agent to think. Send the message, hand the agenttoken to your frontend, and stream the response as it arrives.
Message Lifecycle
Every agent message progresses through a defined set of stages:
Message Statuses
| Status | Description |
|---|---|
agent_queue |
The message is queued and waiting to be picked up by the agent worker. Emitted once when the message enters the queue. |
agent_start |
The agent has accepted the message and begun processing. The underlying LLM call is being prepared. |
agent_output |
The agent is producing output. This event is emitted multiple times — each chunk of the response arrives as a separate agent_output event via WebSocket, enabling real-time streaming. |
agent_end |
The agent has finished generating the response. The full output is available in the response and debugoutput fields. This is the event you should listen for to get the final result. |
agent_error |
The agent encountered an error during processing. The debugoutput field contains the error message. |
agent_cancel |
The message was cancelled by the user before completion. Only messages in agent_queue, agent_start, or agent_output status can be cancelled. |
POST /UserAgent/Message/Send
Sends a user message to a deployed agent. The agent must be in running state (status 4). Returns immediately with an agenttoken that you use to track the response via WebSocket, polling, or webhook.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | The agent instance GUID (from Deploy or MyAgents). |
message |
string | Yes | The user message text to send to the agent. |
sessionkey |
string | No | Session identifier for conversation continuity. Defaults to "default". |
callbackurl |
string | No | Webhook URL — the system will POST the final response to this URL when the agent finishes. |
Response
{
"result": true,
"errors": [],
"messageguid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"status": "agent_queue"
}
| Field | Type | Description |
|---|---|---|
messageguid |
string |
Unique identifier for this message. Use it with Detail, History, or Cancel. |
agenttoken |
string |
Token for WebSocket subscription and polling. Equivalent to tasktoken in model runs. |
status |
string |
Initial status — always "agent_queue" on success. |
POST /UserAgent/Message/Detail
Retrieves the current status and content of a single message. You can query by either messageguid or agenttoken.
| Parameter | Type | Required | Description |
|---|---|---|---|
messageguid |
string | No | The message GUID returned from Send. |
agenttoken |
string | No | The agent token returned from Send (alternative to messageguid). |
messageguid or agenttoken.Response
{
"result": true,
"errors": [],
"data": {
"guid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"uuid": "user-uuid-here",
"sessionkey": "default",
"content": "What are the latest trends in AI?",
"response": "Here are the key AI trends for 2026...",
"debugoutput": "Here are the key AI trends for 2026...",
"status": "agent_end",
"metadata": {
"type": "progressGenerate",
"task": "Generate",
"speed": "14.2",
"speedType": "words/s",
"elapsedTime": "8.1s",
"tokenCount": 105,
"wordCount": 118,
"raw": "Here are the key AI trends for 2026...",
"thinking": [],
"answer": ["Here are the key AI trends for 2026..."],
"isThinking": false
},
"attachments": [],
"deletestatus": 0,
"createdat": "1743350400",
"startedat": "1743350401",
"endedat": "1743350408"
}
}
| Field | Type | Description |
|---|---|---|
guid |
string |
Message GUID. |
uuid |
string |
The account UUID of the user who sent the message. |
sessionkey |
string |
The session this message belongs to. |
content |
string |
The original user message. |
response |
string |
The agent's full response text. Empty until agent_end. |
debugoutput |
string |
Accumulated output text. Updated during streaming, contains the full response after completion. |
status |
string |
Current message status (see Message Lifecycle). |
metadata |
object |
Parsed JSON object (API returns it already decoded). Populated from the agent bridge on agent_end. Fields: type (event type, e.g. "agent_end"), task (user input summary), speed (tokens/sec), speedType ("token"), elapsedTime (ms), tokenCount, wordCount, raw (full output), thinking (array of
<think> blocks), answer (array of post-think answer chunks), isThinking (false when answer finalized). Empty object {} for agent_error, agent_cancel, or when the bridge hasn't finished yet.
|
attachments |
array |
Present only if the message was sent via multipart with files. Array of {url, name, type, size} entries — each file metadata is already URL-resolved server-side (no further lookup needed). Absent when the message had no attachments. Identical shape in Message/History rows. |
createdat |
string |
Unix timestamp when the message was created. |
startedat |
string |
Unix timestamp when the agent started processing. |
endedat |
string |
Unix timestamp when processing completed. |
POST /UserAgent/Message/History
Retrieves conversation history for a specific agent and session. Messages are returned newest-first with cursor-based pagination.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | The agent instance GUID. |
sessionkey |
string | No | Session identifier. Defaults to "default". |
limit |
number | No | Maximum number of messages to return. Defaults to 50, max 200. |
before |
string | No | Message GUID to use as cursor — returns only messages created before this one. Omit for the most recent messages. |
Response
{
"result": true,
"errors": [],
"data": {
"messages": [
{
"guid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"content": "What are the latest trends in AI?",
"response": "Here are the key AI trends for 2026...",
"debugoutput": "Here are the key AI trends for 2026...",
"status": "agent_end",
"metadata": {
"type": "progressGenerate",
"task": "Generate",
"speed": "14.2",
"speedType": "words/s",
"elapsedTime": "8.1s",
"tokenCount": 105,
"wordCount": 118,
"raw": "Here are the key AI trends for 2026...",
"thinking": [],
"answer": ["Here are the key AI trends for 2026..."],
"isThinking": false
},
"attachments": [],
"deletestatus": 0,
"createdat": "1743350400"
},
{
"guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"content": "Tell me more about multimodal models",
"response": "Multimodal models combine...",
"debugoutput": "Multimodal models combine...",
"status": "agent_end",
"metadata": {},
"attachments": [],
"deletestatus": 0,
"createdat": "1743350300"
}
],
"count": 2,
"hasmore": false
}
}
| Field | Type | Description |
|---|---|---|
messages |
array |
Array of message objects, newest first. |
count |
number |
Number of messages in this page. |
hasmore |
boolean |
true if there are older messages available. Pass the last message's guid as before to fetch the next page. |
Pagination
Page 1 — most recent messages:
{
"useragentguid": "...",
"limit": 50
}
Page 2 — pass the last message's guid as cursor:
{
"useragentguid": "...",
"limit": 50,
"before": "a1b2c3d4-..."
}
POST /UserAgent/Message/Sessions
Lists all conversation sessions for an agent. Returns each session's key, message count, last activity time, and the most recent message content.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | The agent instance GUID. |
Response
{
"result": true,
"errors": [],
"data": {
"sessions": [
{
"sessionkey": "default",
"messagecount": "24",
"updatedat": "1743350400",
"lastmessage": "What are the latest trends in AI?"
},
{
"sessionkey": "user-42-support",
"messagecount": "8",
"updatedat": "1743349200",
"lastmessage": "How do I reset my password?"
}
]
}
}
| Field | Type | Description |
|---|---|---|
sessionkey |
string |
The session identifier. |
messagecount |
string |
Total number of messages in this session. |
updatedat |
string |
Unix timestamp of the last activity in this session. |
lastmessage |
string |
The content (user message) of the most recent message. |
POST /UserAgent/Message/DeleteSession
Deletes messages in the given session for the calling user. This action cannot be undone.
Scope: the API matches on useragentguid + sessionkey and the caller's uuid, so only messages the calling user sent/received in this session are removed. In collaborative team mode (teamSessionMode: "collaborative", Telegram group-shared sessions), other team members' messages in the same sessionkey remain intact — each member must call
DeleteSession to clear their own share.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | The agent instance GUID. |
sessionkey |
string | Yes | The session key to delete. |
Response
{
"result": true,
"errors": []
}
POST /UserAgent/Message/Cancel
Cancels an in-progress message. Only messages in agent_queue, agent_start, or agent_output status can be cancelled. Messages that have already reached agent_end, agent_error, or agent_cancel cannot be cancelled.
| Parameter | Type | Required | Description |
|---|---|---|---|
messageguid |
string | No | The message GUID to cancel. |
agenttoken |
string | No | The agent token to cancel (alternative to messageguid). |
messageguid or agenttoken.Response
{
"result": true,
"errors": []
}
On success, the message status changes to agent_cancel in the database.
Behavior depends on when the cancel hits:
-
If the message was already being processed by the agent bridge (status
agent_startoragent_output), the bridge's abort handler fires: subscribed WebSocket clients receive anagent_cancelevent,debugoutput/responseare populated with the abort reason (typically"AbortError"or similar technical message — not guaranteed to be a fixed user-facing string), and thecallbackurlwebhook (if set) is triggered withstatus: "agent_cancel". -
If the message was still queued (status
agent_queue) when cancelled, no processing attempt had started: the row is markedagent_cancelbutresponseanddebugoutputremain empty strings, no WebSocketagent_cancelevent is broadcast, and no webhook is triggered.
When polling for the final state, check status === "agent_cancel" rather than relying on non-empty response / debugoutput (they may be empty for queued-state cancellations).
Session Management
Sessions let you maintain separate conversation threads with the same agent:
- Each
sessionkeyrepresents a separate conversation — the agent remembers context within a session - The default session key is
"default"if you don't specify one - Use unique session keys per end-user for multi-tenant applications (e.g.
"user-42","customer-abc") - Sessions persist across API calls — send the same
sessionkeyto continue a conversation - Delete a session with
/UserAgent/Message/DeleteSessionto clear history and free resources
User A's conversation:
{
"useragentguid": "...",
"message": "Hello!",
"sessionkey": "user-alice"
}
User B's separate conversation with the same agent:
{
"useragentguid": "...",
"message": "Hello!",
"sessionkey": "user-bob"
}
Thinking & Answer Separation
Agent responses may include thinking blocks where the underlying model reasons through the problem before answering. The system automatically parses <think>...</think> tags and separates the output into structured arrays:
{
"thinking": ["Let me analyze the user's question...", "The key factors are..."],
"answer": ["Based on my analysis, here are the main points..."],
"isThinking": false,
"raw": "<think>Let me analyze the user's question...</think>Based on my analysis, here are the main points..."
}
thinking— array of reasoning/chain-of-thought blocks. May be empty if the model doesn't use thinking.answer— array of response chunks. This is the content to show the user.isThinking—truewhile the model is still in a thinking phase (the<think>tag is open but not yet closed),falseduring the answer phase.raw— the full accumulated raw output text including think tags.
Each agent_output WebSocket event contains the full accumulated arrays up to that point — not just the new chunk. Simply replace your displayed content with the latest arrays. Use isThinking to show a "thinking" indicator in your UI while the model reasons.
Tracking a Message
There are three ways to track message progress after sending:
1. WebSocket (Recommended)
Connect to WebSocket and subscribe with the agenttoken for real-time streaming. Each agent_output event delivers the growing response as it's generated.
1. Subscribe to agent message updates:
{
"type": "agent_info",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb"
}
2. Server confirms subscription with current status:
{
"type": "agent_subscribed",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"status": "agent_queue",
"result": true
}
3. Streaming output event (emitted multiple times — replace your displayed content with message.answer on each event):
{
"type": "agent_output",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"message": {
"type": "progressGenerate",
"task": "Generate",
"speed": "12.4",
"speedType": "words/s",
"elapsedTime": "3.2s",
"tokenCount": 156,
"wordCount": 42,
"raw": "Here are the key AI trends...",
"thinking": [],
"answer": ["Here are the key AI trends..."],
"isThinking": false
},
"result": true
}
4. Final event (agent_end — terminal):
{
"type": "agent_end",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"message": {
"type": "progressGenerate",
"task": "Generate",
"speed": "14.2",
"speedType": "words/s",
"elapsedTime": "8.1s",
"tokenCount": 412,
"wordCount": 115,
"raw": "Here are the key AI trends for 2026...",
"thinking": [],
"answer": ["Here are the key AI trends for 2026..."],
"isThinking": false
},
"result": true
}
| Field | Type | Description |
|---|---|---|
message.type |
string |
Always "progressGenerate" for agent output events. |
message.speed |
string |
Generation speed (e.g. "12.4"). |
message.speedType |
string |
Unit for speed — "words/s" (words per second). |
message.elapsedTime |
string |
Elapsed time since generation started (e.g. "3.2s"). |
message.tokenCount |
number |
Number of tokens generated so far. |
message.wordCount |
number |
Number of words generated so far. |
message.raw |
string |
Full accumulated raw output text. |
message.thinking |
string[] |
Array of thinking/reasoning blocks. |
message.answer |
string[] |
Array of answer blocks — the content to display. |
message.isThinking |
boolean |
true while the model is in thinking phase. |
2. Polling via Detail
If you don't need real-time streaming, poll POST /UserAgent/Message/Detail at regular intervals until the status reaches a terminal state (agent_end, agent_error, or agent_cancel).
3. Webhook Callback
Pass a callbackurl when sending the message. The system will POST the final result to your URL when the agent finishes (up to 3 retry attempts):
{
"messageguid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"status": "agent_end",
"content": "What are the latest trends in AI?",
"response": "Here are the key AI trends for 2026...",
"debugoutput": "Here are the key AI trends for 2026...",
"metadata": {
"type": "progressGenerate",
"task": "Generate",
"speed": "14.2",
"speedType": "words/s",
"elapsedTime": "8.1s",
"tokenCount": 105,
"wordCount": 118,
"raw": "Here are the key AI trends for 2026...",
"thinking": [],
"answer": ["Here are the key AI trends for 2026..."],
"isThinking": false
},
"endedat": 1743350408
}
Payload delivered to your callbackurl. metadata is decoded into a JSON object.
Agent WebSocket
Receive real-time agent response streaming via a persistent WebSocket connection.
Connection URL
wss://socket.wiro.ai/v1
Connect to this URL after calling the Message / Send endpoint. Use the agenttoken from the send response to subscribe to the agent session. This is the same WebSocket server used for model tasks — you can subscribe to both task events (task_info) and agent events (agent_info) on the same connection.
No API key or auth header is required on the WebSocket itself. Authorization is enforced via the agenttoken, which is issued by POST /UserAgent/Message/Send against your API key and is scoped to a single message run.
Connection Flow
- Connect — open a WebSocket connection to
wss://socket.wiro.ai/v1. - Receive welcome — the server pushes a one-shot
connectedframe confirming the upgrade. - Subscribe — send an
agent_infoframe with youragenttoken. -
Receive
agent_subscribed— the server acknowledges the subscribe and reports the current lifecycle status (plus any already-accumulateddebugoutput). - Stream — listen for
agent_start→ manyagent_output→agent_end/agent_error/agent_cancel. - Close — disconnect after a terminal event, or keep the socket open and subscribe to the next
agenttoken.
1. Welcome frame (server → client)
Right after the WebSocket upgrade succeeds, the server sends this frame on its own. You don't request it; it arrives before you send anything.
{
"type": "connected",
"version": "1.0"
}
Use it as a signal that the socket is fully ready to receive a subscribe. Most clients can ignore the payload — the version field is informational.
2. Subscribe frame (client → server)
{
"type": "agent_info",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb"
}
type— must be the literal string"agent_info". Other values are ignored (the server routes by type; unknown types are silently dropped).agenttoken— the token returned byPOST /UserAgent/Message/Send. Required. If missing or empty, the server responds with anerrorframe (see below).
3. Subscribe errors
If agenttoken is missing or blank, the server replies with:
{
"type": "error",
"message": "agenttoken-required",
"result": false
}
The connection stays open — no disconnect. Fix the payload and resend.
Multi-session subscription
A single WebSocket connection can hold subscriptions to multiple agent sessions simultaneously. Send one agent_info frame per token; the server de-duplicates, so resending the same token is a no-op.
After this, every server-side event for either token is forwarded to this connection. All events carry the agenttoken field, so your handler can route them back to the right UI surface.
Typical use cases:
- Multi-tab chat clients that keep several live conversations.
- Dashboards watching several users' agents in parallel.
- Combining task streaming and agent streaming on the same connection —
task_infoandagent_infoframes both map to the same connection's token list.
There is no unsubscribe frame. Tokens are cleaned up automatically when the connection closes.
Event Types
Frames flow in two directions. ↓ = server → client, ↑ = client → server.
| Direction | Event Type | Description |
|---|---|---|
| ↓ | connected |
Welcome frame pushed by the server right after the WebSocket upgrade. Fires exactly once per connection. |
| ↑ | agent_info |
Client-initiated subscribe frame. Carries the agenttoken issued by Message/Send. Can be sent multiple times on the same socket (one per token). |
| ↓ | error |
Server-side rejection of a malformed subscribe (missing agenttoken). Connection stays open; retry with a valid frame. |
| ↓ | agent_subscribed |
Subscribe acknowledged. Carries the current lifecycle status plus any accumulated debugoutput. If the agent already finished before you subscribed, this frame is your snapshot of the final output and no further events will fire. |
| ↓ | agent_start |
The bridge has opened an SSE stream to the agent container. The underlying model is now generating. Emits exactly once per message. |
| ↓ | agent_output |
Streaming chunk. Emits many times — each carries the full accumulated raw text so far plus real-time metrics (speed, elapsedTime, tokenCount, wordCount). Replace (don't append) your UI on each event. |
| ↓ | agent_end |
Terminal success event. Same payload shape as agent_output but contains the final complete text with total metrics. Emits at most once. |
| ↓ | agent_error |
Terminal failure event. message is either a sanitized string (when an exception was caught) or a progressGenerate object (when the stream finished but content was a degenerate "..." / "Error: internal error"). Emits at most once. |
| ↓ | agent_cancel |
Terminal cancel event. Fires only when an already-active message is aborted mid-stream. Cancels against a still-queued message do not broadcast this event — check Message/Detail for those. Emits at most once. |
Message Format
Every WebSocket frame is a JSON object. All agent lifecycle frames (agent_subscribed / agent_start / agent_output / agent_end / agent_error / agent_cancel) share this base shape:
{
"type": "agent_output",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"message": { ... },
"result": true
}
| Field | Type | Description |
|---|---|---|
type |
string | Event name. See Event Types for the full list. |
agenttoken |
string | The token you subscribed with. Present on every agent lifecycle frame so multi-token subscribers can route the event to the right session. |
message |
varies | Empty string ("") for agent_start, a progressGenerate object for agent_output / agent_end (and for the object-shaped agent_error), and a plain string for string-shaped agent_error and agent_cancel. |
result |
boolean | true for success-side events (agent_subscribed / agent_start / agent_output / agent_end), false for failure-side events (agent_error / agent_cancel). |
The control frames (connected, error) use a different shape with no agenttoken:
{
"type": "connected",
"version": "1.0"
}
{
"type": "error",
"message": "agenttoken-required",
"result": false
}
The agent_subscribed frame additionally carries status (the DB row's current status) and, when the token is known, debugoutput (accumulated text so far). When the token is unknown, status is "unknown" and debugoutput is omitted entirely.
agent_subscribed
Sent immediately after the server accepts your subscription. The status field reflects where the agent currently is in its lifecycle.
- If the agenttoken is valid and pending/active (known to the server, not yet finished),
debugoutputis always present — an empty string""if nothing has streamed yet, or the accumulated text so far. - If the agenttoken is unknown (typo, expired, already cleaned up from the buffer),
debugoutputis omitted entirely from the payload (no field at all). Always use"debugoutput" in payloadorpayload.debugoutput !== undefinedto distinguish unknown-token from empty-output, rather than relying on truthiness.
Valid token, queued — debugoutput present and empty:
{
"type": "agent_subscribed",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"status": "agent_queue",
"debugoutput": "",
"result": true
}
Unknown token — status is "unknown" and no debugoutput field:
{
"type": "agent_subscribed",
"agenttoken": "wrongtoken123",
"status": "unknown",
"result": true
}
| Status | Meaning |
|---|---|
agent_queue |
Message is queued, waiting for the agent to pick it up. |
agent_start |
Agent has started processing. |
agent_output |
Agent is actively streaming. debugoutput will contain accumulated text. |
agent_end |
Agent already finished. debugoutput contains the complete response. |
agent_error |
Agent encountered an error. debugoutput contains the error message. |
agent_cancel |
Message was cancelled before completion. |
unknown |
The agenttoken was not found in the database. |
agent_start
Signals that the agent has begun generating a response:
{
"type": "agent_start",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"message": "",
"result": true
}
agent_output (streaming)
Emitted multiple times as the agent generates its response. Each event contains the full accumulated text up to that point (not just the delta), along with real-time performance metrics:
{
"type": "agent_output",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"message": {
"type": "progressGenerate",
"task": "Generate",
"speed": "12.5",
"speedType": "words/s",
"elapsedTime": "2.4s",
"tokenCount": 35,
"wordCount": 28,
"raw": "Here is the accumulated response text so far...",
"thinking": [],
"answer": ["Here is the accumulated response text so far..."],
"isThinking": false
},
"result": true
}
The raw field contains the full response as a single string. The answer array contains the same text split into segments. To display streaming text, replace your UI content with raw (or join answer) on each event.
agent_end
Fires when the agent finishes responding. The structure is identical to agent_output — the message contains the final complete text with total metrics:
{
"type": "agent_end",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"message": {
"type": "progressGenerate",
"task": "Generate",
"speed": "14.2",
"speedType": "words/s",
"elapsedTime": "8.1s",
"tokenCount": 156,
"wordCount": 118,
"raw": "The complete agent response text...",
"thinking": [],
"answer": ["The complete agent response text..."],
"isThinking": false
},
"result": true
}
agent_error
An error occurred during processing. The message field can take two forms — a sanitized string when the stream is aborted by an exception, or a progress object when the stream finished naturally but the model returned a non-response.
Sanitized string error — any exception during streaming (bridge timeout, upstream HTTP 5xx, worker crash, SSE read error, etc.) surfaces as a single user-safe sentence:
{
"type": "agent_error",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"message": "Agent is temporarily unavailable. Please try again shortly.",
"result": false
}
The raw runtime error is never broadcast over WebSocket. Strings like
"Bridge timeout","OpenClaw returned HTTP 500","SSE request timeout (30min)","Could not resolve agent endpoint"are recorded in the databasedebugoutputfield (retrievable viaPOST /UserAgent/Message/Detail) but replaced with the generic sentence above before being pushed to subscribed clients. This is intentional so end users never see internal infrastructure errors.
Progress-object error — the SSE stream completes normally but the model returns "..." or "Error: internal error". In that case Wiro flags the message as agent_error and broadcasts the same progressGenerate shape as agent_output / agent_end:
{
"type": "agent_error",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"message": {
"type": "progressGenerate",
"task": "Generate",
"speed": "2.5",
"speedType": "words/s",
"elapsedTime": "1.2s",
"tokenCount": 3,
"wordCount": 1,
"raw": "...",
"thinking": [],
"answer": ["..."],
"isThinking": false
},
"result": false
}
Check the runtime type of message to branch:
if (msg.type === 'agent_error') {
if (typeof msg.message === 'string') showToast(msg.message)
else renderResponse(msg.message.raw)
}
agent_cancel
Sent when the user cancels a message before the agent completes its response — but only when the abort hits the bridge mid-flight (queued-state cancels do not broadcast this event):
{
"type": "agent_cancel",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"message": "AbortError",
"result": false
}
The
messagefield carries the abort reason from the runtime (typically"AbortError"or a short technical string). It is not a fixed user-facing message — do not parse it for exact strings. Usetype === "agent_cancel"as the canonical signal. Subscribers that cancel from a queued state receive no event at all (the message is simply markedagent_cancelin the database; check withPOST /UserAgent/Message/Detail).
The result Field
Every agent lifecycle event includes a result boolean:
| Value | Events |
|---|---|
true |
agent_subscribed, agent_start, agent_output, agent_end |
false |
error, agent_error, agent_cancel |
Use result to quickly determine whether the event represents a successful state. When result is false, inspect message for error details or cancellation context. The welcome connected frame has no result field — it's a one-shot ack and always implies success (you got the frame, so the upgrade worked).
Streaming Metrics
Each agent_output and agent_end event includes real-time performance data in the message object:
| Field | Type | Description |
|---|---|---|
speed |
string | Current generation speed (e.g. "12.5"). |
speedType |
string | Speed unit — always "words/s" for agent responses. |
elapsedTime |
string | Wall-clock time since the stream started (e.g. "2.4s"). |
tokenCount |
number | Total tokens generated so far. |
wordCount |
number | Total words in the accumulated response. |
These metrics update with every agent_output event, allowing you to display a live speed indicator or progress bar in your UI.
Thinking Model Support
When the agent is backed by a thinking-capable model (e.g. DeepSeek-R1, QwQ), the response may include thinking blocks alongside the answer:
{
"type": "agent_output",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"message": {
"type": "progressGenerate",
"raw": "<think>Let me work through this step by step...</think>Based on my analysis...",
"thinking": ["Let me work through this step by step..."],
"answer": ["Based on my analysis..."],
"isThinking": true
},
"result": true
}
| Field | Description |
|---|---|
isThinking |
true while the model is in a thinking phase, false when emitting the answer. |
thinking |
Array of thinking block text segments. Empty if the model does not use thinking. |
answer |
Array of answer text segments. This is the user-facing response. |
raw |
The unprocessed output including <think> tags. Use thinking and answer instead for display. |
Rendering guidance:
- While
isThinkingistrue, show a "Thinking..." indicator or render thethinkingtext in a collapsible block. - When
isThinkingbecomesfalse, the model has finished reasoning and is now producing the answer. - On
agent_end, join theanswerarray for the final display text.
Full Integration Example
A typical integration follows this pattern: call the REST API to send a message, then subscribe via WebSocket to stream the response.
Step 1 — Send a message via REST:
curl -X POST https://api.wiro.ai/v1/UserAgent/Message/Send \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"useragentguid": "your-useragent-guid",
"message": "Explain quantum computing in simple terms",
"sessionkey": "user-42"
}'
Step 2 — Subscribe via WebSocket:
const ws = new WebSocket('wss://socket.wiro.ai/v1');
ws.onopen = () => {
ws.send(JSON.stringify({
type: 'agent_info',
agenttoken: 'aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb'
}));
};
Step 3 — Handle streaming events:
← connected { version: "1.0" }
→ agent_info { agenttoken: "..." }
← agent_subscribed { status: "agent_queue", debugoutput: "" }
← agent_start { message: "" }
← agent_output { message: { raw: "Quantum", wordCount: 1 } }
← agent_output { message: { raw: "Quantum computing uses", wordCount: 3 } }
← agent_output { message: { raw: "Quantum computing uses qubits...", wordCount: 28 } }
← agent_end { message: { raw: "Quantum computing uses qubits that...", wordCount: 118 } }
← = server → client, → = client → server. Each agent_output contains the full accumulated text. Replace (don't append) your display content on each event.
Quick Reference
connected — welcome frame (server → client, sent once on upgrade):
{
"type": "connected",
"version": "1.0"
}
Subscribe frame (client → server):
{
"type": "agent_info",
"agenttoken": "aB3xK9..."
}
error — malformed subscribe (server → client, sent when agent_info is missing agenttoken):
{
"type": "error",
"message": "agenttoken-required",
"result": false
}
agent_subscribed — valid token (empty debugoutput):
{
"type": "agent_subscribed",
"agenttoken": "aB3xK9...",
"status": "agent_queue",
"debugoutput": "",
"result": true
}
agent_subscribed — unknown token (status: "unknown", no debugoutput):
{
"type": "agent_subscribed",
"agenttoken": "wrongtoken",
"status": "unknown",
"result": true
}
agent_start:
{
"type": "agent_start",
"agenttoken": "aB3xK9...",
"message": "",
"result": true
}
agent_output — streaming partials, emitted multiple times:
{
"type": "agent_output",
"agenttoken": "aB3xK9...",
"message": {
"raw": "Accumulated text so far...",
"thinking": [],
"answer": ["Accumulated text so far..."],
"isThinking": false,
"speed": "12.5",
"speedType": "words/s",
"tokenCount": 35,
"wordCount": 28
},
"result": true
}
agent_end — final response:
{
"type": "agent_end",
"agenttoken": "aB3xK9...",
"message": {
"raw": "Complete response text...",
"thinking": [],
"answer": ["Complete response text..."],
"isThinking": false,
"speed": "14.2",
"speedType": "words/s",
"tokenCount": 156,
"wordCount": 118
},
"result": true
}
agent_error — sanitized string (any exception during streaming):
{
"type": "agent_error",
"agenttoken": "aB3xK9...",
"message": "Agent is temporarily unavailable. Please try again shortly.",
"result": false
}
agent_cancel — active-processing abort only; queued-state cancels don't broadcast:
{
"type": "agent_cancel",
"agenttoken": "aB3xK9...",
"message": "AbortError",
"result": false
}
Correlating Events With Your Messages
Every agent lifecycle frame (agent_subscribed / agent_start / agent_output / agent_end / agent_error / agent_cancel) carries agenttoken — this is the only correlation key available on the wire. The wire payload does not include messageguid, sessionkey, or useragentguid. If you need any of those fields to route
events back to a specific message in your UI, build the mapping yourself when you call Message/Send.
Why agenttoken is the correlation key
Message/Sendissues exactly oneagenttokenper message and returns it alongsidemessageguidin the HTTP response.- The bridge stamps the same
agenttokenon every event it emits for that message. - Multiple connections can subscribe to the same
agenttokenand each gets the full event stream — soagenttokenis also the fan-out key. - A single socket can hold subscriptions for many
agenttokens at once — the per-eventagenttokenlets your handler dispatch into the right UI element.
Recommended client pattern
- Call
POST /UserAgent/Message/Send— you get back{ messageguid, agenttoken, status: "agent_queue", ... }. - Store the mapping
agenttoken → messageguid(oragenttoken → your UI message id). - Send
{ "type": "agent_info", "agenttoken" }on an open socket (new or existing — connections can be reused). - In
ws.onmessage, readmsg.agenttokenon every agent lifecycle frame, look up your stored mapping, and update the matching UI element. - When you see a terminal event (
agent_end/agent_error/agent_cancel), delete the mapping so it doesn't leak memory across sessions.
const tokenToMessageId = new Map()
async function sendMessage(text, uiMessageId) {
const resp = await fetch('https://api.wiro.ai/v1/UserAgent/Message/Send', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'x-api-key': 'YOUR_API_KEY' },
body: JSON.stringify({
useragentguid: 'your-useragent-guid',
message: text,
sessionkey: 'user-42'
})
}).then(r => r.json())
tokenToMessageId.set(resp.agenttoken, uiMessageId)
ws.send(JSON.stringify({ type: 'agent_info', agenttoken: resp.agenttoken }))
return { messageguid: resp.messageguid, agenttoken: resp.agenttoken }
}
ws.onmessage = (event) => {
const msg = JSON.parse(event.data)
if (!msg.agenttoken) return // control frames (connected / error) have no token
const uiMessageId = tokenToMessageId.get(msg.agenttoken)
if (!uiMessageId) return
switch (msg.type) {
case 'agent_output':
updateUI(uiMessageId, { streaming: msg.message.raw })
break
case 'agent_end':
updateUI(uiMessageId, { final: msg.message.raw, status: 'agent_end' })
tokenToMessageId.delete(msg.agenttoken)
break
case 'agent_error':
case 'agent_cancel':
updateUI(uiMessageId, { error: msg.message, status: msg.type })
tokenToMessageId.delete(msg.agenttoken)
break
}
}
Concurrency: multiple in-flight messages on one session
Sending two messages back-to-back in the same sessionkey produces two independent agenttokens. Both reach the bridge in queue order; both stream events independently. Your client must:
- Subscribe to both
agenttokens (oneagent_infoframe each — the server de-duplicates automatically). - Key all UI updates on
agenttoken, not onsessionkey(which is shared) or timestamps (which can interleave).
The server never mixes streams — every event is stamped with the originating agenttoken so routing is unambiguous even when chunks from two messages interleave on the wire.
Control frames have no agenttoken
The welcome frame and the subscribe-error frame are connection-level signals, not per-message events. They intentionally omit agenttoken:
{
"type": "connected",
"version": "1.0"
}
{
"type": "error",
"message": "agenttoken-required",
"result": false
}
Always null-check msg.agenttoken before looking it up in your mapping — see the if (!msg.agenttoken) return guard in the example above.
Reconnection & Recovery
The agent keeps running server-side regardless of whether any client is subscribed. A disconnected socket never cancels the agent. This means a dropped connection is always recoverable — just reconnect and re-subscribe with the same agenttoken.
Recovery flow
- Detect disconnect —
ws.onclose/ stream exception / ping timeout. - Reconnect — open a new WebSocket to
wss://socket.wiro.ai/v1. - Wait for welcome — receive
{ "type": "connected", "version": "1.0" }(optional but clean). - Re-subscribe — send
{ "type": "agent_info", "agenttoken": "..." }with the same token. -
Handle
agent_subscribed— the server reports the current status. Three cases:statusisagent_queue/agent_start/agent_output→ stream is still live; accumulated text so far is indebugoutput. Future events will be forwarded normally.statusisagent_end→ the agent already finished.debugoutputholds the full final response. No further WebSocket events will fire. FetchPOST /UserAgent/Message/Detailfor the canonical record (includingmetadata,attachments,endedat), then close the socket.statusisagent_error/agent_cancel→ the agent already failed / was cancelled.debugoutputmay contain partial output. No further events. FetchPOST /UserAgent/Message/Detailfor the persisted error details.
On reconnect you do not receive replays of the past agent_output frames — only events emitted after re-subscribe. Use the debugoutput on agent_subscribed as the snapshot of what you missed.
Retry strategy
- Exponential backoff, capped at 30 seconds. The server is usually responsive, so don't hammer it.
- Stop retrying once you hit a terminal event (
agent_end/agent_error/agent_cancel) or anunknownstatus — the work is either done or the token is gone. - Idempotent re-subscribe: sending the same
agenttokenagain on a fresh socket is always safe. - Fall back to polling if WebSocket is blocked (strict corporate proxies, mobile cellular with long-poll fallbacks). Use
POST /UserAgent/Message/Detailat 1–2 second intervals untilstatusis terminal.
Token Lifecycle
An agenttoken is issued per message by POST /UserAgent/Message/Send and stays addressable on the WebSocket for as long as the underlying agentmessages row exists (Wiro does not auto-purge rows on a short timer; tokens remain queryable indefinitely after the run ends).
| Event | Effect on token |
|---|---|
Message/Send |
Token is minted, row is inserted with status: "agent_queue". |
| Worker picks up | Emits agent_start to every active subscriber. |
| Each SSE chunk | Emits agent_output to every active subscriber (with full accumulated raw). |
| Stream finishes | Emits agent_end (or agent_error for "..." / internal-error content) with final progressGenerate payload; DB row status is updated to terminal. |
| Bridge exception | Emits agent_error with sanitized string; DB row status → agent_error, raw error in debugoutput. |
Message/Cancel during active stream |
Bridge aborts, emits agent_cancel; DB row status → agent_cancel. |
Message/Cancel while queued |
DB row status → agent_cancel immediately. No WebSocket event is broadcast (the bridge never started). Clients checking via the socket must consult Message/Detail for queued-state cancels. |
Multi-subscriber semantics: multiple WebSocket connections can subscribe to the same agenttoken and all receive the same event stream in parallel. The server does not enforce a subscriber limit per token. This is how the Wiro Dashboard shows the same agent chat on multiple tabs for the same user — each tab opens its own socket and subscribes independently.
Cross-user subscription: the agenttoken alone authenticates subscription — if you leak a token to another user, they can read the stream. Treat tokens like short-lived secrets scoped to the message.
Agent Webhooks
Receive agent response notifications via HTTP callbacks.
How It Works
When you send a message to an agent via POST /UserAgent/Message/Send, include a callbackurl parameter. Once the agent finishes processing — whether it completes successfully, encounters an error, or the message is cancelled — Wiro sends a POST request to your URL with the result.
This lets you build fully asynchronous workflows: fire a message and let your backend handle the response whenever it arrives, without polling or maintaining a WebSocket connection.
Setting a Callback URL
Include callbackurl in your message request body:
{
"useragentguid": "your-useragent-guid",
"message": "What are today's trending topics?",
"sessionkey": "user-123",
"callbackurl": "https://your-server.com/webhooks/agent-response"
}
The callback URL is stored per-message. You can use different URLs for different messages, or omit it entirely if you prefer polling or WebSocket.
Callback Payload
When the agent finishes, Wiro sends a POST request to your callbackurl with Content-Type: application/json. The payload contains the complete message result including structured metadata:
Successful Completion (agent_end)
{
"messageguid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"status": "agent_end",
"content": "What are today's trending topics?",
"response": "Here are today's trending topics in tech...",
"debugoutput": "Here are today's trending topics in tech...",
"metadata": {
"type": "progressGenerate",
"task": "Generate",
"speed": "14.2",
"speedType": "words/s",
"elapsedTime": "8.1s",
"tokenCount": 156,
"wordCount": 118,
"raw": "Here are today's trending topics in tech...",
"thinking": [],
"answer": ["Here are today's trending topics in tech..."],
"isThinking": false
},
"endedat": 1712050004
}
Error (agent_error)
{
"messageguid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"status": "agent_error",
"content": "What are today's trending topics?",
"response": "Could not resolve agent endpoint",
"debugoutput": "Could not resolve agent endpoint",
"metadata": {},
"endedat": 1712050004
}
Cancelled (agent_cancel)
{
"messageguid": "c3d4e5f6-a7b8-9012-cdef-345678901234",
"status": "agent_cancel",
"content": "What are today's trending topics?",
"response": "AbortError",
"debugoutput": "AbortError",
"metadata": {},
"endedat": 1712050004
}
The response and debugoutput fields contain the raw abort reason from the runtime — typically "AbortError" or a short technical string. Do not rely on a specific fixed user-facing message; use status === "agent_cancel" as the signal.
When the cancel webhook fires: agent_cancel is delivered only when the agent bridge catches an AbortError during active processing — i.e. the message had already started on the agent side and was aborted mid-flight (via POST /UserAgent/Message/Cancel or an upstream timeout). For messages cancelled before they reach the bridge (still queued, or an instant
Message/Cancel that beats dispatching), the message is marked agent_cancel in the database and returned as such in POST /UserAgent/Message/Detail, but no webhook is fired. Use POST /UserAgent/Message/Detail (checking status === "agent_cancel") as the canonical source of truth for cancellation; WebSocket subscribers receive the agent_cancel event only on active-processing aborts
(same condition as the webhook). Treat the webhook as a best-effort "processing was interrupted" signal.
Field Reference
| Field | Type | Description |
|---|---|---|
messageguid |
string | Unique identifier of the message. Use this to correlate with your records. |
status |
string | Final status: agent_end, agent_error, or agent_cancel. |
content |
string | The original user message you sent. |
response |
string | The agent's full response text on success. For errors, contains the error message. For cancellation, contains the abort reason. |
debugoutput |
string | Same as response — the full accumulated output text. Included for consistency with the polling API. |
metadata |
object | Structured response data. Contains thinking/answer separation, performance metrics, and raw text. Empty object ({}) for error and cancel statuses. |
endedat |
number | Unix timestamp (UTC seconds) when processing finished. |
The metadata Object
On successful completion (agent_end), the metadata object contains the structured response with thinking/answer separation and real-time metrics:
| Field | Type | Description |
|---|---|---|
type |
string | Always "progressGenerate". |
task |
string | Always "Generate". |
raw |
string | The complete response text including any <think> tags. |
thinking |
array | Array of reasoning/chain-of-thought blocks extracted from <think>...</think> tags. Empty if the model doesn't use thinking. |
answer |
array | Array of response segments — the content to show the user. |
isThinking |
boolean | Always false in webhooks (streaming is complete). |
speed |
string | Final generation speed (e.g. "14.2"). |
speedType |
string | Speed unit — "words/s". |
elapsedTime |
string | Total generation time (e.g. "8.1s"). |
tokenCount |
number | Total tokens generated. |
wordCount |
number | Total words in the response. |
For agent_error and agent_cancel, metadata is an empty object {}. Always check status before accessing metadata fields.
Status Values
| Status | Description | response contains |
metadata contains |
|---|---|---|---|
agent_end |
Agent completed successfully | Full response text | Structured data with thinking, answer, metrics |
agent_error |
An error occurred during processing | Error message string | Empty object {} |
agent_cancel |
Message was cancelled before completion | Cancellation reason | Empty object {} |
Retry Policy
Wiro attempts to deliver each webhook up to 3 times:
- First attempt is immediate when the agent finishes
- 2-second delay between retries
- Your endpoint must return HTTP 200 to acknowledge receipt
- Any non-200 response triggers a retry
- After 3 failed attempts, the webhook is abandoned and the failure is logged server-side
The message result is always persisted in the database regardless of webhook delivery. You can retrieve it at any time via POST /UserAgent/Message/Detail.
Security Considerations
- Webhook calls do not include authentication headers — verify incoming requests by checking the
messageguidagainst your own records - Always use HTTPS endpoints in production
- Validate the payload structure before processing
- Consider returning 200 immediately and processing the payload asynchronously to avoid timeouts
Code Examples
Below are complete webhook receiver implementations and a sample request with a callback URL. Select your language from the code panel.
Agent Credentials & OAuth
Configure third-party service connections for your agent instances.
Integration Catalog
OAuth Integrations
- Meta Ads — own mode only (Wiro mode coming soon). End-to-end setup with no App Review required.
- Facebook Page — own mode only (Wiro mode coming soon). Includes multi-page selection via
FBSetPage. - Instagram — own mode only (Wiro mode coming soon). Requires Instagram Business Account linked to a Facebook Page.
- LinkedIn — own mode only (Wiro mode coming soon). Company Page publishing.
- Twitter / X — Wiro mode + own mode. OAuth 2.0 PKCE.
- TikTok — Wiro mode + own mode. Content Posting API.
- Google Ads — Wiro mode + own mode. MCC + customer ID selection.
- HubSpot — Wiro mode + own mode. CRM object management.
- Mailchimp — Wiro mode + own mode + direct API key.
- Google Drive — Wiro mode + own mode. Scoped folder access.
API Key Integrations
- Gmail — App Password via IMAP/SMTP.
- Telegram — Bot Token + allowed users.
- Firebase — FCM service account JSON.
- WordPress — Application Password + REST API.
- App Store Connect — ES256 API key + .p8 private key.
- Google Play — Service account + Play Console permissions.
- Apollo — API key + optional master key.
- Lemlist — API key.
- Brevo — API key.
- SendGrid — API key.
Overview
After deploying an agent, call POST /UserAgent/Detail to see its full configuration. The response includes:
configuration.credentials— which services the agent connects to and which fields are editable (_editable: true)configuration.custom_skills— configurable automated tasks the agent can run on a schedulesetuprequired— whether credentials still need to be filled before the agent can start
Use this response to build your credential form or automate setup. Then update credentials and skills via POST /UserAgent/Update.
Two Types of Credentials
- API Key credentials — set directly via
POST /UserAgent/Update(Telegram, WordPress, Gmail, Brevo, etc.) - OAuth credentials — redirect-based authorization flow via
POST /UserAgentOAuth/{Provider}Connect(Twitter, Instagram, Facebook, Google Ads, etc.)
Setting Credentials & Skills
Use POST /UserAgent/Update with configuration to set credentials and custom skills. You can update credentials, custom_skills, or both in a single request.
Request Format
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"<service>": {
"<field>": "value"
}
},
"custom_skills": [
{
"key": "skill-name",
"enabled": true,
"interval": "0 9 * * *"
}
]
}
}
POST /UserAgent/Detail first to see which fields are available and editable (_editable: true). Only editable fields can be set — non-editable fields are silently ignored.Credential Configuration by Agent
Each agent requires different credentials. Select your agent below to see exactly which credentials to configure and the complete POST /UserAgent/Update request.
Manages social media accounts — posts, replies, scheduling across Twitter/X, Instagram, Facebook, LinkedIn, TikTok. OAuth providers are connected separately via the OAuth flow.
| Service | Type | Fields |
|---|---|---|
twitter |
OAuth | Connected via XConnect. Set authMethod to "wiro" or "own". |
instagram |
OAuth | Connected via IGConnect. |
facebook |
OAuth | Connected via FBConnect. |
linkedin |
OAuth | Connected via LIConnect. Also set organizationId. |
tiktok |
OAuth | Connected via TikTokConnect. |
gmail |
API Key | account, appPassword |
telegram |
API Key | botToken, allowedUsers, sessionMode |
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"gmail": {
"account": "[email protected]",
"appPassword": "xxxx xxxx xxxx xxxx"
},
"telegram": {
"botToken": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"allowedUsers": ["761381461"],
"sessionMode": [
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
}
}
}
}
gmail and telegram credentials.Publishes blog posts to WordPress, monitors a Gmail inbox for content requests.
| Service | Fields | Description |
|---|---|---|
wordpress |
url, user, appPassword |
WordPress site URL, username, and application password |
gmail |
account, appPassword |
Gmail address + Google App Password for inbox monitoring |
telegram |
botToken, allowedUsers, sessionMode |
Telegram bot for operator notifications |
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"wordpress": {
"url": "https://blog.example.com",
"user": "WiroBlogAgent",
"appPassword": "xxxx xxxx xxxx xxxx"
},
"gmail": {
"account": "[email protected]",
"appPassword": "xxxx xxxx xxxx xxxx"
},
"telegram": {
"botToken": "123456:ABC-DEF1234ghIkl",
"allowedUsers": ["761381461"],
"sessionMode": [
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
}
}
}
}
Monitors and replies to App Store and Google Play reviews.
| Service | Fields | Description |
|---|---|---|
appstore |
keyId, issuerId, privateKeyBase64, appIds, supportEmail |
App Store Connect API credentials |
googleplay |
serviceAccountJsonBase64, packageNames, supportEmail |
Google Play service account |
telegram |
botToken, allowedUsers, sessionMode |
Telegram bot for operator notifications |
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"appstore": {
"keyId": "ABC1234DEF",
"issuerId": "12345678-1234-1234-1234-123456789012",
"privateKeyBase64": "LS0tLS1CRUdJTi...",
"appIds": ["6479306352"],
"supportEmail": "[email protected]"
},
"googleplay": {
"serviceAccountJsonBase64": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii...",
"packageNames": ["com.example.app"],
"supportEmail": "[email protected]"
},
"telegram": {
"botToken": "123456:ABC-DEF1234ghIkl",
"allowedUsers": ["761381461"],
"sessionMode": [
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
}
}
}
}
Suggests and creates App Store in-app events based on holidays and trends.
| Service | Fields | Description |
|---|---|---|
appstore |
keyId, issuerId, privateKeyBase64, appIds |
App Store Connect API credentials |
telegram |
botToken, allowedUsers, sessionMode |
Telegram bot for operator notifications |
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"appstore": {
"keyId": "ABC1234DEF",
"issuerId": "12345678-1234-1234-1234-123456789012",
"privateKeyBase64": "LS0tLS1CRUdJTi...",
"appIds": ["6479306352"]
},
"telegram": {
"botToken": "123456:ABC-DEF1234ghIkl",
"allowedUsers": ["761381461"],
"sessionMode": [
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
}
}
}
}
Sends targeted push notifications via Firebase Cloud Messaging.
| Service | Fields | Description |
|---|---|---|
firebase |
accounts[] |
Array of Firebase projects. Each: appName, serviceAccountJsonBase64, apps (platform + id), topics (array of {topicKey, topicDesc}) |
telegram |
botToken, allowedUsers, sessionMode |
Telegram bot for operator notifications |
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"firebase": {
"accounts": [
{
"appName": "My App",
"serviceAccountJsonBase64": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii...",
"apps": [
{
"platform": "ios",
"id": "6479306352"
},
{
"platform": "android",
"id": "com.example.app"
}
],
"topics": [
{
"topicKey": "locale_en",
"topicDesc": "English-speaking users"
},
{
"topicKey": "tier_paid",
"topicDesc": "Paid subscribers"
}
]
}
]
},
"telegram": {
"botToken": "123456:ABC-DEF1234ghIkl",
"allowedUsers": ["761381461"],
"sessionMode": [
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
}
}
}
}
Creates and sends newsletters via Brevo, SendGrid, HubSpot, or Mailchimp.
| Service | Type | Fields |
|---|---|---|
brevo |
API Key | apiKey |
sendgrid |
API Key | apiKey |
hubspot |
OAuth | Connected via HubSpotConnect |
mailchimp |
OAuth/Key | OAuth via MailchimpConnect or set apiKey directly |
newsletter |
Config | testEmail |
telegram |
API Key | botToken, allowedUsers, sessionMode |
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"brevo": { "apiKey": "xkeysib-abc123..." },
"sendgrid": { "apiKey": "SG.xxxx..." },
"newsletter": { "testEmail": "[email protected]" },
"telegram": {
"botToken": "123456:ABC-DEF1234ghIkl",
"allowedUsers": ["761381461"],
"sessionMode": [
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
}
}
}
}
apiKey without OAuth.Finds leads and manages outreach campaigns via Apollo.io, Lemlist, and HubSpot.
| Service | Type | Fields |
|---|---|---|
apollo |
API Key | apiKey, masterApiKey (optional, for sequences) |
lemlist |
API Key | apiKey |
hubspot |
OAuth | Connected via HubSpotConnect |
telegram |
API Key | botToken, allowedUsers, sessionMode |
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"apollo": {
"apiKey": "your-apollo-api-key",
"masterApiKey": "your-master-key"
},
"lemlist": { "apiKey": "your-lemlist-key" },
"telegram": {
"botToken": "123456:ABC-DEF1234ghIkl",
"allowedUsers": ["761381461"],
"sessionMode": [
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
}
}
}
}
Manages Google Ads campaigns, keywords, and ad copy.
| Service | Type | Fields |
|---|---|---|
googleads |
OAuth + Config | OAuth via GAdsConnect, then set customerId via GAdsSetCustomerId. Also set developerToken and managerCustomerId for "own" mode. |
website |
Config | urls — array of { websiteName, url } |
appstore |
Config | apps — array of { appName, appId } |
googleplay |
Config | apps — array of { appName, packageName } |
telegram |
API Key | botToken, allowedUsers, sessionMode |
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"website": {
"urls": [
{
"websiteName": "Main Site",
"url": "https://example.com"
}
]
},
"appstore": {
"apps": [
{
"appName": "My iOS App",
"appId": "6479306352"
}
]
},
"googleplay": {
"apps": [
{
"appName": "My Android App",
"packageName": "com.example.app"
}
]
},
"telegram": {
"botToken": "123456:ABC-DEF1234ghIkl",
"allowedUsers": ["761381461"],
"sessionMode": [
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
}
}
}
}
POST /UserAgentOAuth/GAdsSetCustomerId.Manages Meta (Facebook/Instagram) ad campaigns and creatives.
| Service | Type | Fields |
|---|---|---|
metaads |
OAuth + Config | OAuth via MetaAdsConnect, then set ad account via MetaAdsSetAdAccount. Also set pageId for Facebook page association. |
website |
Config | urls — array of { websiteName, url } |
appstore |
Config | apps — array of { appName, appId } |
googleplay |
Config | apps — array of { appName, packageName } |
telegram |
API Key | botToken, allowedUsers, sessionMode |
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"website": {
"urls": [
{
"websiteName": "Landing Page",
"url": "https://example.com"
}
]
},
"appstore": {
"apps": [
{
"appName": "My iOS App",
"appId": "6479306352"
}
]
},
"googleplay": {
"apps": [
{
"appName": "My Android App",
"packageName": "com.example.app"
}
]
},
"telegram": {
"botToken": "123456:ABC-DEF1234ghIkl",
"allowedUsers": ["761381461"],
"sessionMode": [
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
}
}
}
}
POST /UserAgentOAuth/MetaAdsSetAdAccount.Credential Field Reference
Quick reference for all credential field names across services:
| Service Key | Editable Fields |
|---|---|
telegram |
botToken, allowedUsers, sessionMode |
wordpress |
url, user, appPassword |
gmail |
account, appPassword |
brevo |
apiKey |
sendgrid |
apiKey |
apollo |
apiKey, masterApiKey |
lemlist |
apiKey |
newsletter |
testEmail |
appstore |
keyId, issuerId, privateKeyBase64, appIds — or apps array for ads agents |
googleplay |
serviceAccountJsonBase64, packageNames — or apps array for ads agents |
firebase |
accounts[]: appName, serviceAccountJsonBase64, apps [{platform,id}], topics [{topicKey,topicDesc}] |
website |
urls array of { websiteName, url } |
twitter |
OAuth — authMethod (own: + clientId, clientSecret) |
instagram |
OAuth — authMethod (own: + appId, appSecret) |
facebook |
OAuth — authMethod (own: + appId, appSecret) |
linkedin |
OAuth — authMethod, organizationId (own: + clientId, clientSecret) |
tiktok |
OAuth — authMethod (own: + clientKey, clientSecret) |
googleads |
OAuth — authMethod, customerId, developerToken, managerCustomerId (own: + clientId, clientSecret) |
metaads |
OAuth — authMethod, adAccountId, pageId (own: + appId, appSecret) |
hubspot |
OAuth — authMethod (own: + clientId, clientSecret) |
mailchimp |
OAuth — authMethod, apiKey (own: + clientId, clientSecret) |
Setup Required State
If an agent has required (non-optional) credentials that haven't been filled in, the agent is in Setup Required state (status 6) and cannot be started. After setting all required credentials via Update, the status automatically changes to 0 (Stopped) and you can call Start.
Check the setuprequired boolean in UserAgent/Detail or UserAgent/MyAgents responses to determine if credentials still need to be configured.
OAuth Authorization Flow
For services that require user authorization (social media accounts, ad platforms, CRMs), Wiro implements a full OAuth flow. The entire process is fully white-label — your end-users interact only with your app and the provider's consent screen. They never see or visit wiro.ai at any point.
redirectUrl you pass to the Connect endpoint is your own URL. After authorization, users are redirected back to your app — not to Wiro. Any HTTPS URL is accepted. Use http://localhost or http://127.0.0.1 for development.Supported OAuth Providers
| Provider | Connect Endpoint | Redirect Success Params | Redirect Error Params |
|---|---|---|---|
| Twitter/X | XConnect |
x_connected=true&x_username=... |
x_error=... |
| TikTok | TikTokConnect |
tiktok_connected=true&tiktok_username=... |
tiktok_error=... |
IGConnect |
ig_connected=true&ig_username=... |
ig_error=... |
|
FBConnect |
fb_connected=true&fb_pagename=... |
fb_error=... |
|
LIConnect |
li_connected=true&li_name=... |
li_error=... |
|
| Google Ads | GAdsConnect |
gads_connected=true&gads_accounts=[...] |
gads_error=... |
| Meta Ads | MetaAdsConnect |
metaads_connected=true&metaads_accounts=[...] |
metaads_error=... |
| HubSpot | HubSpotConnect |
hubspot_connected=true&hubspot_portal=...&hubspot_name=... |
hubspot_error=... |
| Mailchimp | MailchimpConnect |
mailchimp_connected=true&mailchimp_account=... |
mailchimp_error=... |
Flow Diagram
Your App (Frontend) Your Backend Wiro API Provider (e.g. Twitter)
| | | |
(1) | "Connect Twitter" click | | |
|--------------------------->| | |
| | POST /XConnect | |
(2) | |--> { userAgentGuid, | |
| | redirectUrl, | |
| | authMethod } | |
| | | |
(3) | |<-- { authorizeUrl } | |
| | | |
(4) |<--- redirect to authorizeUrl | |
|--------------------------------------------------------> User sees Twitter |
| | | consent screen |
(5) | | |<-- User clicks Allow |
| | | |
(6) | | (invisible callback)| |
| | Wiro exchanges code |<-----------------------|
| | for tokens, saves | |
| | them to agent config| |
| | | |
(7) |<------- 302 redirect to YOUR redirectUrl ----------------------------------|
| https://your-app.com/settings?x_connected=true&x_username=johndoe |
| | | |
What the User Sees
| Step | User Sees | URL |
|---|---|---|
| 1 | Your app — "Connect Twitter" button | https://your-app.com/settings |
| 2–3 | (Backend API call — invisible to user) | — |
| 4–5 | Provider's consent screen (Twitter, TikTok, etc.) | https://x.com/i/oauth2/authorize?... |
| 6 | (Wiro's server-side callback — invisible 302 redirect) | — |
| 7 | Your app — "Connected!" confirmation | https://your-app.com/settings?x_connected=true |
Your users never visit wiro.ai. The only pages they see are your app and the provider's authorization screen.
Connect Endpoint
POST /UserAgentOAuth/{Provider}Connect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | The agent instance GUID |
redirectUrl |
string | Yes | Where to redirect after OAuth completes (HTTPS or localhost) |
authMethod |
string | No | "wiro" (default) or "own" |
Response
{
"result": true,
"authorizeUrl": "https://x.com/i/oauth2/authorize?response_type=code&client_id=...",
"errors": []
}
Auth Methods — "wiro" vs "own"
Both modes produce the same white-label user experience. The only difference is whose OAuth app credentials are used for the authorization flow:
"wiro" (default) |
"own" |
|
|---|---|---|
| OAuth app credentials | Wiro's pre-configured app | Your own app from the provider's developer portal |
| Setup required | None — just call Connect | Create an app on the provider, set credentials via Update, register Wiro's callback URL |
| Consent screen branding | Shows "Wiro" as the app name | Shows your app name and branding |
| Redirect after auth | To your redirectUrl |
To your redirectUrl |
| User sees wiro.ai? | No | No |
| Token management | Automatic by Wiro | Automatic by Wiro |
| Best for | Quick setup, prototyping, most use cases | Custom branding on consent screen, custom scopes |
"wiro" mode. It works out of the box with no configuration. Switch to "own" only if you need your brand name on the provider's consent screen or require custom OAuth scopes/permissions.To use "own" mode, first set your app credentials via POST /UserAgent/Update, then call Connect with authMethod: "own". Each provider requires different credential field names:
"own" Mode Credentials per Provider
| Provider | Credential Key | Required Fields | Request Example |
|---|---|---|---|
| Twitter/X | twitter |
clientId, clientSecret |
|
| TikTok | tiktok |
clientKey, clientSecret |
|
instagram |
appId, appSecret |
|
|
facebook |
appId, appSecret |
|
|
linkedin |
clientId, clientSecret, organizationId |
|
|
| Google Ads | googleads |
clientId, clientSecret, developerToken, managerCustomerId |
|
| Meta Ads | metaads |
appId, appSecret |
|
| HubSpot | hubspot |
clientId, clientSecret |
|
| Mailchimp | mailchimp |
clientId, clientSecret (or apiKey without OAuth) |
|
clientKey not clientId, Instagram/Facebook use appId/appSecret not clientId/clientSecret). Always use the exact field names from the table above."own" Mode Full Flow
Step 1 — Set your app credentials:
{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"twitter": {
"clientId": "your-twitter-client-id",
"clientSecret": "your-twitter-client-secret"
}
}
}
}
Step 2 — Initiate OAuth with authMethod: "own":
{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/callback",
"authMethod": "own"
}
Step 3 — Redirect the user to the authorizeUrl returned by the Connect endpoint.
Step 4 — The provider sends the user back to your redirectUrl with ?result=true (or ?result=false&error=...) once consent is complete.
When using "own" mode, you must register Wiro's callback URL in your OAuth app settings on the provider's developer portal:
https://api.wiro.ai/v1/UserAgentOAuth/{Provider}Callback
Status Check
Check whether a provider is connected for a given agent instance.
POST /UserAgentOAuth/{Provider}Status
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | The agent instance GUID |
Response
{
"result": true,
"connected": true,
"username": "johndoe",
"connectedAt": "2025-04-01T12:00:00.000Z",
"tokenExpiresAt": "2025-04-01T14:00:00.000Z",
"refreshTokenExpiresAt": "2025-10-01T12:00:00.000Z",
"errors": []
}
| Field | Description | Providers |
|---|---|---|
connected |
Whether the provider is connected | All |
username |
Connected account name or identifier | Most providers |
linkedinName |
LinkedIn profile name (replaces username) |
LinkedIn only |
customerId |
Google Ads customer ID (replaces username) |
Google Ads only |
connectedAt |
ISO timestamp of when the account was connected | All |
tokenExpiresAt |
ISO timestamp of access token expiry | All except Mailchimp |
refreshTokenExpiresAt |
ISO timestamp of refresh token expiry | Twitter/X, TikTok, LinkedIn |
Disconnect
Revoke access and remove stored tokens for a provider.
POST /UserAgentOAuth/{Provider}Disconnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | The agent instance GUID |
Response
{
"result": true,
"errors": []
}
Wiro attempts to revoke the token on the provider's side before clearing it from the configuration. The agent restarts automatically if it was running.
Token Refresh
Manually trigger a token refresh for a connected provider.
POST /UserAgentOAuth/TokenRefresh
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | The agent instance GUID |
provider |
string | Yes | One of: twitter, tiktok, instagram, facebook, linkedin, googleads, metaads, hubspot |
Response
{
"result": true,
"accessToken": "new-access-token...",
"refreshToken": "new-refresh-token...",
"errors": []
}
The agent restarts automatically after a token refresh if it was running.
Extra Provider Endpoints
Google Ads — Set Customer ID
After connecting Google Ads via OAuth, you must set the Google Ads customer ID to target:
POST /UserAgentOAuth/GAdsSetCustomerId
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | The agent instance GUID |
customerId |
string | Yes | Google Ads customer ID (e.g. "123-456-7890"). Non-digit characters are stripped automatically. |
Response
{
"result": true,
"customerId": "1234567890",
"errors": []
}
Meta Ads — Set Ad Account
After connecting Meta Ads via OAuth, set the ad account to manage:
POST /UserAgentOAuth/MetaAdsSetAdAccount
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | The agent instance GUID |
adAccountId |
string | Yes | Meta Ads account ID (e.g. "act_123456789"). The act_ prefix is stripped automatically. |
adAccountName |
string | No | Display name for the ad account |
Response
{
"result": true,
"errors": []
}
Facebook Page — Set Page (Multi-Page Selection)
When the OAuth callback returns multiple Facebook Pages in fb_pages, let the user pick one and persist the choice with this endpoint. Call within 15 minutes of the callback (page list is cached server-side with 15-minute TTL).
POST /UserAgentOAuth/FBSetPage
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | The agent instance GUID |
pageId |
string | Yes | ID of the Facebook Page selected from fb_pages. |
pageName |
string | No | Display name to store. If omitted, Wiro uses the name from the cached page list. |
Response
{
"result": true,
"errors": []
}
Wiro automatically persists the selected page's access token (stored server-side only) and restarts the agent if it was running. See the Facebook Page integration guide for the full flow.
Custom Skills
Agents support configurable skills — preferences and scheduled tasks. See the dedicated Agent Skills page for the complete guide with examples.
Security
- Tokens are stored server-side in the agent instance configuration. The
TokenRefreshendpoint returns new tokens — all other endpoints (Status, Detail, Update) sanitize token fields before responding. - The
redirectUrlreceives only connection status parameters — no tokens, no secrets - API responses from Status, Detail, and Update endpoints are sanitized:
accessToken,refreshToken,clientSecret, andappSecretfields are stripped before returning - OAuth state parameters use a 15-minute TTL cache to prevent replay attacks
- Redirect URLs must be HTTPS (or localhost for development)
For Third-Party Developers
If you're building a product on top of Wiro agents and need your customers to connect their own accounts (e.g., their Twitter, their Google Ads), here's the recommended flow:
Architecture
- Deploy an agent instance per customer via
POST /UserAgent/Deploy - Connect — your backend calls
POST /UserAgentOAuth/{Provider}Connectwith the customer'suserAgentGuidand aredirectUrlpointing back to your app - Redirect — send your customer's browser to the returned
authorizeUrl - Authorize — customer logs in and authorizes on the provider
- Return — customer lands back on your
redirectUrlwith success/error query parameters - Verify — call
POST /UserAgentOAuth/{Provider}Statusto confirm connection
Your customers never interact with Wiro directly. The entire flow happens through your app, and Wiro handles token management behind the scenes.
Handling the Redirect in Your App
// Express route handling the OAuth redirect
app.get('/settings/social', (req, res) => {
const provider = req.query.provider;
if (req.query.x_connected === 'true') {
const username = req.query.x_username;
return res.redirect(`/dashboard?connected=${provider}&username=${username}`);
}
if (req.query.x_error) {
const error = req.query.x_error;
return res.redirect(`/dashboard?error=${provider}&reason=${error}`);
}
});
Error Values
OAuth redirect error parameters follow the pattern {provider_prefix}_error. Possible values:
| Error | Description |
|---|---|
authorization_denied |
User declined the authorization |
token_exchange_failed |
Provider accepted the code but token exchange failed |
useragent_not_found |
The agent instance GUID is invalid or unauthorized |
invalid_config |
Agent configuration doesn't have credentials for this provider |
internal_error |
Unexpected server error during callback processing |
Agent Skills
Configure agent behavior with editable preferences and scheduled automation tasks.
Overview
Every agent has a set of custom skills that define its behavior. Skills come in two types:
| Type | Has Interval | Purpose | What You Can Change |
|---|---|---|---|
| Preferences | No (null) |
Instructions that shape agent behavior — tone, style, targeting rules, content strategy | value, description, enabled |
| Scheduled Tasks | Yes (cron) | Automated actions that run on a schedule — scanning, reporting, dispatching | enabled, interval |
Call POST /UserAgent/Detail to discover an agent's skills. They appear in configuration.custom_skills.
Discovering Skills
Call POST /UserAgent/Detail to fetch the agent instance. The skill array lives under configuration.custom_skills.
Request:
{
"guid": "your-useragent-guid"
}
Response (configuration.custom_skills excerpt):
[
{
"key": "content-tone",
"value": "## Voice\nShort punchy lines, developer-friendly...",
"description": "Brand voice, hashtags, and posting style",
"enabled": true,
"interval": null,
"_editable": true
},
{
"key": "cron-content-scanner",
"value": "",
"description": "What content to find and post about",
"enabled": true,
"interval": "0 * * * *",
"_editable": false
}
]
| Field | Type | Description |
|---|---|---|
key |
string | Unique skill identifier. Use this in Update requests. |
value |
string | Skill instructions/content. Visible only when _editable: true — otherwise empty string. |
description |
string | Human-readable description of what the skill does. Read-only — set by the agent template, never accepted in Update payloads. |
enabled |
boolean | Whether the skill is active. Writable on cron skills only (keys prefixed with cron-). Dropped by the backend if sent on a preference skill. |
interval |
string | null | Cron expression for scheduled execution, or null for preference skills. Writable on cron skills only. Dropped if sent on a preference skill. |
_editable |
boolean | true for preference skills (you can update value), false for cron skills (you can update enabled / interval). See Skill types cheat-sheet below. |
Skill Types Cheat-Sheet
The backend decides which fields are writable by looking at the skill's key prefix:
| Skill type | Key prefix | _editable |
Writable fields | Silently dropped if sent |
|---|---|---|---|---|
| Cron skill (scheduled task) | cron-* |
false |
enabled, interval |
value, description, and everything else |
| Preference skill (instructions) | no prefix | true |
value |
enabled, interval, description, and everything else |
The cron- prefix is the canonical discriminator. Both mergeUserConfig (backend) and the Wiro Dashboard UI branch on this prefix. description is never writable from the client on either skill type — it's a template-only field. The interval field still carries the cron expression for scheduled tasks, but the skill type is determined by the key prefix.
Cross-type fields are dropped at merge time. If you send
{ key: "content-tone", enabled: false }(preference skill + cron-only field), the backend dropsenabledbefore writing — no error is raised, but the change is not persisted. Same the other way:{ key: "cron-content-scanner", value: "..." }dropsvalue.descriptionis dropped regardless of skill type. Always fetchPOST /UserAgent/Detailfirst to see the realkey(with or withoutcron-prefix) and pick the right update shape.
Updating Preferences
Preference skills (_editable: true, interval: null) let you customize the agent's behavior by editing its instructions. Every agent has one — it controls tone, style, targeting, and strategy. Select your agent below to see its preference skill and a complete update request.
Send only
valueto a preference skill.enabled,interval, anddescriptionare all dropped bymergeUserConfigon preference skills. They have no runtime effect anyway — preference skills are read on-demand by cron tasks viacs-<slug>; they're never scheduled themselves. If you need the skill's display description, read it fromPOST /UserAgent/Detail.
Skill key: content-tone — Controls brand voice, hashtags, and posting style per platform.
{
"guid": "your-social-manager-guid",
"configuration": {
"custom_skills": [
{
"key": "content-tone",
"value": "## Brand Voice\nTone: Professional and informative. No slang.\nTarget Audience: Developers and product managers\nHashtag Strategy: Max 3 per post. Always include #AI and #YourBrand.\n\n## Content Sources\nPrimary Source: https://your-site.com/blog/feed.xml\nCTA URL Pattern: https://your-site.com/posts/{slug}\n\n## Posting Style\nEvery post must include a link.\nUse bullet points for features.\n\n## Platform Rules\n- Twitter: Thread format for long content\n- Instagram: Carousel with square images only\n- LinkedIn: Professional tone, no emojis\n- TikTok: Short captions, trending sounds"
}
]
}
}
Preview: what the agent sees
Voice
Professional and informative. No slang.Hashtags
Max 3 per post. Always include #AI and #YourBrand.Content Sources
Primary Source: https://your-site.com/blog/feed.xml CTA URL Pattern: https://your-site.com/posts/{slug}Posting Style
Every post must include a link. Use bullet points for features.Platform Rules
- Twitter: Thread format for long content - Instagram: Carousel with square images only - LinkedIn: Professional tone, no emojis - TikTok: Short captions, trending soundsThe value is markdown-formatted text. Use ## headings to organize sections. The agent reads these instructions before every action.
Skill key: content-strategy — Controls writing style, topics, research rules, and blog categories.
{
"guid": "your-blog-editor-guid",
"configuration": {
"custom_skills": [
{
"key": "content-strategy",
"value": "## Writing Style\n- Short simple sentences. Max 15-20 words.\n- Use active voice.\n- No filler phrases.\n\n## Topics\n- AI model comparisons\n- Prompt engineering tutorials\n- Industry trend analysis\n\n## Blog Types\nRotate between: Single Review, VS Comparison, Category Roundup, Prompt Showcase\n\n## WordPress Categories\nUse: AI, Tutorial, Comparison, News"
}
]
}
}
Preview: what the agent sees
Writing Style
- Short simple sentences. Max 15-20 words.
- Use active voice.
- No filler phrases.
Topics
- AI model comparisons
- Prompt engineering tutorials
- Industry trend analysis
Blog Types
Rotate between: Single Review, VS Comparison, Category Roundup, Prompt Showcase
WordPress Categories
Use: AI, Tutorial, Comparison, News
Skill key: review-preferences — Controls response tone, support channels, and custom rules for review replies.
{
"guid": "your-app-review-guid",
"configuration": {
"custom_skills": [
{
"key": "review-preferences",
"value": "## Response Tone\nCasual and natural. Not corporate.\nShow empathy for negative reviews.\n\n## Support Channels\nDirect users to: [email protected]\nFor billing: [email protected]\n\n## Rules\n- Always thank 5-star reviewers\n- For bugs: acknowledge and promise investigation\n- Never argue with users\n- Max response length: 3 sentences"
}
]
}
}
Preview: what the agent sees
Response Tone
Casual and natural. Not corporate. Show empathy for negative reviews.
Support Channels
Direct users to: [email protected]
For billing: [email protected]
Rules
- Always thank 5-star reviewers
- For bugs: acknowledge and promise investigation
- Never argue with users
- Max response length: 3 sentences
Skill key: event-preferences — Controls target regions, holiday priorities, and event style.
{
"guid": "your-app-event-guid",
"configuration": {
"custom_skills": [
{
"key": "event-preferences",
"value": "## Target Regions\nUS, TR, DE, GB\n\n## Holiday Priorities\nHigh: New Year, Black Friday, Christmas\nMedium: Valentine's Day, Halloween\nSkip: Regional holidays outside target regions\n\n## Event Style\nShort punchy titles (max 30 chars).\nAction-oriented descriptions.\nAlways tie events to app features."
}
]
}
}
Preview: what the agent sees
Target Regions
US, TR, DE, GB
Holiday Priorities
High: New Year, Black Friday, Christmas
Medium: Valentine's Day, Halloween
Skip: Regional holidays outside target regions
Event Style
Short punchy titles (max 30 chars). Action-oriented descriptions.
Skill key: push-preferences — Controls push notification tone, language, targeting, and holiday choices.
{
"guid": "your-push-agent-guid",
"configuration": {
"custom_skills": [
{
"key": "push-preferences",
"value": "## Push Tone\nFriendly and casual.\nTurkish for locale_tr users, English for locale_en.\n\n## Emoji Usage\nMax 1 emoji per push.\n\n## Holiday Preferences\nFocus on: New Year, Ramadan, Republic Day\nSkip: Valentine's Day, Halloween\n\n## Targeting\nAlways segment by locale.\nSend paid-tier users a premium version.\nFree users get engagement-focused pushes."
}
]
}
}
Preview: what the agent sees
Push Tone
Friendly and casual. Turkish for locale_tr users, English for locale_en.
Emoji Usage
Max 1 emoji per push.
Holiday Preferences
Focus on: New Year, Ramadan, Republic Day
Skip: Valentine's Day, Halloween
Targeting
Always segment by locale. Premium version for paid users.
Skill key: newsletter-strategy — Controls newsletter topics, tone, audience, frequency, and branding.
{
"guid": "your-newsletter-guid",
"configuration": {
"custom_skills": [
{
"key": "newsletter-strategy",
"value": "## Topics\nIndustry news, product updates, tutorials.\nPull content from: https://your-site.com/blog/feed.xml\n\n## Tone\nFriendly and educational.\nUse first person plural (we, our).\n\n## Frequency\nWeekly on Mondays.\n\n## Branding\nSubject line prefix: [YourBrand Weekly]\nAlways include unsubscribe link.\nMax 3 sections per newsletter."
}
]
}
}
Preview: what the agent sees
Topics
Industry news, product updates, tutorials. Pull from your own blog or source feed.
Tone
Friendly and educational. First person plural.
Frequency
Weekly on Mondays.
Branding
Subject line prefix: [YourBrand Weekly]. Max 3 sections per newsletter.
Skill key: lead-strategy — Controls ICP definition, outreach tone, email templates, and scoring rules.
{
"guid": "your-leadgen-guid",
"configuration": {
"custom_skills": [
{
"key": "lead-strategy",
"value": "## Our Business\nCompany: Acme Corp\nProduct: AI-powered CRM\nWebsite: https://acme.com\nValue Proposition: 10x faster lead qualification\n\n## Ideal Customer Profile\nIndustry: SaaS, FinTech\nCompany size: 50-500 employees\nJob titles: VP Sales, Head of Growth, CTO\nLocation: US, UK, Germany\n\n## Outreach Tone\nCasual but professional.\nReference their recent LinkedIn posts.\nKeep emails under 100 words.\n\n## Disqualifiers\nCompanies with fewer than 10 employees.\nAgencies and consultancies."
}
]
}
}
Preview: what the agent sees
Our Business
Company: Acme Corp
Product: AI-powered CRM
Website: acme.com
Ideal Customer Profile
Industry: SaaS, FinTech. Size: 50-500. Titles: VP Sales, CTO. Location: US, UK, Germany.
Outreach Tone
Casual but professional. Reference LinkedIn posts. Under 100 words.
Disqualifiers
Under 10 employees. Agencies and consultancies.
Skill key: ad-strategy — Controls target audience, budget goals, KPI thresholds, and ad creative preferences.
{
"guid": "your-google-ads-guid",
"configuration": {
"custom_skills": [
{
"key": "ad-strategy",
"value": "## Target Audience\nCountries: US, GB, DE\nLanguages: English\nAge range: 25-54\nInterests: Technology, AI, SaaS\n\n## Budget & Goals\nMonthly budget limit: $5,000\nTarget CPA: $25\nTarget ROAS: 4.0\n\n## Creative Preferences\nTone: Professional, benefit-focused\nAlways include pricing in ads\nUse numbers and statistics in headlines\n\n## Negative Keywords\nfree, cheap, tutorial, how to"
}
]
}
}
Preview: what the agent sees
Target Audience
Countries: US, GB, DE. Age: 25-54. Interests: Technology, AI, SaaS.
Budget & Goals
Monthly limit: $5,000. Target CPA: $25. Target ROAS: 4.0.
Creative Preferences
Professional, benefit-focused. Include pricing. Use numbers in headlines.
Negative Keywords
free, cheap, tutorial, how to
Skill key: ad-strategy — Controls target audience, budget goals, KPI thresholds, and creative preferences for Meta campaigns.
{
"guid": "your-meta-ads-guid",
"configuration": {
"custom_skills": [
{
"key": "ad-strategy",
"value": "## Target Audience\nCountries: US, GB, DE\nLanguages: English\nAge range: 25-54\n\n## Budget & Goals\nMonthly budget limit: $5,000\nTarget CPA: $20\n\n## Creative Preferences\nVisual style: Clean, minimal, product-focused\nUse carousel ads for feature showcases\nVideo ads max 15 seconds\n\n## Campaign Types\nConversions: Main budget (70%)\nRetargeting: Website visitors (20%)\nBrand awareness: Lookalike audiences (10%)"
}
]
}
}
Preview: what the agent sees
Target Audience
Countries: US, GB, DE. Age: 25-54.
Budget & Goals
Monthly limit: $5,000. Target CPA: $20.
Creative Preferences
Clean, minimal, product-focused. Carousel for features. Video max 15s.
Campaign Types
Conversions: 70%. Retargeting: 20%. Brand awareness: 10%.
Managing Scheduled Tasks
Scheduled tasks (_editable: false, non-null interval) run automatically on a cron schedule.
Only
enabledandintervalare writable for cron skills (keys prefixed withcron-).valueanddescriptionon a cron skill are dropped bymergeUserConfig— the task body is template-controlled and re-materialised on every container restart from the instance JSON. To change what a scheduled task does, edit the paired preference skill (cs-<slug>,_editable: true) that the cron reads at runtime.
Example: Change scanner frequency
{
"guid": "your-useragent-guid",
"configuration": {
"custom_skills": [
{
"key": "cron-review-scanner",
"enabled": true,
"interval": "0 */4 * * *"
},
{
"key": "cron-content-scanner",
"enabled": false
}
]
}
}
This changes cron-review-scanner to run every 4 hours and disables cron-content-scanner entirely.
Common Cron Expressions
| Expression | Meaning |
|---|---|
*/30 * * * * |
Every 30 minutes |
0 * * * * |
Every hour |
0 */2 * * * |
Every 2 hours |
0 */4 * * * |
Every 4 hours |
0 */6 * * * |
Every 6 hours |
0 9 * * * |
Daily at 9:00 AM UTC |
0 10 * * * |
Daily at 10:00 AM UTC |
0 9 * * 1 |
Every Monday at 9:00 AM UTC |
0 10 * * 3 |
Every Wednesday at 10:00 AM UTC |
Full Example: Push Notification Manager
Complete flow — fetch skills, then update preferences and schedules in one request.
Step 1 — Discover skills. Call POST /UserAgent/Detail with the agent instance GUID:
{
"guid": "your-push-agent-guid"
}
Response excerpt (configuration.custom_skills):
[
{
"key": "push-preferences",
"value": "## Push Tone\nWrite like a mobile growth expert...",
"description": "Push notification style, language, and targeting preferences",
"enabled": true,
"interval": null,
"_editable": true
},
{
"key": "cron-push-scanner",
"value": "",
"description": "Scan holidays and craft push notification suggestions",
"enabled": true,
"interval": "0 9 * * *",
"_editable": false
},
{
"key": "cron-push-dispatcher",
"value": "",
"description": "Send queued push notifications on schedule",
"enabled": true,
"interval": "0 * * * *",
"_editable": false
}
]
Step 2 — Update everything in one request:
{
"guid": "your-push-agent-guid",
"configuration": {
"custom_skills": [
{
"key": "push-preferences",
"value": "## Push Tone\nFriendly and casual. Turkish for locale_tr, English for locale_en.\n\n## Holiday Preferences\nFocus on: New Year, Ramadan, Republic Day.\nSkip: Valentine's Day, Halloween.\n\n## Targeting\nAlways segment by locale. Premium version for paid users."
},
{
"key": "cron-push-scanner",
"enabled": true,
"interval": "0 9 * * 1"
},
{
"key": "cron-push-dispatcher",
"interval": "0 */2 * * *"
}
]
}
}
This single request:
- push-preferences — rewrites targeting rules (editable skill,
valueupdated) - push-scanner — changes from daily to Mondays only (
intervalupdated) - push-dispatcher — changes from hourly to every 2 hours (
intervalupdated)
Available Skills by Agent
Preferences (Editable Instructions)
Every agent has exactly one editable preference skill that controls its behavior:
| Agent | Skill Key | What It Controls |
|---|---|---|
| Social Manager | content-tone |
Brand voice, hashtags, posting style per platform |
| Blog Content Editor | content-strategy |
Writing style, topics, research rules, blog types |
| App Review Support | review-preferences |
Response tone, support channels, custom rules |
| App Event Manager | event-preferences |
Event regions, holiday priorities, style |
| Push Notification | push-preferences |
Push tone, language, targeting, holiday choices |
| Newsletter Manager | newsletter-strategy |
Topics, tone, audience, frequency, branding |
| Lead Generation Manager | lead-strategy |
ICP definition, outreach tone, scoring rules |
| Google Ads Manager | ad-strategy |
Target audience, budget goals, KPI thresholds |
| Meta Ads Manager | ad-strategy |
Target audience, budget goals, creative preferences |
Scheduled Tasks
Automated tasks that run on a cron schedule. Toggle enabled and adjust interval:
| Agent | Task Key | Description | Default Schedule |
|---|---|---|---|
| Social Manager | cron-content-scanner |
Content discovery + draft generation (reads cs-content-tone) |
Every 4 hours |
| Social Manager | cron-gmail-checker |
Check inbox for content requests (disabled by default) | Every 30 min |
| Social Manager | cron-drive-scanner |
Google Drive asset scanning (disabled by default) | Daily 10 AM |
| Blog Content | cron-blog-scanner |
Discover blog topics and write content (reads cs-content-strategy) |
Daily 9 AM |
| Blog Content | cron-gmail-checker |
Check inbox for blog topic requests | Every 30 min |
| App Review | cron-review-scanner |
Scan stores for new reviews (reads cs-review-preferences) |
Every 2 hours |
| App Event | cron-app-event-scanner |
Scan holidays, suggest App Store events (reads cs-event-preferences) |
Monday 9 AM |
| Push Notification | cron-push-scanner |
Scan holidays, craft push suggestions (reads cs-push-preferences) |
Daily 9 AM |
| Push Notification | cron-push-dispatcher |
Send queued push notifications | Hourly |
| Newsletter | cron-newsletter-sender |
Create and send scheduled newsletters (reads cs-newsletter-strategy) |
Monday 9 AM |
| Newsletter | cron-subscriber-scanner |
Daily subscriber list health check | Daily 10 AM |
| Lead Gen | cron-prospect-scanner |
Weekly prospect search and scoring (reads cs-lead-strategy) |
Monday 10 AM |
| Lead Gen | cron-outreach-reporter |
Daily outreach performance report | Daily 9 AM |
| Lead Gen | cron-reply-handler |
Check replies, analyze sentiment | Every 4 hours |
| Google Ads | cron-performance-reporter |
Daily performance report (reads cs-ad-strategy) |
Daily 9 AM |
| Google Ads | cron-competitor-scanner |
Weekly competitor analysis | Monday 10 AM |
| Google Ads | cron-holiday-ad-planner |
Scan holidays, suggest ad campaigns | Wednesday 10 AM |
| Google Ads | cron-drive-scanner |
Google Drive creative asset scanning (disabled by default) | Daily 10 AM |
| Meta Ads | cron-performance-reporter |
Daily performance report (reads cs-ad-strategy) |
Daily 9 AM |
| Meta Ads | cron-audience-scanner |
Weekly audience analysis | Monday 10 AM |
| Meta Ads | cron-holiday-ad-planner |
Scan holidays, suggest campaigns | Wednesday 10 AM |
| Meta Ads | cron-drive-scanner |
Google Drive creative asset scanning (disabled by default) | Daily 10 AM |
Update Rules
| Field | Editable Skills (_editable: true) |
System Skills (_editable: false) |
|---|---|---|
key |
Read-only (used for lookup) | Read-only |
enabled |
Can toggle on/off | Can toggle on/off |
interval |
Can change cron schedule | Can change cron schedule |
value |
Can rewrite instructions | Ignored (hidden in API response) |
description |
Can update description | Ignored |
_editable |
Read-only | Read-only |
- Include only the fields you want to change — omitted fields keep their current values
- New skills cannot be added — only existing skills (matched by
key) can be updated - Send empty string
""forintervalto clear the schedule (becomesnull) - You can update credentials and skills in the same
POST /UserAgent/Updaterequest
What happens when Wiro updates an agent template
Skills occasionally evolve on Wiro's side — new skills, renamed keys, improved cron instructions. Deployed instances are reconciled with the latest template without destroying your edits:
| Skill type | Behavior on template update |
|---|---|
Editable preference (_editable: true, interval: null) |
Your value is preserved. New placeholder fields appear only on fresh deploys, not on existing instances. |
Scheduled cron (_editable: false, interval set) |
The value is overwritten from the new template. Your interval and enabled flags are kept. |
| New skill added upstream | Added to your instance with default value, enabled, interval. |
| Skill removed upstream | Removed from your instance on the next reconciliation. |
This means your UserAgent/Update preference edits are durable across template upgrades, while Wiro can push improvements to the scanning/reporting workflows without you having to re-deploy.
Meta Ads Integration
Connect your agent to Meta's advertising platform to manage campaigns, ad sets, creatives, and performance insights across Facebook and Instagram ads.
Overview
The Meta Ads integration powers the metaads-manage skill — creating and managing campaigns, pulling insights, managing creatives, and analyzing ad account data via the Meta Marketing API.
Skills that use this integration:
metaads-manage— Campaign / ad set / creative CRUD, insights reporting, Marketing API operationsads-manager-common— Shared ads helpers (works alongsidemetaads-manageandgoogleads-manage)
Agents that typically enable this integration:
- Meta Ads Manager
- Any custom agent that needs paid-media capabilities on Meta
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" |
Coming soon | Wiro's shared Meta App is under review by Meta. |
"own" |
Available now | You create your own Meta Developer App and connect it to Wiro. No App Review required when Development Mode + App Roles is used. |
Why own mode only right now? Meta's approval process for multi-tenant apps that request ads_management is long and strict. While our shared app is pending, you can skip the review bottleneck entirely by using your own Meta Developer App in Development Mode — App Review is not needed as long as every user who connects is listed under your app's Roles as Tester or Developer.
Prerequisites
- A Wiro API key — see Authentication for how to issue keys and sign requests.
- A deployed agent — see Agent Overview and call
POST /UserAgent/Deployfirst. You need the returneduseragents[0].guidfor every step below. - A Meta Business account — business.facebook.com.
- A Meta Developer account — developers.facebook.com.
- An HTTPS callback URL that your backend controls.
http://localhostandhttp://127.0.0.1are accepted for local development only.
Complete Integration Walkthrough
Every curl example and response shape below matches Wiro's production behavior verified against the source code. Nothing is invented.
Step 1: Create a Meta Developer App
- Go to developers.facebook.com/apps and click Create app.
- Choose "Other" as the use case, then "Business" as the app type.
- Set an App display name (this is what your end users see on the consent screen — use your company or product name, not "Wiro").
- Enter an App contact email.
- Select the Meta Business Account that owns the ad accounts you plan to manage, then click Create app.
You're now on the app dashboard. The app is in Development Mode by default — leave it there. Development Mode is exactly what lets you skip App Review.
Step 2: Add the Marketing API product
- From the app dashboard, click Add product.
- Find "Marketing API" and click Set up.
- No further configuration is required inside Marketing API itself — adding the product unlocks the
ads_*permissions.
Step 3: Add "Facebook Login for Business" and register the redirect URI
Meta Ads OAuth uses Facebook Login under the hood.
- Click Add product again.
- Find "Facebook Login for Business" and click Set up.
- Left sidebar: Facebook Login for Business → Settings.
-
Scroll to Valid OAuth Redirect URIs and add exactly:
https://api.wiro.ai/v1/UserAgentOAuth/MetaAdsCallback - Save changes at the bottom.
This is the single most common place where own-mode setups fail. The redirect URI must be exact — HTTPS, no trailing slash, same capitalization.
Step 4: Note the required permissions
Wiro requests these exact scopes during OAuth (verified against api-useragent-oauth.js L2204–L2208):
ads_management,ads_read,business_management,pages_show_list,pages_read_engagement
| Permission | Why Wiro requests it |
|---|---|
ads_management |
Create, update, and pause campaigns, ad sets, and ads. |
ads_read |
Read insights, performance metrics, and account metadata. |
business_management |
Enumerate ad accounts and pages the user has access to under their Business Manager. |
pages_show_list |
Resolve the first administered Facebook Page so its ID can be attached to ad creatives (see How Meta Ads uses pageId).
|
pages_read_engagement |
Read page-level engagement metrics that some creative types reference. |
These permissions normally require App Review for Live Mode. In Development Mode they work without App Review for any Facebook user listed under your app's Roles. You don't need to request Advanced Access.
Step 5: Copy your App ID and App Secret
App settings → Basic → copy the App ID, click Show next to App Secret and copy that too.
Step 6: Add the users who will connect as Testers (only if they're not you)
- Connecting your own Facebook account? You're already the app Admin; skip this step.
- Connecting a different Facebook account (typical for SaaS customers)? Go to App Roles → Roles → Add People and invite them as Testers or Developers. They accept at facebook.com/settings → Business Integrations.
Users not listed in App Roles will be blocked at the consent screen in Development Mode.
Step 7: Save your Meta App credentials to Wiro
Push the appId and appSecret into the agent's metaads credential group. Wiro merges credential updates per group — fields you don't send are preserved, and credentials from other groups are untouched.
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"metaads": {
"appId": "YOUR_META_APP_ID",
"appSecret": "YOUR_META_APP_SECRET"
}
}
}
}'
Successful response (sanitized — OAuth tokens, if any, are stripped):
{
"result": true,
"useragents": [
{
"guid": "your-useragent-guid",
"setuprequired": true,
"status": 0
}
],
"errors": []
}
Prepaid deploy users: If you deployed your agent with useprepaid: true, the credentials you passed in the Deploy body were not saved (prepaid deploy writes only a template placeholder). You must call this Update step explicitly before initiating OAuth.
Only _editable: true fields are accepted. appId and appSecret are editable by default in the metaads template. Attempts to set non-editable fields are silently ignored. Call POST /UserAgent/Detail and inspect configuration.credentials.metaads._editable if you see a silent no-op.
Step 8: Initiate OAuth
Start the flow. Include authMethod: "own" so Wiro uses your appId/appSecret instead of its own shared app.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MetaAdsConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations",
"authMethod": "own"
}'
Response:
{
"result": true,
"authorizeUrl": "https://www.facebook.com/v25.0/dialog/oauth?client_id=...&redirect_uri=https%3A%2F%2Fapi.wiro.ai%2Fv1%2FUserAgentOAuth%2FMetaAdsCallback&state=...&scope=ads_management%2Cads_read%2Cbusiness_management%2Cpages_show_list%2Cpages_read_engagement",
"errors": []
}
Redirect the user's browser to authorizeUrl. Full-page redirect is recommended over a popup — some browsers block third-party cookies in popups, breaking the OAuth session.
State TTL: Wiro caches the OAuth state for 15 minutes. If the user takes longer to complete consent, the callback returns metaads_error=session_expired and you must restart from this step.
Step 9: Handle the callback
After the user consents, Meta sends them back to Wiro's callback URL. Wiro exchanges the code for a long-lived token, fetches the user's active ad accounts, writes the access token into the agent's config, and redirects the user to your redirectUrl with query parameters.
Success URL looks like:
https://your-app.com/settings/integrations?metaads_connected=true&metaads_accounts=%5B%7B%22id%22%3A%22123456789%22%2C%22name%22%3A%22My%20Ad%20Account%22%7D%5D
metaads_accountsisencodeURIComponent(JSON.stringify([...])).- Each array element:
{ id, name }. - The
idhas theact_prefix stripped by Wiro. - Only ad accounts with
account_status === 1(active) are included.
Parse in the browser:
const params = new URLSearchParams(window.location.search);
if (params.get("metaads_connected") === "true") {
const accounts = JSON.parse(decodeURIComponent(params.get("metaads_accounts") || "[]"));
if (accounts.length === 0) {
showError("No active ad accounts found on this Meta user.");
} else if (accounts.length === 1) {
await setAdAccount(accounts[0]);
} else {
presentAccountPicker(accounts);
}
} else if (params.get("metaads_error")) {
handleError(params.get("metaads_error"));
}
Step 10: Persist the ad account selection
Wiro doesn't automatically pick an ad account — you must tell it which one to use. This is required for the agent to function.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MetaAdsSetAdAccount" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"adAccountId": "123456789",
"adAccountName": "My Ad Account"
}'
Response:
{
"result": true,
"errors": []
}
Behavior:
- Pass the ad account ID without the
act_prefix. If you include it, Wiro strips it automatically. adAccountNameis optional but recommended — it surfaces inStatusresponses and dashboards asusername.- If the agent was running (status
3or4), Wiro marks itstatus: 1withrestartafter: trueso the daemon picks up the new ad account after the next stop cycle. No manual Start needed.
Step 11: Verify the connection
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MetaAdsStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
Response:
{
"result": true,
"connected": true,
"username": "My Ad Account",
"connectedAt": "2026-04-17T12:00:00.000Z",
"tokenExpiresAt": "2026-06-16T12:00:00.000Z",
"errors": []
}
Field notes:
connected: truerequires both anaccessTokenandauthMethod("wiro"or"own").username= the savedadAccountName(empty string if you didn't pass one in Step 10).tokenExpiresAt= 60 days from the connect moment (Meta's long-lived user token lifetime).-
No
refreshTokenExpiresAt— Meta user tokens don't have refresh tokens; renewal usesfb_exchange_tokenwith the long-lived token itself.
Step 12: Start the agent if it's not running
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Check POST /UserAgent/Detail first: if setuprequired is still true, some other credential the agent requires is missing — Start will refuse. See Agent Credentials — Setup Required.
Agents already running when you connected Meta Ads restart automatically.
How Meta Ads uses pageId
During Step 9 (the callback), Wiro silently calls Meta's Graph API GET /me/accounts?fields=id,name&limit=5 and stores the first returned page ID as credentials.metaads.pageId. This is not the Facebook Page integration — it's a Marketing-API-only field used internally by the metaads-manage skill when creating creatives that need object_story_spec.page_id (for example, when boosting a
page post).
If your agent needs a specific page for creatives and the user administers multiple pages, the first one won't always be what they want. In that case, update metaads.pageId manually via POST /UserAgent/Update after the OAuth flow completes.
If you need organic posting (writing posts directly to a Facebook Page rather than running ads), that's a separate integration — see the Facebook Page integration. The facebookpage-post skill lives under the facebook credential group, not metaads.
API Reference
All endpoints require Wiro authentication — see Authentication for x-api-key + optional signature headers.
POST /UserAgentOAuth/MetaAdsConnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
redirectUrl |
string | Yes | HTTPS URL (or http://localhost / http://127.0.0.1 for dev) where users return after consent. |
authMethod |
string | No | "wiro" (default) or "own". Use "own" while the shared app is pending. |
Response: { result, authorizeUrl, errors }. If result: false, inspect errors[0].message — common messages: Missing userAgentGuid, Missing redirectUrl, Invalid redirect URL, User agent not found or unauthorized, Meta Ads credentials not configured (own mode without prior Update).
GET /UserAgentOAuth/MetaAdsCallback
Server-side endpoint invoked by Meta. You don't call it — you only handle the final redirect back to your redirectUrl:
| Query param | Meaning |
|---|---|
metaads_connected=true |
OAuth completed successfully. |
metaads_accounts |
URL-encoded JSON array of { id, name } for active ad accounts. |
metaads_error=<code> |
OAuth failed. See Troubleshooting. |
POST /UserAgentOAuth/MetaAdsSetAdAccount
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
adAccountId |
string | Yes | Ad account ID without the act_ prefix (prefix stripped automatically if sent). |
adAccountName |
string | No | Display name shown in dashboards and Status responses. |
Response: { result: true, errors: [] } on success. Triggers an automatic agent restart if the agent was running.
POST /UserAgentOAuth/MetaAdsStatus
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
Response fields:
| Field | Type | Description |
|---|---|---|
connected |
boolean |
true when accessToken is set and authMethod is "wiro" or "own". Note: this reflects OAuth completion, not full setup readiness — adAccountId is not required for connected: true. Use setupcomplete (from POST /UserAgent/Detail) or check configuration.credentials.metaads.adAccountId for end-to-end readiness
to create campaigns.
|
username |
string | The saved adAccountName. |
connectedAt |
string | ISO timestamp of connection. |
tokenExpiresAt |
string | ISO timestamp (~60 days from connection). |
POST /UserAgentOAuth/MetaAdsDisconnect
Clears Meta Ads credentials (no remote revoke — Facebook's Graph API doesn't have a straightforward revoke for long-lived tokens).
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MetaAdsDisconnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
Response: { "result": true, "errors": [] }. Running agents restart automatically.
POST /UserAgentOAuth/TokenRefresh
You don't normally need to call this. Running agents refresh their Meta Ads token automatically via fb_exchange_token on a daily maintenance cron. Use this endpoint only for debugging or forcing a new token immediately.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TokenRefresh" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"provider": "metaads"
}'
Response: { result: true, accessToken: "...", refreshToken: "", errors: [] }. See Automatic token refresh for the full cron schedule.
Using the Skill
Enable metaads-manage on the agent — see Agent Skills. Adjust the cron of the built-in cron-performance-reporter task (Meta Ads Manager) with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for _editable: false skills:
{
"guid": "your-useragent-guid",
"configuration": {
"custom_skills": [
{
"key": "cron-performance-reporter",
"enabled": true,
"interval": "0 9 * * *"
}
]
}
}
To change what the reporter includes (thresholds, reporting preferences, holiday markets), edit the paired preference skill ad-strategy instead — see Agent Skills → Updating Preferences.
Troubleshooting
| Error code | Meaning | What to do |
|---|---|---|
missing_params |
Callback was hit without state or code. |
Don't hit the callback URL directly. Start a new flow from Step 8. |
session_expired |
More than 15 minutes elapsed between MetaAdsConnect and the consent return. |
Call MetaAdsConnect again to refresh the state. |
authorization_denied |
User clicked Cancel, or Facebook returned error=access_denied. In Development Mode this also happens when the user isn't listed under App Roles. |
Add the user as a Tester (Step 6), have them accept, retry. |
token_exchange_failed |
Facebook rejected the token exchange. Usually wrong App Secret, revoked app, or redirect URI mismatch. | Re-copy the App Secret from Settings → Basic, verify the redirect URI exactly matches, retry. |
useragent_not_found |
Wrong userAgentGuid or agent doesn't belong to your API key's user. |
Fetch the correct guid with POST /UserAgent/MyAgents. |
invalid_config |
The agent has no credentials.metaads block at all. |
Call POST /UserAgent/Update to add metaads.appId and metaads.appSecret, then retry MetaAdsConnect. |
internal_error |
Unexpected server error during callback processing. | Retry once. If it persists, contact Wiro support with the timestamp and your userAgentGuid. |
"App not verified" warning on consent
Facebook shows a yellow banner in Development Mode. This is expected and not a blocker — users listed under App Roles can click Continue and finish authorization. Users outside App Roles are hard-blocked.
connected: false after completing OAuth
Status returns connected: true only when both an access token is saved and an authMethod is set. If you skipped Step 10 (MetaAdsSetAdAccount), the ad account is empty but connected is still true as long as the token is present. If you see connected: false, the token didn't save — check for error parameters in the callback URL or retry OAuth.
Token keeps expiring
Long-lived Meta tokens last ~60 days. The agent's daily maintenance cron refreshes them automatically via fb_exchange_token. If you see tokenExpiresAt in the past and the agent is running, the refresh cron either failed or hasn't run yet — check agent logs. If you can't wait, force a refresh manually with POST /UserAgentOAuth/TokenRefresh. If that also fails (typically a 190-series Graph error), the user must redo OAuth from Step 8.
Multi-Tenant Architecture
For SaaS products connecting many customers' Meta Ads accounts through a single Wiro-powered backend:
- One Meta Developer App per product, not per customer. The same
appId/appSecretpair serves unlimited customers. - One Wiro agent instance per customer. Call
POST /UserAgent/Deployduring onboarding and follow Steps 7–11 per customer'suserAgentGuid. - Tokens are isolated per agent instance. Customer A's Meta token is never visible to Customer B — they live under different
useragentguidvalues. - Your consent screen carries your branding. Users see your app display name, not "Wiro". Keep the display name clean and trustworthy; Meta occasionally flags apps with generic names.
- Add each customer to App Roles → Testers until you go Live Mode. Collect their Facebook user ID during onboarding and add them via the Meta Business API or Roles UI.
- Rate limits are per app, not per customer. The Marketing API tier (Development → Standard → Advanced) governs aggregate call volume. See Meta's Rate Limiting docs.
Related
- Agent Credentials & OAuth — integration catalog hub and generic OAuth reference.
- Agent Overview — deploying, starting, and lifecycle.
- Agent Skills — configuring
metaads-manageand scheduled runs. - Google Ads integration — for cross-platform paid campaigns.
- Meta for Developers — Marketing API
Facebook Page Integration
Connect your agent to a Facebook Page to publish posts, photos, and videos on the user's behalf.
Overview
The Facebook Page integration uses Meta's Graph API with a page-scoped access token. The connecting Facebook user must be an admin of at least one Page, and the connection is not complete until a specific Page is selected.
Skills that use this integration:
facebookpage-post— Publish text, photo, and video posts to a Facebook Page
Agents that typically enable this integration:
- Social Manager
- Any custom agent that needs Facebook Page posting
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" |
Coming soon | Wiro's shared Meta App is under review by Meta. |
"own" |
Available now | Use your own Meta Developer App in Development Mode — no App Review required. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview; keep the
useragents[0].guid. - A Meta Business account — business.facebook.com.
- A Meta Developer account — developers.facebook.com.
- At least one Facebook Page where the end user is an admin — OAuth enumerates every admin-managed Page.
- An HTTPS callback URL for your backend.
Complete Integration Walkthrough
All scopes, endpoints, and callback parameters are verified against the backend source code.
Step 1: Create a Meta Developer App
You can reuse a single Meta App for Facebook Page, Instagram, and Meta Ads.
- developers.facebook.com/apps → Create app → Other → Business.
- Enter an App display name (what users see on consent screens), App contact email, select your Business Account, then Create app.
- Leave it in Development Mode.
Step 2: Add "Facebook Login for Business" and register the redirect URI
- Add product → Facebook Login for Business → Set up.
- Facebook Login for Business → Settings.
-
Under Valid OAuth Redirect URIs, add:
https://api.wiro.ai/v1/UserAgentOAuth/FBCallback - Save changes.
Step 3: Note the required permissions
Wiro requests these exact scopes (verified against api-useragent-oauth.js L1099):
pages_show_list,pages_manage_posts,pages_read_engagement,pages_read_user_content,pages_manage_metadata,pages_messaging
| Permission | Why |
|---|---|
pages_show_list |
Enumerate the Pages the user administers. |
pages_manage_posts |
Publish posts, photos, and videos to a Page. |
pages_read_engagement |
Read likes, comments, and shares on the Page's posts. |
pages_read_user_content |
Read user-generated content on the Page (for context). |
pages_manage_metadata |
Webhook subscriptions and Page metadata. |
pages_messaging |
Send and receive messages on behalf of the Page (some skills use this). |
These work without App Review in Development Mode for any Facebook user in App Roles.
Step 4: Copy your App ID and App Secret
App settings → Basic → copy App ID and App Secret.
Step 5: Add other Facebook accounts as Testers (only if needed)
Connecting your own Facebook account? You're the app Admin — skip. Connecting a customer's account? Add them under App Roles → Roles → Add People → Testers. They accept at facebook.com/settings → Business Integrations.
Step 6: Save your Meta App credentials to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"facebook": {
"appId": "YOUR_META_APP_ID",
"appSecret": "YOUR_META_APP_SECRET"
}
}
}
}'
Wiro merges this into only the facebook group — other credentials are untouched.
Step 7: Initiate OAuth
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/FBConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations",
"authMethod": "own"
}'
Response:
{
"result": true,
"authorizeUrl": "https://www.facebook.com/v25.0/dialog/oauth?client_id=...&redirect_uri=...&scope=pages_show_list%2Cpages_manage_posts%2Cpages_read_engagement%2Cpages_read_user_content%2Cpages_manage_metadata%2Cpages_messaging&auth_type=rerequest&response_type=code&state=...",
"errors": []
}
Redirect the user's browser to authorizeUrl. State has a 15-minute TTL.
Step 8: Handle the callback and list returned Pages
After consent, Wiro exchanges the code for a user access token, fetches every admin-managed Page with its page-specific access token, caches the full list server-side, and redirects the user to your redirectUrl.
Crucial: Wiro does not auto-select a Page. The connection is incomplete until the client calls FBSetPage with a chosen pageId. POST /UserAgentOAuth/FBStatus returns connected: false during this window.
Success URL:
https://your-app.com/settings/integrations?fb_connected=true&fb_pages=%5B%7B%22id%22%3A%22123%22%2C%22name%22%3A%22Page%20A%22%7D%2C%7B%22id%22%3A%22456%22%2C%22name%22%3A%22Page%20B%22%7D%5D
Query parameters:
| Param | Meaning |
|---|---|
fb_connected=true |
OAuth completed; credentials are cached server-side awaiting page selection. |
fb_pages |
URL-encoded JSON array [{ id, name }, ...] of every admin-managed Page. The per-page access tokens stay server-side — the client only receives ID and name. |
fb_error=<code> |
Failure. See Troubleshooting. |
Parse:
const params = new URLSearchParams(window.location.search);
if (params.get("fb_connected") === "true") {
const pages = JSON.parse(decodeURIComponent(params.get("fb_pages") || "[]"));
if (pages.length === 0) {
// Shouldn't normally happen — the callback returns fb_error=no_pages if the user
// has no Pages. But handle defensively.
showError("No Facebook Pages to manage.");
} else if (pages.length === 1) {
// One-page case: still required to confirm via FBSetPage
await setPage(pages[0]);
} else {
presentPagePicker(pages);
}
} else if (params.get("fb_error")) {
handleError(params.get("fb_error"));
}
Step 9: Persist the page selection (required)
This step is mandatory. The agent has no valid Facebook credentials until you call FBSetPage. Until then, credentials.facebook.accessToken and pageId remain empty, and FBStatus reports connected: false.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/FBSetPage" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"pageId": "456",
"pageName": "Page B"
}'
Response:
{
"result": true,
"pageId": "456",
"pageName": "Page B",
"errors": []
}
What happens server-side:
- Wiro looks up the pending payload in cache (keyed by
userAgentGuid, 15-minute TTL). - Finds the page matching
pageIdin the cached list. - Writes the page access token (not the user token) to
credentials.facebook.accessTokenalong withpageId,fbPageName,authMethod,connectedAt, andtokenExpiresAt(~60 days). - Triggers an agent restart if it was running.
- Clears the pending cache.
If the 15-minute window lapses before you call FBSetPage, you'll get No pending Facebook connection. Please reconnect via FBConnect. — start again from Step 7.
pageName is optional; if omitted, Wiro uses the name from the cached page list.
Step 10: Verify the connection
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/FBStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
Response:
{
"result": true,
"connected": true,
"username": "Page B",
"connectedAt": "2026-04-17T12:00:00.000Z",
"tokenExpiresAt": "2026-06-16T12:00:00.000Z",
"errors": []
}
connected: truerequires bothaccessTokenandpageIdto be set — meaningFBSetPagewas called successfully.username= the savedfbPageName.- No
refreshTokenExpiresAt— Facebook page tokens are long-lived (~60 days) and refresh withfb_exchange_token, not a refresh token flow.
Step 11: Start the agent if it's not running
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Agents already running at FBSetPage time restart automatically to pick up the new credentials.
API Reference
POST /UserAgentOAuth/FBConnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
redirectUrl |
string | Yes | HTTPS URL (or http://localhost / http://127.0.0.1 for dev). |
authMethod |
string | No | "wiro" (default) or "own". |
Response: { result, authorizeUrl, errors }.
GET /UserAgentOAuth/FBCallback
Server-side. Query params appended to your redirectUrl:
| Param | Meaning |
|---|---|
fb_connected=true |
OAuth completed; pending payload cached awaiting FBSetPage. |
fb_pages |
URL-encoded JSON [{id, name}, ...] of admin-managed Pages. |
fb_error=<code> |
Failure. |
POST /UserAgentOAuth/FBSetPage
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
pageId |
string | Yes | A page ID from the fb_pages array returned in the callback. |
pageName |
string | No | Override the display name. If omitted, Wiro uses the cached name. |
Response: { result, pageId, pageName, errors }. Triggers auto-restart if running.
Must be called within 15 minutes of the callback (cache TTL). After that, the pending payload is gone and you'll need to restart OAuth.
POST /UserAgentOAuth/FBStatus
Response fields: connected (only true when both accessToken and pageId are set), username (= fbPageName), connectedAt, tokenExpiresAt.
POST /UserAgentOAuth/FBDisconnect
Clears Facebook credentials (no remote revoke).
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/FBDisconnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
POST /UserAgentOAuth/TokenRefresh
Running agents refresh the Facebook page token automatically via the daily maintenance cron. Use this only for debugging or manual overrides.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TokenRefresh" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"provider": "facebook"
}'
Uses fb_exchange_token under the hood. See Automatic token refresh.
Using the Skill
Once the Facebook Page is connected (page selected via FBSetPage), the agent's scheduled tasks use the facebookpage-post platform skill to publish text, photo, and video posts to the Page. To adjust the cron of the built-in cron-content-scanner task (Social Manager), send an Update with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for
_editable: false skills:
{
"guid": "your-useragent-guid",
"configuration": {
"custom_skills": [
{
"key": "cron-content-scanner",
"enabled": true,
"interval": "0 */4 * * *"
}
]
}
}
To change what the scheduled task posts (topics, tone, content angle), edit the paired preference skill content-tone instead — see Agent Skills → Updating Preferences.
Troubleshooting
| Error code | Meaning | What to do |
|---|---|---|
missing_params |
Callback hit without state or code. |
Start a new flow from Step 7. |
session_expired |
>15 min between FBConnect and the callback. |
Call FBConnect again. |
authorization_denied |
User cancelled, or not listed in App Roles (Development Mode). | Add as Tester (Step 5), retry. |
token_exchange_failed |
Wrong App Secret or redirect URI mismatch. | Re-copy App Secret; verify redirect URI exactly. |
no_pages |
User has no administered Facebook Pages. | Ask the user to create/administer a Page first, retry. |
useragent_not_found |
Invalid or unauthorized userAgentGuid. |
Use POST /UserAgent/MyAgents. |
invalid_config |
Agent has no credentials.facebook block. |
Call POST /UserAgent/Update with facebook.appId and facebook.appSecret, retry. |
internal_error |
Unexpected server error (includes cache write failures). | Retry once. If persistent, contact support. |
FBSetPage returns "No pending Facebook connection"
The 15-minute pending cache expired, or you passed a pageId that wasn't in the fb_pages list. Start a new OAuth flow from Step 7.
FBSetPage returns "Selected pageId not found in pending pages list"
The pageId you sent doesn't match any ID in the cached list. Verify you're parsing fb_pages correctly and sending the exact ID string.
Posts publish but as the wrong author
Check POST /UserAgent/Detail and verify configuration.credentials.facebook.pageId is the Page you intended. If not, disconnect and reconnect, or call FBSetPage again within a fresh 15-minute window.
"App not verified" banner on consent
Expected in Development Mode. Users in App Roles can click Continue.
Multi-Tenant Architecture
- One Meta Developer App per product — same
appId/appSecretfor all customers. - One Wiro agent instance per customer.
- Each customer's page token is isolated per
useragentguid. Customer A's token never leaks to Customer B. - Your app display name shows on every consent screen — not "Wiro".
- Each customer must be in App Roles until you go Live Mode. Capture their Facebook user ID during onboarding.
- Page admin rights must be current. Meta returns only Pages the user is currently an admin of. If a customer loses admin access later, the agent will start failing — build a revalidation loop that periodically checks
FBStatus.
Related
- Agent Credentials & OAuth
- Agent Overview
- Agent Skills
- Meta Ads integration — separate product; used for paid media, not organic posting.
- Instagram integration
- Meta for Developers — Pages API
Instagram Integration
Connect your agent to an Instagram Business Account to publish feed posts, carousels, reels, and stories.
Overview
The Instagram integration uses Meta's Graph API against an Instagram Business (or Creator) account that's linked to a Facebook Page.
Skills that use this integration:
instagram-post— Publish feed carousels, reels, and stories
Agents that typically enable this integration:
- Social Manager
- Any custom agent that needs Instagram publishing
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" |
Coming soon | Wiro's shared Meta App is under review. |
"own" |
Available now | Use your own Meta Developer App in Development Mode — no App Review required. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview; keep the
useragents[0].guid. - A Meta Business account — business.facebook.com.
- A Meta Developer account — developers.facebook.com.
- An Instagram Business or Creator account — personal Instagram accounts cannot be connected via the Graph API.
- An HTTPS callback URL for your backend.
Preparing the Instagram account
Before the user can complete OAuth:
- In the Instagram mobile app: Settings → Account → Switch to Professional Account → pick Business or Creator.
- In Meta Business Suite: select the Facebook Page that should own the Instagram account → Settings → Linked accounts → Instagram → Connect account. Sign in with the Instagram account and grant manage permissions.
Without both steps, the Graph API won't return the Instagram account during OAuth.
Complete Integration Walkthrough
Step 1: Create a Meta Developer App
If you already have one for Meta Ads or Facebook Page, reuse it.
- developers.facebook.com/apps → Create app → Other → Business.
- App display name, contact email, Business Account → Create app.
- Leave in Development Mode.
Step 2: Add the "Instagram" product
- From the app dashboard, Add product.
- Find "Instagram" (not "Instagram Basic Display" — that's for personal accounts and is being deprecated).
- Set up.
Step 3: Configure the OAuth redirect URI
- Left sidebar: Instagram → API setup with Instagram login.
- Scroll to Business login settings → OAuth settings.
-
Add to Valid OAuth Redirect URIs:
https://api.wiro.ai/v1/UserAgentOAuth/IGCallback - Save changes.
Note: Instagram OAuth has its own authorize URL at instagram.com/oauth/authorize (not facebook.com/…), but the redirect URI is still registered inside the Meta Developer App.
Step 4: Note the required permissions
Wiro requests these exact scopes (verified against api-useragent-oauth.js L805-L806):
instagram_business_basic,instagram_business_content_publish,instagram_business_manage_messages,instagram_business_manage_comments,instagram_business_manage_insights
| Permission | Why |
|---|---|
instagram_business_basic |
Basic account info, profile data. |
instagram_business_content_publish |
Publish feed, carousel, reel, and story content. |
instagram_business_manage_messages |
Read and reply to DMs (used by some skills). |
instagram_business_manage_comments |
Read, reply, hide, delete comments. |
instagram_business_manage_insights |
Read engagement insights for posts and profile. |
These work without App Review in Development Mode for any Facebook user in App Roles. No pages_* scopes are requested — Instagram Login uses its own scope family.
Step 5: Copy your App ID and App Secret
App settings → Basic → copy App ID, click Show → copy App Secret.
Step 6: Add users as Testers (only if needed)
If the connecting person isn't the app Admin, add them under App Roles → Roles → Add People → Testers. The Facebook account they accept with must be the one linked to the Instagram Business Account via Meta Business Suite.
Step 7: Save your Meta App credentials to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"instagram": {
"appId": "YOUR_META_APP_ID",
"appSecret": "YOUR_META_APP_SECRET"
}
}
}
}'
Step 8: Initiate OAuth
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/IGConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations",
"authMethod": "own"
}'
Response:
{
"result": true,
"authorizeUrl": "https://www.instagram.com/oauth/authorize?client_id=...&redirect_uri=...&scope=instagram_business_basic%2Cinstagram_business_content_publish%2Cinstagram_business_manage_messages%2Cinstagram_business_manage_comments%2Cinstagram_business_manage_insights&response_type=code&state=...",
"errors": []
}
Step 9: Handle the callback
After consent, Wiro exchanges the code for a short-lived token, upgrades it to a long-lived token via graph.instagram.com/access_token?grant_type=ig_exchange_token, fetches the Instagram user info, and redirects the user back.
Success URL:
https://your-app.com/settings/integrations?ig_connected=true&ig_username=my_brand
Parse:
const params = new URLSearchParams(window.location.search);
if (params.get("ig_connected") === "true") {
const username = params.get("ig_username");
showSuccess(`Connected @${username}`);
} else if (params.get("ig_error")) {
handleError(params.get("ig_error"));
}
Instagram has no secondary selection step — the Business Account tied to the chosen Facebook Page is used directly.
Step 10: Verify the connection
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/IGStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
{
"result": true,
"connected": true,
"username": "my_brand",
"connectedAt": "2026-04-17T12:00:00.000Z",
"tokenExpiresAt": "2026-06-16T12:00:00.000Z",
"errors": []
}
connected: truerequires anaccessTokenandauthMethod("wiro"or"own").username= Instagram handle (without@).tokenExpiresAt= ~60 days from connection.-
No
refreshTokenExpiresAt— Instagram long-lived tokens don't use refresh tokens; they refresh viagrant_type=ig_refresh_token.
Step 11: Start the agent if it's not running
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
API Reference
POST /UserAgentOAuth/IGConnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
redirectUrl |
string | Yes | HTTPS URL (or localhost/127.0.0.1 for dev). |
authMethod |
string | No | "wiro" (default) or "own". |
GET /UserAgentOAuth/IGCallback
Server-side. Query params appended to your redirectUrl:
| Param | Meaning |
|---|---|
ig_connected=true |
OAuth succeeded. |
ig_username |
Connected Instagram handle (without @). |
ig_error=<code> |
Failure. |
POST /UserAgentOAuth/IGStatus
Response: connected, username, connectedAt, tokenExpiresAt.
POST /UserAgentOAuth/IGDisconnect
Clears Instagram credentials (no remote revoke).
POST /UserAgentOAuth/TokenRefresh
Running agents refresh the Instagram token automatically via the daily maintenance cron. Use this only for debugging or manual overrides.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TokenRefresh" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"provider": "instagram"
}'
Uses grant_type=ig_refresh_token with the current access token (Instagram has no separate refresh token). See Automatic token refresh.
Using the Skill
Once the Instagram Business account is connected, the agent's scheduled tasks use the instagram-post platform skill to publish feed carousels, reels, and stories. To adjust the cron of the built-in cron-content-scanner task (Social Manager), send an Update with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for _editable: false skills:
{
"guid": "your-useragent-guid",
"configuration": {
"custom_skills": [
{
"key": "cron-content-scanner",
"enabled": true,
"interval": "0 */4 * * *"
}
]
}
}
To change what the scheduled task posts (topics, tone, hashtag rules, caption style), edit the paired preference skill content-tone instead — see Agent Skills → Updating Preferences.
Troubleshooting
| Error code | Meaning | What to do |
|---|---|---|
missing_params |
Callback hit without state or code. |
Start a new flow from Step 8. |
session_expired |
>15 min between IGConnect and callback. |
Call IGConnect again. |
authorization_denied |
User cancelled, or not in App Roles (Development Mode). | Add as Tester (Step 6), retry. |
token_exchange_failed |
Wrong App Secret, redirect URI mismatch, or no linked Instagram Business Account. | Re-copy App Secret; verify redirect URI; verify IG Business → FB Page linkage. |
useragent_not_found |
Invalid or unauthorized guid. | Use POST /UserAgent/MyAgents. |
invalid_config |
No credentials.instagram block. |
POST /UserAgent/Update with instagram.appId and instagram.appSecret. |
internal_error |
Unexpected server error. | Retry. If persistent, contact support. |
"No Instagram Business Account found" during OAuth
Most common cause: the Instagram account is still in Personal mode, or not linked to a Facebook Page the user administers. Walk through the Preparing the Instagram account checklist.
Publishing fails with "media upload failed"
Common causes:
- Image resolution too low (<320px) or aspect ratio outside Instagram's allowed ranges.
- Videos exceeding allowed durations (feed: 60s, reel: 90s, story: 60s).
- Instagram account switched back to Personal after connection — the token becomes invalid. Ask the user to switch back to Business and reconnect.
Multi-Tenant Architecture
- One Meta Developer App per product, same for Facebook, Instagram, Meta Ads.
- One Wiro agent instance per customer.
- Each customer's Facebook user must be added to App Roles until you go Live Mode.
- Business Account linkage is strict. Build pre-flight validation during onboarding — the Graph API returns
instagram_business_accounton the Page object only when the linkage is set up. - Tokens are isolated per agent instance.
Related
- Agent Credentials & OAuth
- Agent Overview
- Agent Skills
- Facebook Page integration — the Facebook Page linkage is mandatory for Instagram.
- Meta Ads integration — for Instagram-placement paid ads.
- Meta for Developers — Instagram Graph API
LinkedIn Integration
Connect your agent to a LinkedIn Company Page to publish posts and engage with followers.
Overview
The LinkedIn integration uses the LinkedIn Marketing Developer Platform via OAuth 2.0. Agents publish posts on behalf of a Company Page using the connecting member's admin rights.
Skills that use this integration:
linkedin-post— Publish text, image, and video posts to a Company Page
Agents that typically enable this integration:
- Social Manager
- Any custom agent that needs LinkedIn Company Page publishing
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" |
Coming soon | LinkedIn partner app review pending. |
"own" |
Available now | Create your own LinkedIn Developer App. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A LinkedIn Company Page the connecting user is an admin of — personal profiles are not supported.
- The numeric LinkedIn organization ID (not the vanity slug). Find it in
linkedin.com/company/<ID>/admin/. - An HTTPS callback URL for your backend.
Complete Integration Walkthrough
Step 1: Create a LinkedIn Developer App
- linkedin.com/developers/apps → Create app.
-
Fill in:
- App name (shown on consent screen).
- LinkedIn Page (associate with a Company Page you own — this gives admins automatic development access).
- Privacy policy URL.
- App logo (128×128 PNG).
- Agree to Legal terms → Create app.
Step 2: Request the required products
Products tab. Request:
- Sign In with LinkedIn using OpenID Connect — for
openidandprofilescopes. - Community Management API — required for Company Page posting (
w_organization_social,r_organization_social).
Community Management API approval is a manual review that can take days. While pending, your app can still post to the Company Page it's associated with for admins listed on that page — this is enough for development and testing.
Step 3: Configure the OAuth redirect URI
- Auth tab.
-
OAuth 2.0 settings → Authorized redirect URLs for your app → add:
https://api.wiro.ai/v1/UserAgentOAuth/LICallback - Save.
Step 4: Note the required OAuth 2.0 scopes
Wiro requests these exact scopes (verified against api-useragent-oauth.js L1484):
openid profile w_organization_social r_organization_social
| Scope | Why |
|---|---|
openid |
OpenID Connect basic identity. |
profile |
Member's display name and headline (shown on consent). |
w_organization_social |
Post, comment, and reply on behalf of the Company Page. |
r_organization_social |
Read Company Page posts and engagement. |
Wiro does not request email, w_member_social, or rw_organization_admin. Keep your app's scope list limited to the four above for consistency with the Wiro flow.
Enable all four in Auth → OAuth 2.0 scopes. Scopes not enabled in this list will fail at the consent screen.
Step 5: Copy your Client ID and Client Secret
Auth → Application credentials → copy Client ID. Copy the Primary Client Secret — it's shown in plain text here. Store it like a password.
Step 6: Save credentials to Wiro
LinkedIn requires clientId, clientSecret, and organizationId all in the same credential block.
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"linkedin": {
"clientId": "YOUR_LINKEDIN_CLIENT_ID",
"clientSecret": "YOUR_LINKEDIN_CLIENT_SECRET",
"organizationId": "12345678"
}
}
}
}'
organizationId is the numeric ID from your Company Page admin URL. The vanity slug won't work.
Step 7: Initiate OAuth
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/LIConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations",
"authMethod": "own"
}'
Response:
{
"result": true,
"authorizeUrl": "https://www.linkedin.com/oauth/v2/authorization?response_type=code&client_id=...&redirect_uri=...&scope=openid%20profile%20w_organization_social%20r_organization_social&state=...",
"errors": []
}
Step 8: Handle the callback
After consent, LinkedIn redirects to Wiro's callback. Wiro exchanges the code for access + refresh tokens, fetches the member's localizedFirstName + localizedLastName from GET /v2/me, and returns the user to your redirectUrl.
Success URL:
https://your-app.com/settings/integrations?li_connected=true&li_name=Jane%20Doe
li_name is the connected LinkedIn member's display name (a human), not the Company Page name — the page is identified by organizationId which you set in Step 6.
const params = new URLSearchParams(window.location.search);
if (params.get("li_connected") === "true") {
const name = params.get("li_name");
showSuccess(`Connected as ${name}`);
} else if (params.get("li_error")) {
handleError(params.get("li_error"));
}
Step 9: Verify
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/LIStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
Response (note the non-standard field name):
{
"result": true,
"connected": true,
"linkedinName": "Jane Doe",
"connectedAt": "2026-04-17T12:00:00.000Z",
"tokenExpiresAt": "2026-06-16T12:00:00.000Z",
"refreshTokenExpiresAt": "2027-04-17T12:00:00.000Z",
"errors": []
}
linkedinNameis the field name — notusernamelike other providers.- Access tokens last ~60 days; refresh tokens ~1 year (LinkedIn returns both durations in the token exchange response).
Step 10: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
API Reference
POST /UserAgentOAuth/LIConnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
redirectUrl |
string | Yes | HTTPS URL. |
authMethod |
string | No | "wiro" (coming soon) or "own". |
GET /UserAgentOAuth/LICallback
Query params: li_connected=true&li_name=... or li_error=....
POST /UserAgentOAuth/LIStatus
Response fields: connected, linkedinName (not username), connectedAt, tokenExpiresAt, refreshTokenExpiresAt.
POST /UserAgentOAuth/LIDisconnect
Clears LinkedIn credentials (no remote revoke).
POST /UserAgentOAuth/TokenRefresh
Running agents refresh the LinkedIn token automatically via the daily maintenance cron. Use this only for debugging or manual overrides.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TokenRefresh" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"provider": "linkedin"
}'
Uses the stored refresh token. Returns new access + refresh tokens. See Automatic token refresh.
Using the Skill
Once the LinkedIn Company Page is connected (organization ID persisted), the agent's scheduled tasks use the linkedin-post platform skill to publish text, image, and video posts to the Company Page. To adjust the cron of the built-in cron-content-scanner task (Social Manager), send an Update with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for
_editable: false skills:
{
"guid": "your-useragent-guid",
"configuration": {
"custom_skills": [
{
"key": "cron-content-scanner",
"enabled": true,
"interval": "0 */4 * * *"
}
]
}
}
To change what the scheduled task posts (topics, tone, audience angle), edit the paired preference skill content-tone instead — see Agent Skills → Updating Preferences.
Troubleshooting
| Error code | Meaning | What to do |
|---|---|---|
missing_params |
Callback reached without state or code. |
Restart from Step 7. |
session_expired |
>15 min between LIConnect and callback. |
Call LIConnect again. |
authorization_denied |
User cancelled, or missing required scopes in app. | Verify all four scopes are enabled under Auth → OAuth 2.0 scopes. |
token_exchange_failed |
Wrong Client Secret or redirect URI mismatch. | Re-copy secret; verify URL. |
useragent_not_found |
Invalid or unauthorized guid. | Use POST /UserAgent/MyAgents. |
invalid_config |
No credentials.linkedin block. |
Update with clientId, clientSecret, organizationId. |
internal_error |
Server error. | Retry; contact support if persistent. |
Posts rejected with 401 Unauthorized
Most likely cause: the Community Management API product hasn't been approved yet. During the pending phase, posting works only for admins of the Company Page the app is associated with (My Pages in LinkedIn Developers). Verify admin membership.
"Scope w_organization_social not authorized"
Enable it under Auth → OAuth 2.0 scopes, then have the user reconnect. LinkedIn doesn't automatically grant scopes you haven't enabled.
Wrong organization ID
Use the numeric ID from linkedin.com/company/<ID>/admin/ — not the slug. Update via POST /UserAgent/Update and reconnect if needed.
Multi-Tenant Architecture
- One LinkedIn Developer App per product.
- One Wiro agent instance per customer; capture
organizationIdduring onboarding. - Community Management API approval is per app (not per customer) — apply once.
- Tokens are isolated per agent instance.
- Rate limits per app and per organization; see LinkedIn's rate-limits docs.
Related
Twitter / X Integration
Connect your agent to an X (formerly Twitter) account to publish posts, read timelines, and reply to mentions.
Overview
The Twitter / X integration uses X API v2 with OAuth 2.0 Authorization Code Flow + PKCE.
Skills that use this integration:
twitterx-post— Publish posts, threads, and replies; read mentions
Agents that typically enable this integration:
- Social Manager
- Any custom agent that needs X posting
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" |
Available | One-click connect using Wiro's shared X app. |
"own" |
Available | Use your own X Developer app for custom branding. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- (Own mode) An X Developer account — developer.x.com.
- An HTTPS callback URL for your backend.
Wiro Mode (Simplest)
Skip all the own-mode setup. Just call Connect without authMethod:
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/XConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations"
}'
User consents on the Wiro-branded consent screen. On return parse x_connected=true&x_username=<handle>. Jump to Step 8: Verify below.
Complete Integration Walkthrough — Own Mode
Step 1: Create an X Developer App
- developer.x.com/portal → sign in.
- Apply for a developer account if needed (free tier works for testing).
- Create a Project → create an App inside it.
- Name your app — this shows on the consent screen.
Step 2: Enable OAuth 2.0 with PKCE
- User authentication settings → Set up.
- Pick OAuth 2.0, type: Web App, Automated App or Bot.
- Enable any extras you need (e.g. email).
-
Callback URI / Redirect URL:
https://api.wiro.ai/v1/UserAgentOAuth/XCallback - Set your Website URL (public product URL).
- Save.
Step 3: Note the required scopes
Wiro requests these exact scopes (verified against api-useragent-oauth.js L159):
tweet.read tweet.write users.read offline.access
| Scope | Why |
|---|---|
tweet.read |
Read timeline, mentions, replies. |
tweet.write |
Publish posts and replies. |
users.read |
Get connected user's handle and display name. |
offline.access |
Issues a refresh token alongside the access token. |
Step 4: Copy Client ID and Client Secret
After enabling OAuth 2.0, X shows Client ID and Client Secret — save the secret immediately. You cannot retrieve it later, only regenerate.
Step 5: Save credentials to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"twitter": {
"clientId": "YOUR_X_CLIENT_ID",
"clientSecret": "YOUR_X_CLIENT_SECRET"
}
}
}
}'
Step 6: Initiate OAuth
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/XConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations",
"authMethod": "own"
}'
Response:
{
"result": true,
"authorizeUrl": "https://x.com/i/oauth2/authorize?response_type=code&client_id=...&redirect_uri=...&scope=tweet.read%20tweet.write%20users.read%20offline.access&state=...&code_challenge=...&code_challenge_method=S256",
"errors": []
}
PKCE: Twitter/X is the only Wiro integration that uses PKCE (Proof Key for Code Exchange, S256). Wiro generates the code_verifier / code_challenge automatically and stores them in the OAuth state cache. You don't need to handle PKCE yourself.
Step 7: Handle the callback
User returns with:
https://your-app.com/settings/integrations?x_connected=true&x_username=jane_doe
const params = new URLSearchParams(window.location.search);
if (params.get("x_connected") === "true") {
const handle = params.get("x_username");
showSuccess(`Connected @${handle}`);
} else if (params.get("x_error")) {
handleError(params.get("x_error"));
}
Step 8: Verify
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/XStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
Response:
{
"result": true,
"connected": true,
"username": "jane_doe",
"connectedAt": "2026-04-17T12:00:00.000Z",
"tokenExpiresAt": "2026-04-17T14:00:00.000Z",
"refreshTokenExpiresAt": "2026-10-14T12:00:00.000Z",
"errors": []
}
- Access token lifetime: ~2 hours (short!). Wiro auto-refreshes.
- Refresh token lifetime: ~180 days from connection (hardcoded by Wiro, since X doesn't report one).
username=@-less X handle.
Step 9: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
API Reference
POST /UserAgentOAuth/XConnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
redirectUrl |
string | Yes | HTTPS URL. |
authMethod |
string | No | "wiro" (default) or "own". |
GET /UserAgentOAuth/XCallback
Query params: x_connected=true&x_username=<handle> or x_error=<code>.
POST /UserAgentOAuth/XStatus
Response: connected, username, connectedAt, tokenExpiresAt (~2h), refreshTokenExpiresAt (~180d).
POST /UserAgentOAuth/XDisconnect
Calls X's revoke endpoint (POST https://api.x.com/2/oauth2/revoke) with Basic auth, then clears credentials. X is one of the few providers where Wiro actively revokes.
POST /UserAgentOAuth/TokenRefresh
Running agents refresh the X token automatically every 90 minutes (a dedicated background cron) because X access tokens only last 2 hours. Use this endpoint only for debugging or manual overrides.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TokenRefresh" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"provider": "twitter"
}'
Uses grant_type=refresh_token. Returns new access + refresh tokens. See Automatic token refresh.
Using the Skill
Once the X account is connected, the agent's existing scheduled tasks use the twitterx-post platform skill to publish. To adjust the cron of the built-in cron-content-scanner task (Social Manager), send an Update with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for _editable: false skills:
{
"guid": "your-useragent-guid",
"configuration": {
"custom_skills": [
{
"key": "cron-content-scanner",
"enabled": true,
"interval": "0 */4 * * *"
}
]
}
}
To change what the scheduled task posts (topics, tone, hashtag rules), edit the paired preference skill content-tone instead — see Agent Skills → Updating Preferences.
Troubleshooting
| Error code | Meaning | What to do |
|---|---|---|
missing_params |
Callback reached without state or code. |
Start a new flow from Step 6. |
authorization_denied |
User cancelled, or OAuth 2.0 not enabled in app settings. | Verify OAuth 2.0 setup (Step 2); retry. |
session_expired |
15-min state cache expired (includes PKCE verifier). | Call XConnect again. |
token_exchange_failed |
Wrong Client Secret, redirect URI mismatch, or lost PKCE verifier. | Re-copy Client Secret; verify URL; start over. |
useragent_not_found |
Invalid guid. | Use POST /UserAgent/MyAgents. |
invalid_config |
No credentials.twitter block. |
UserAgent/Update with clientId + clientSecret. |
internal_error |
Server error. | Retry; contact support. |
Posts fail with 429 Too Many Requests
Free-tier X Developer apps have strict per-app rate limits. For production, move to Basic ($100/mo) or higher. Limits are per app, not per user — high-volume multi-tenant partners need a higher tier.
Token expires every 2 hours
Access token lifetime is unusually short. Wiro's agent runs a dedicated background cron every 90 minutes to refresh X tokens before they expire. If the refresh cron hits a wall (e.g. user revoked the app in X settings, or X flagged the app), the next skill call fails with a 401 — reconnect is required.
Multi-Tenant Architecture
- One X Developer app per product in own mode. Wiro-mode partners share Wiro's app.
- One Wiro agent instance per customer.
- Your app display name appears on every customer's consent screen (own mode).
- Rate limits are per app. Plan your X Developer tier around aggregate volume.
Related
TikTok Integration
Connect your agent to a TikTok account to publish videos.
Overview
The TikTok integration uses TikTok's OAuth 2.0 with the Content Posting API.
Skills that use this integration:
tiktok-post— Publish videos and carousel posts
Agents that typically enable this integration:
- Social Manager
- Any custom agent that needs TikTok publishing
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" |
Available | One-click connect using Wiro's shared TikTok app. |
"own" |
Available | Use your own TikTok for Developers app. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- (Own mode) A TikTok for Developers account — developers.tiktok.com.
- An HTTPS callback URL.
Wiro Mode
Call TikTokConnect without authMethod, redirect, parse tiktok_connected=true&tiktok_username=<display_name> (display name, not the @handle).
Complete Integration Walkthrough — Own Mode
Step 1: Create a TikTok for Developers App
- developers.tiktok.com/apps → sign in.
- Create app.
- App name, category, description, icon.
Step 2: Add Login Kit + Content Posting API
- Add products.
- Add Login Kit and Content Posting API.
Step 3: Configure redirect URI
- Login Kit → Platforms → Web.
-
Add callback URL:
https://api.wiro.ai/v1/UserAgentOAuth/TikTokCallback - Save.
Step 4: Note the required scopes
Wiro requests these exact scopes (verified against api-useragent-oauth.js L483-L484):
user.info.basic,video.publish
| Scope | Why |
|---|---|
user.info.basic |
User handle, avatar, display name. |
video.publish |
Publish video content to the authorized account. |
Other scopes like video.upload, video.list are not used by Wiro.
Step 5: Copy Client Key and Client Secret
App details → copy Client Key and Client Secret. Note: TikTok calls the first one "key" (not "ID") — the field name in Wiro is clientKey.
Step 6: Save credentials
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"tiktok": {
"clientKey": "YOUR_TIKTOK_CLIENT_KEY",
"clientSecret": "YOUR_TIKTOK_CLIENT_SECRET"
}
}
}
}'
Step 7: Initiate OAuth
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TikTokConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations",
"authMethod": "own"
}'
Response:
{
"result": true,
"authorizeUrl": "https://www.tiktok.com/v2/auth/authorize/?client_key=...&scope=user.info.basic,video.publish&response_type=code&redirect_uri=...&state=...",
"errors": []
}
Step 8: Handle the callback
Success: ?tiktok_connected=true&tiktok_username=<display_name>.
Error: ?tiktok_error=<code>.
tiktok_username in the callback is populated from TikTok's display_name field (the creator's public display name), not the handle. The handle/username as it appears in URLs is not exposed by TikTok's OAuth user info endpoint.
Step 9: Verify
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TikTokStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
Response:
{
"result": true,
"connected": true,
"username": "Creator Display Name",
"connectedAt": "2026-04-17T12:00:00.000Z",
"tokenExpiresAt": "2026-04-18T12:00:00.000Z",
"refreshTokenExpiresAt": "2027-04-17T12:00:00.000Z",
"errors": []
}
- Access token: ~1 day (86400s).
- Refresh token: ~1 year (31536000s).
username= TikTok display name (the creator's public display name as set in their profile), NOT the@handle. The handle/URL username is not exposed by TikTok's OAuthuser/info/endpoint, so Wiro storesdisplay_nameand returns it asusername.
Step 10: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
API Reference
POST /UserAgentOAuth/TikTokConnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
redirectUrl |
string | Yes | HTTPS URL. |
authMethod |
string | No | "wiro" (default) or "own". |
GET /UserAgentOAuth/TikTokCallback
Query params: tiktok_connected=true&tiktok_username=<display_name> or tiktok_error=<code>. tiktok_username is TikTok's display name (from the OAuth user/info/ endpoint's display_name field), not the @handle.
POST /UserAgentOAuth/TikTokStatus
Response: connected, username (= tiktokUsername), connectedAt, tokenExpiresAt (~1 day), refreshTokenExpiresAt (~1 year).
POST /UserAgentOAuth/TikTokDisconnect
Calls TikTok's revoke endpoint (POST https://open.tiktokapis.com/v2/oauth/revoke/), then clears credentials. TikTok is one of the few providers where Wiro actively revokes.
POST /UserAgentOAuth/TokenRefresh
Running agents refresh the TikTok token automatically via the daily maintenance cron (access token lifetime is 1 day). Use this only for debugging.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TokenRefresh" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"provider": "tiktok"
}'
Troubleshooting
| Error code | Meaning | What to do |
|---|---|---|
missing_params |
Callback hit without state or code. |
Start a new OAuth flow. |
authorization_denied |
User cancelled, or scopes not enabled. | Verify scope configuration. |
session_expired |
15-min state cache expired. | Restart OAuth. |
token_exchange_failed |
Wrong Client Secret or redirect URI mismatch. | Re-copy; verify URL. |
useragent_not_found |
Invalid guid. | Use POST /UserAgent/MyAgents. |
invalid_config |
No credentials.tiktok block. |
Update with clientKey + clientSecret. |
internal_error |
Server error. | Retry. |
"unaudited_client" or limited publishing
Until your TikTok app is audited, publishing may be limited to private posts or a small set of listed test users. Submit for audit in the TikTok Developer portal for production volume.
Multi-Tenant Architecture
- One TikTok app per product in own mode.
- One Wiro agent instance per customer.
- TikTok per-app limits apply — plan around aggregate volume.
Related
Google Ads Integration
Connect your agent to Google Ads for campaign management, keyword research, and ad copy.
Overview
The Google Ads integration uses Google OAuth 2.0 with the Google Ads API v23 REST endpoints.
Skills that use this integration:
googleads-manage— Campaign / ad group / keyword management, insightsads-manager-common— Shared ads helpers
Agents that typically enable this integration:
- Google Ads Manager
- Any custom agent that needs paid-search capabilities
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" |
Available | One-click connect using Wiro's Google Cloud project. |
"own" |
Available | Own Google Cloud project, Developer Token, and MCC manager customer. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Google Ads account (or MCC) the connecting user administers.
- (Own mode) A Google Cloud project with the Google Ads API enabled.
- (Own mode) A Google Ads Developer Token — request from your MCC.
- (Own mode) Your Manager (MCC) Customer ID (10 digits, no dashes) for server-to-server calls.
- An HTTPS callback URL.
Wiro Mode
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GAdsConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations"
}'
User returns with ?gads_connected=true&gads_accounts=[{...}]. Present to user if multiple. Call GAdsSetCustomerId. Skip to Step 8: Verify.
Complete Integration Walkthrough — Own Mode
Step 1: Create a Google Cloud Project
- console.cloud.google.com → create a project.
- APIs & Services → Library → enable Google Ads API.
-
OAuth consent screen:
- External user type for multi-tenant.
- App name, support email, dev contact.
- Add scope:
https://www.googleapis.com/auth/adwords. - While in Testing status: add test users (the Google accounts that will connect).
Step 2: Create OAuth 2.0 Client ID
- APIs & Services → Credentials → Create credentials → OAuth client ID.
- Application type: Web application.
-
Authorized redirect URIs:
https://api.wiro.ai/v1/UserAgentOAuth/GAdsCallback - Save; copy Client ID and Client Secret.
Step 3: Get a Developer Token
- Sign in to your Google Ads MCC.
- Tools → API Center → request a token.
- Start with a test token; apply for basic access for production.
Step 4: Save credentials to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"googleads": {
"clientId": "YOUR_GOOGLE_OAUTH_CLIENT_ID",
"clientSecret": "YOUR_GOOGLE_OAUTH_CLIENT_SECRET",
"developerToken": "YOUR_GOOGLE_ADS_DEVELOPER_TOKEN",
"managerCustomerId": "1234567890"
}
}
}
}'
managerCustomerId is your MCC's 10-digit customer ID without dashes.
Step 5: Initiate OAuth
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GAdsConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations",
"authMethod": "own"
}'
Response:
{
"result": true,
"authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=...&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fadwords&state=...&access_type=offline&prompt=consent",
"errors": []
}
Scope is a single string: https://www.googleapis.com/auth/adwords. access_type=offline&prompt=consent ensures a refresh token is issued.
Step 6: Handle the callback
After the token exchange, Wiro queries customers:listAccessibleCustomers and fetches customer.descriptive_name for each accessible customer via the Google Ads API.
Success URL:
https://your-app.com/settings/integrations?gads_connected=true&gads_accounts=%5B%7B%22id%22%3A%221234567890%22%2C%22name%22%3A%22My%20Client%22%7D%5D
gads_accountsis only populated when a developer token is available. If the callback finishes without accessible customers,gads_accountsis omitted entirely (not an empty array).- Each entry:
{ id, name, status }—statuscomes from the Google Ads API customer status (e.g."ENABLED","CANCELLED") and may be an empty string if the per-customer lookup failed.
const params = new URLSearchParams(window.location.search);
if (params.get("gads_connected") === "true") {
const accounts = JSON.parse(decodeURIComponent(params.get("gads_accounts") || "[]"));
if (accounts.length === 1) {
await setCustomerId(accounts[0]);
} else if (accounts.length > 1) {
presentCustomerPicker(accounts);
}
}
Step 7: Persist the customer ID selection
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GAdsSetCustomerId" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"customerId": "1234567890"
}'
Either 10-digit or 123-456-7890 format works — non-digits are stripped automatically.
Response:
{
"result": true,
"customerId": "1234567890",
"errors": []
}
Triggers agent restart if running.
Step 8: Verify and Start
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GAdsStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
Response (note the non-standard field name — customerId instead of username):
{
"result": true,
"connected": true,
"customerId": "1234567890",
"connectedAt": "2026-04-17T12:00:00.000Z",
"tokenExpiresAt": "2026-04-17T13:00:00.000Z",
"errors": []
}
- Access token lifetime: 1 hour (short). The agent runs a background refresh cron every 45 minutes.
- No
refreshTokenExpiresAt— Google's refresh tokens don't expire in typical use (unless revoked).
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
API Reference
POST /UserAgentOAuth/GAdsConnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
redirectUrl |
string | Yes | HTTPS URL. |
authMethod |
string | No | "wiro" (default) or "own". |
GET /UserAgentOAuth/GAdsCallback
Query params: gads_connected=true&gads_accounts=<JSON> (when developer token available) or gads_error=<code>.
POST /UserAgentOAuth/GAdsSetCustomerId
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
customerId |
string | Yes | 10-digit customer ID. Non-digits stripped. |
Response: { result, customerId, errors }.
POST /UserAgentOAuth/GAdsStatus
Response fields: connected, customerId (not username), connectedAt, tokenExpiresAt (~1h).
POST /UserAgentOAuth/GAdsDisconnect
Clears Google Ads credentials (no remote revoke).
POST /UserAgentOAuth/TokenRefresh
Running agents refresh the Google Ads token automatically every 45 minutes (access tokens last 1 hour). Use this only for debugging.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TokenRefresh" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"provider": "googleads"
}'
Using the Skill
Once Google Ads is connected and customerId is persisted, the agent's scheduled tasks use the googleads-manage platform skill to pull metrics and manage campaigns. Adjust the cron of the built-in cron-performance-reporter task (Google Ads Manager) with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for _editable: false skills:
{
"guid": "your-useragent-guid",
"configuration": {
"custom_skills": [
{
"key": "cron-performance-reporter",
"enabled": true,
"interval": "0 9 * * *"
}
]
}
}
To change what the reporter includes (wasted-spend threshold, target ROAS, reporting preferences), edit the paired preference skill ad-strategy instead — see Agent Skills → Updating Preferences.
Troubleshooting
| Error code | Meaning | What to do |
|---|---|---|
missing_params |
Callback hit without state or code. |
Start a new flow from Step 5. |
authorization_denied |
User cancelled, or consent screen in Testing and the user isn't a test user. | Add test user or publish consent screen. |
session_expired |
State cache expired. | Restart. |
token_exchange_failed |
Wrong Client Secret or redirect URI mismatch. | Re-copy; verify URL. |
template_not_found (wiro mode) |
Wiro's template doesn't have googleads credentials. |
Contact support or switch to own mode. |
useragent_not_found |
Invalid guid. | Use POST /UserAgent/MyAgents. |
invalid_config |
No credentials.googleads block. |
Update with all four fields. |
internal_error |
Server error. | Retry; contact support. |
USER_PERMISSION_DENIED on API calls
The OAuth-authorized user lacks access to the customerId you chose. Pick a different customer from gads_accounts or have the user request access.
Developer Token rejected
Test tokens can only query accounts in your own MCC hierarchy. For customer accounts outside your MCC, you need Basic Access — apply in Tools → API Center.
Multi-Tenant Architecture
- One Google Cloud project per product. Publish the OAuth consent screen.
- Apply for Basic or Standard Developer Token access based on expected volume.
- One Wiro agent instance per customer;
customerIdis per-instance. - Tokens auto-refresh via the stored refresh token.
Related
HubSpot Integration
Connect your agent to HubSpot for contact, deal, and engagement management via the HubSpot CRM API.
Overview
The HubSpot integration uses HubSpot's OAuth 2.0.
Skills that use this integration:
hubspot-crm— Contact/deal CRUD, note and task creation, sequence enrollmentnewsletter-compose— optional; uses HubSpot as an ESP when enabled alongside
Agents that typically enable this integration:
- Lead Generation Manager
- Newsletter Manager (HubSpot as ESP)
- Any custom agent with CRM capabilities
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" |
Available | One-click connect using Wiro's HubSpot app. |
"own" |
Available | Your own HubSpot developer app. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A HubSpot account the connecting user is an admin of.
- (Own mode) A HubSpot developer account — developers.hubspot.com.
- An HTTPS callback URL.
Wiro Mode
Call HubSpotConnect without authMethod, redirect, parse hubspot_connected=true&hubspot_portal=<id>&hubspot_name=<name>.
Complete Integration Walkthrough — Own Mode
Step 1: Create a HubSpot App
- developers.hubspot.com → sign in → Create app.
- Set App name and App description (shown on consent).
Step 2: Configure Auth
- Open the Auth tab.
-
Redirect URL:
https://api.wiro.ai/v1/UserAgentOAuth/HubSpotCallback -
Scopes — Wiro requests a fixed scope string plus optional_scopes (verified against
api-useragent-oauth.jsL2551-L2556). Enable all of the following in your HubSpot app's Auth tab:Required
scope(mandatory — OAuth fails if any is missing):crm.objects.contacts.readcrm.objects.contacts.writecrm.lists.readcrm.lists.writeoauth
optional_scope(granted on consent if enabled, otherwise skipped — Wiro doesn't fail if missing):crm.objects.companies.readcrm.objects.companies.writecrm.objects.deals.readcrm.objects.deals.writecrm.objects.owners.readcrm.schemas.contacts.readcontenttransactional-emailfiles
- Save.
Wiro's authorize URL is built with this exact scope list — you cannot customize it per integration. Enabling additional scopes in your HubSpot app beyond this set has no effect (Wiro won't request them). If a required scope is missing from your app's configuration, the consent screen will error.
Step 3: Copy Client ID and Client Secret
Auth tab → copy Client ID and Client Secret.
Step 4: Save credentials to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"hubspot": {
"clientId": "YOUR_HUBSPOT_CLIENT_ID",
"clientSecret": "YOUR_HUBSPOT_CLIENT_SECRET"
}
}
}
}'
Step 5: Initiate OAuth
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/HubSpotConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations",
"authMethod": "own"
}'
Response:
{
"result": true,
"authorizeUrl": "https://app.hubspot.com/oauth/authorize?client_id=...&redirect_uri=...&scope=...&state=...",
"errors": []
}
Step 6: Handle the callback
Wiro exchanges the code, then calls GET https://api.hubapi.com/oauth/v1/access-tokens/<access_token> to fetch hub_id (portalId) and hub_domain (portalName).
Success URL:
https://your-app.com/settings/integrations?hubspot_connected=true&hubspot_portal=12345678&hubspot_name=My%20Workspace
Step 7: Verify and Start
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/HubSpotStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
Response:
{
"result": true,
"connected": true,
"username": "12345678",
"connectedAt": "2026-04-17T12:00:00.000Z",
"tokenExpiresAt": "2026-04-17T12:30:00.000Z",
"errors": []
}
HubSpot tokens expire in 30 minutes. This is the shortest token lifetime of any Wiro integration. Every running agent has a dedicated background cron that refreshes HubSpot tokens every 20 minutes — you never need to call TokenRefresh from your own app. If you see stale tokens despite the agent being running, check agent logs for refresh failures.
Note: username in the response is actually the portalId as a string (not portalName) — this is backend behavior. hubspot_name is only set on the callback URL, not re-surfaced by Status.
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
API Reference
POST /UserAgentOAuth/HubSpotConnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
redirectUrl |
string | Yes | HTTPS URL. |
authMethod |
string | No | "wiro" (default) or "own". |
GET /UserAgentOAuth/HubSpotCallback
Query params: hubspot_connected=true&hubspot_portal=<id>&hubspot_name=<name> or hubspot_error=<code>.
POST /UserAgentOAuth/HubSpotStatus
Response fields: connected, username (= portalId string), connectedAt, tokenExpiresAt (~30 min).
POST /UserAgentOAuth/HubSpotDisconnect
Clears HubSpot credentials (no remote revoke).
POST /UserAgentOAuth/TokenRefresh
Running agents refresh HubSpot tokens automatically every 20 minutes (tokens last 30 minutes). Use this endpoint only for debugging.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TokenRefresh" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"provider": "hubspot"
}'
Returns new access + refresh tokens. See Automatic token refresh.
Using the Skill
{
"guid": "your-useragent-guid",
"configuration": {
"custom_skills": [
{
"key": "lead-enrichment",
"enabled": true,
"interval": "0 */4 * * *",
"value": "Enrich new contacts with company information"
}
]
}
}
Troubleshooting
| Error code | Meaning | What to do |
|---|---|---|
missing_params |
Callback hit without state or code. |
User didn't complete consent; restart the flow. |
authorization_denied |
User cancelled, or missing scopes. | Verify scope list in the HubSpot app's Auth tab. |
session_expired |
State cache expired (15 min TTL). | Restart the OAuth flow. |
token_exchange_failed |
Wrong Client Secret or redirect URI mismatch. | Re-copy; verify URL. |
useragent_not_found |
Invalid guid. | Use POST /UserAgent/MyAgents. |
invalid_config |
No credentials.hubspot block. |
Update with clientId + clientSecret. |
internal_error |
Server error. | Retry; contact support. |
403 Forbidden on API calls
Usually a missing scope. Look up the specific HubSpot API endpoint you're hitting, add the required scope in your app's Auth tab, then disconnect and reconnect (scope changes require re-consent).
Token expired error at runtime
HubSpot's 30-minute token lifetime makes refresh critical. The agent container runs the HubSpot refresh cron every 20 minutes, so stale tokens should only appear if:
- The agent is stopped (status 0/1/6). Start it:
POST /UserAgent/Start. - The refresh token was revoked in HubSpot's app management UI. User must reconnect.
- The refresh cron itself is failing — check agent logs via dashboard or support.
Multi-Tenant Architecture
- One HubSpot developer app per product — submit to the HubSpot App Marketplace for listed visibility, or stay private.
- One Wiro agent instance per customer.
- Portal IDs are unique per customer's HubSpot account.
- Per-app rate limits apply — see HubSpot's API usage guidelines.
Related
Mailchimp Integration
Connect your agent to Mailchimp for audience and campaign management. Supports OAuth 2.0 or direct API key.
Overview
The Mailchimp integration is unique in supporting three authentication options: Wiro's shared OAuth app, your own OAuth app, or a direct API key bypassing OAuth entirely.
Skills that use this integration:
mailchimp-email— Audience and campaign managementnewsletter-compose— uses Mailchimp as an ESP when enabled alongside
Agents that typically enable this integration:
- Newsletter Manager
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" |
Available | OAuth with Wiro's shared Mailchimp app. |
"own" |
Available | OAuth with your own Mailchimp registered app. |
| API key | Available | Paste a server-scoped Mailchimp API key directly — no OAuth. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Mailchimp account.
- (Own OAuth mode) A registered Mailchimp app — admin.mailchimp.com/account/oauth2_client.
- (API-key mode) A server-scoped Mailchimp API key.
Option A: OAuth (Wiro or Own Mode)
Own Step 1: Register a Mailchimp app
- admin.mailchimp.com/account/oauth2_client.
- Register and manage your apps.
- Fill App name, Description, Company, App website.
-
Under Redirect URI, add:
https://api.wiro.ai/v1/UserAgentOAuth/MailchimpCallback - Save; copy Client ID and Client Secret.
No OAuth scopes to configure. Mailchimp's OAuth 2.0 doesn't use scopes — connected apps get full account access. Wiro's authorizeUrl omits any scope parameter.
Own Step 2: Save credentials
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"mailchimp": {
"clientId": "YOUR_MAILCHIMP_CLIENT_ID",
"clientSecret": "YOUR_MAILCHIMP_CLIENT_SECRET"
}
}
}
}'
OAuth Step 3: Initiate
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MailchimpConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations",
"authMethod": "own"
}'
Response:
{
"result": true,
"authorizeUrl": "https://login.mailchimp.com/oauth2/authorize?response_type=code&client_id=...&redirect_uri=...&state=...",
"errors": []
}
No scope parameter — Mailchimp OAuth doesn't use scopes.
OAuth Step 4: Handle the callback
Wiro exchanges the code via POST https://login.mailchimp.com/oauth2/token, then fetches metadata from GET https://login.mailchimp.com/oauth2/metadata with Authorization: OAuth <access_token> to retrieve the server prefix (dc) and account name.
Success URL:
https://your-app.com/settings/integrations?mailchimp_connected=true&mailchimp_account=Your%20Company
OAuth Step 5: Verify
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MailchimpStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
Response:
{
"result": true,
"connected": true,
"username": "Your Company",
"connectedAt": "2026-04-17T12:00:00.000Z",
"errors": []
}
Mailchimp tokens don't expire. Status responses don't include tokenExpiresAt or refreshTokenExpiresAt. There's no TokenRefresh support for Mailchimp either — it's excluded from the valid providers list.
Option B: Direct API Key (No OAuth)
For server-side agents where OAuth is overkill:
Step 1: Get a Mailchimp API Key
- Sign in → Profile → Extras → API keys.
- Create A Key, name it, copy the value. The key ends in a datacenter prefix like
-us14.
Step 2: Save to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"mailchimp": {
"apiKey": "abcdef1234567890-us14"
}
}
}
}'
No further OAuth step. The agent uses the API key directly.
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
API Reference
POST /UserAgentOAuth/MailchimpConnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
redirectUrl |
string | Yes | HTTPS URL. |
authMethod |
string | No | "wiro" (default) or "own". |
GET /UserAgentOAuth/MailchimpCallback
Query params: mailchimp_connected=true&mailchimp_account=<name> or mailchimp_error=<code>.
POST /UserAgentOAuth/MailchimpStatus
Response fields: connected, username (= accountName), connectedAt. No tokenExpiresAt, no refreshTokenExpiresAt — tokens don't expire.
API key-only mode caveat: connected is computed from authMethod in {wiro, own} and a non-empty accessToken. If you set up Mailchimp via direct API key (no OAuth), authMethod and accessToken stay empty and MailchimpStatus.connected returns false — even though the agent runtime is fully functional (the mailchimp-email skill reads
$MAILCHIMP_API_KEY directly via start.sh). Don't use MailchimpStatus.connected as the source of truth for API key setups; instead, check that credentials.mailchimp.apiKey is non-empty in POST /UserAgent/Detail.
POST /UserAgentOAuth/MailchimpDisconnect
Clears credentials (no remote revoke — Mailchimp doesn't expose a revoke endpoint for OAuth tokens).
TokenRefresh
Not supported for Mailchimp. Calling POST /UserAgentOAuth/TokenRefresh with provider: "mailchimp" returns an error — Mailchimp tokens don't expire, so refresh is unnecessary.
Using the Skill
Once Mailchimp is connected (OAuth or API key), the agent's scheduled tasks use the mailchimp-email platform skill for audience and campaign operations. Adjust the cron of the built-in cron-subscriber-scanner task (Newsletter Manager) with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for _editable: false skills:
{
"guid": "your-useragent-guid",
"configuration": {
"custom_skills": [
{
"key": "cron-subscriber-scanner",
"enabled": true,
"interval": "0 10 * * *"
}
]
}
}
To change what the scanner checks (target lists, bounce thresholds, tone, audience segments), edit the paired preference skill newsletter-strategy instead — see Agent Skills → Updating Preferences.
Troubleshooting
| Error code | Meaning | What to do |
|---|---|---|
authorization_denied |
User cancelled. | Retry. |
session_expired |
State cache expired (15 min). | Restart. |
token_exchange_failed |
Wrong Client Secret or redirect URI mismatch. | Re-copy; verify URL. |
useragent_not_found |
Invalid guid. | Use POST /UserAgent/MyAgents. |
invalid_config |
No credentials.mailchimp block. |
Update with credentials. |
internal_error |
Server error. | Retry; contact support. |
API calls fail with 401
- OAuth: stored token is invalid; disconnect and reconnect.
- API key: wrong key or datacenter suffix stripped. Paste the full key including
-us14.
Multi-Tenant Architecture
- One Mailchimp registered app per product in own-OAuth mode.
- API-key mode is simplest for tenants who prefer a single-purpose key.
- One Wiro agent instance per customer.
- Mailchimp rate limits are per-datacenter and per-account (~10 concurrent connections).
Related
Google Drive Integration
Connect your agent to Google Drive to read, write, and organize files in selected folders.
Overview
The Google Drive integration uses Google's OAuth 2.0 with the full Drive API. The connecting user explicitly picks which folders the agent can access.
Skills that use this integration:
google-drive— Read files, write outputs, manage folders
Agents that typically enable this integration:
- Any content or research agent that needs persistent file storage accessible to humans.
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" |
Available | One-click connect with Wiro's Google Cloud project. |
"own" |
Available | Your own Google Cloud project for custom branding. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Google account for the connecting user.
- (Own mode) A Google Cloud project with the Drive API enabled.
- An HTTPS callback URL.
Wiro Mode
Call GoogleDriveConnect without authMethod, redirect user, parse gdrive_connected=true&gdrive_folders=<JSON> on return.
Complete Integration Walkthrough — Own Mode
Step 1: Create a Google Cloud Project
- console.cloud.google.com → create a project.
- APIs & Services → Library → enable Drive API.
-
OAuth consent screen:
- External user type.
- App name, support email.
- Add scope:
https://www.googleapis.com/auth/drive. - Test users (while in Testing status).
Step 2: Create OAuth 2.0 Client ID
- APIs & Services → Credentials → Create credentials → OAuth client ID.
- Application type: Web application.
-
Authorized redirect URIs:
https://api.wiro.ai/v1/UserAgentOAuth/GoogleDriveCallback - Save; copy Client ID and Client Secret.
Step 3: Save credentials to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"googledrive": {
"clientId": "YOUR_GOOGLE_OAUTH_CLIENT_ID",
"clientSecret": "YOUR_GOOGLE_OAUTH_CLIENT_SECRET"
}
}
}
}'
Step 4: Initiate OAuth
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GoogleDriveConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"redirectUrl": "https://your-app.com/settings/integrations",
"authMethod": "own"
}'
Response:
{
"result": true,
"authorizeUrl": "https://accounts.google.com/o/oauth2/v2/auth?client_id=...&redirect_uri=...&response_type=code&scope=https%3A%2F%2Fwww.googleapis.com%2Fauth%2Fdrive&state=...&access_type=offline&prompt=consent",
"errors": []
}
Wiro requests the full drive scope (not the narrow drive.file). This is needed for folder discovery and file operations across the user's Drive.
Step 5: Handle the callback
After the token exchange, Wiro queries Drive's files.list for root-level folders and prepends a virtual { id: "shared", name: "Shared with me", virtual: true } entry. These folder suggestions are returned in the callback URL.
Success URL:
https://your-app.com/settings/integrations?gdrive_connected=true&gdrive_folders=%5B%7B%22id%22%3A%22shared%22%2C%22name%22%3A%22Shared%20with%20me%22%2C%22virtual%22%3Atrue%7D%2C%7B%22id%22%3A%221AbC%22%2C%22name%22%3A%22Agent%20Outputs%22%7D%5D
gdrive_folders is encodeURIComponent(JSON.stringify([...])). Each entry: { id, name, virtual? }.
const params = new URLSearchParams(window.location.search);
if (params.get("gdrive_connected") === "true") {
const folders = JSON.parse(decodeURIComponent(params.get("gdrive_folders") || "[]"));
presentFolderPicker(folders);
}
Step 6: Browse more folders (optional)
If the user wants to drill into subfolders rather than pick from root, call:
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GoogleDriveListFolder" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"parentId": "1AbC",
"searchQuery": ""
}'
Response:
{
"result": true,
"folders": [
{
"id": "2XyZ",
"name": "2025 Q1"
},
{
"id": "3MnO",
"name": "Archive"
}
],
"errors": []
}
parentIddefaults to"root".searchQueryis optional; passes through to Drive's query syntax.
Step 7: Persist selected folders
Once the user picks (up to 5 folders), commit the selection:
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GoogleDriveSetFolder" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"folders": [
{
"id": "1AbC",
"name": "Agent Outputs"
},
{
"id": "2XyZ",
"name": "2025 Q1"
}
]
}'
Note the endpoint is GoogleDriveSetFolder (singular) even though it accepts an array.
Response:
{
"result": true,
"folders": [
{
"id": "1AbC",
"name": "Agent Outputs"
},
{
"id": "2XyZ",
"name": "2025 Q1"
}
],
"errors": []
}
Requirements:
foldersmust be a non-empty array.- Each item needs
id;nameis optional (falls back to empty string). - Maximum 5 folders per agent. Sending more than 5 fails with
Maximum 5 folders allowed— the request is rejected, not truncated.
Triggers agent restart if running.
Step 8: Verify
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GoogleDriveStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "userAgentGuid": "your-useragent-guid" }'
Response:
{
"result": true,
"connected": true,
"folders": [
{
"id": "1AbC",
"name": "Agent Outputs"
},
{
"id": "2XyZ",
"name": "2025 Q1"
}
],
"connectedAt": "2026-04-17T12:00:00.000Z",
"tokenExpiresAt": "2026-04-17T13:00:00.000Z",
"errors": []
}
- Access token lifetime: 1 hour (short). The agent runs a background refresh cron every 45 minutes.
- No
refreshTokenExpiresAt— Google refresh tokens don't normally expire. foldersshows the currently selected folders (unique to Google Drive Status).
Agent Runtime Usage (inside the container)
Once the OAuth flow is done and GoogleDriveSetFolder has persisted the folder selection, the agent container runs independently of the Wiro API — the google-drive platform skill (loaded from skills/google-drive/SKILL.md) reads env vars and talks to Google's REST APIs directly.
Env vars exported by docker/start.sh (only when skills.google-drive is enabled and a Drive accessToken exists):
| Env var | Source | Notes |
|---|---|---|
GDRIVE_CLIENT_ID |
credentials.googledrive.clientId |
For token refresh |
GDRIVE_CLIENT_SECRET |
credentials.googledrive.clientSecret |
For token refresh |
GDRIVE_ACCESS_TOKEN |
credentials.googledrive.accessToken |
Auto-refreshed by the container every 45 minutes via POST /UserAgentOAuth/TokenRefresh (short-lived: Google access tokens expire in 1 hour) |
GDRIVE_REFRESH_TOKEN |
credentials.googledrive.refreshToken |
Passed to the refresh cron |
GDRIVE_FOLDERS |
credentials.googledrive.folders |
JSON array of [{id, name}] — the user-selected folders from GoogleDriveSetFolder |
How cron skills access the token: There is no gdrive-token helper command. The agent reads the pre-refreshed token directly from the env var:
exec command="echo $GDRIVE_ACCESS_TOKEN"
Then uses it in curl calls as Authorization: Bearer $GDRIVE_ACCESS_TOKEN. If an API call returns 401, the agent re-reads $GDRIVE_ACCESS_TOKEN (the 45-minute cron may have just refreshed it). See the full SKILL.md (skills/google-drive/SKILL.md) for all endpoint examples.
Checking folder selection in cron scripts: Since GDRIVE_FOLDERS is a JSON array (not a single ID), cron skills check it like this:
exec command="echo $GDRIVE_FOLDERS"
Empty output or [] means the user hasn't selected any folder yet — the scan should notify the operator and stop instead of iterating over an empty list.
Step 9: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
API Reference
POST /UserAgentOAuth/GoogleDriveConnect
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
redirectUrl |
string | Yes | HTTPS URL. |
authMethod |
string | No | "wiro" (default) or "own". |
GET /UserAgentOAuth/GoogleDriveCallback
Query params: gdrive_connected=true&gdrive_folders=<JSON> or gdrive_error=<code>.
POST /UserAgentOAuth/GoogleDriveListFolder
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
parentId |
string | No | Drive folder ID to list. Defaults to "root". Accepts the virtual "shared" ID. |
searchQuery |
string | No | Drive query string (passed through). |
Response: { result, folders: [{id, name}], errors }.
POST /UserAgentOAuth/GoogleDriveSetFolder
| Parameter | Type | Required | Description |
|---|---|---|---|
userAgentGuid |
string | Yes | Agent instance GUID. |
folders |
object[] | Yes | Array of { id, name? }. Max 5 items; sending more returns Maximum 5 folders allowed. |
Response: { result: true, folders: [{id, name}], errors: [] }. Triggers agent restart if running.
POST /UserAgentOAuth/GoogleDriveStatus
Response fields: connected, folders (selected folder list), connectedAt, tokenExpiresAt (~1h).
POST /UserAgentOAuth/GoogleDriveDisconnect
Clears Google Drive credentials (no remote revoke).
POST /UserAgentOAuth/TokenRefresh
Running agents refresh Google Drive tokens automatically every 45 minutes (access tokens last 1 hour). Use this only for debugging.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/TokenRefresh" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"userAgentGuid": "your-useragent-guid",
"provider": "googledrive"
}'
Using the Skill
The google-drive skill can browse, upload, download, and organize files within the selected folders.
Troubleshooting
| Error code | Meaning | What to do |
|---|---|---|
authorization_denied |
User cancelled, or consent screen in Testing and the user isn't a test user. | Add test user or publish consent screen. |
session_expired |
State cache expired (15 min). | Restart. |
token_exchange_failed |
Wrong Client Secret or redirect URI mismatch. | Re-copy; verify URL. |
useragent_not_found |
Invalid guid. | Use POST /UserAgent/MyAgents. |
invalid_config |
No credentials.googledrive block. |
Update with credentials. |
internal_error |
Server error. | Retry. |
Agent can't list folders beyond root
Use GoogleDriveListFolder with the parentId you want to drill into. Or call again with the "shared" virtual ID to see shared drives.
"quotaExceeded" from Drive API
Rate limits are per-project in Google Cloud Console — check and raise quota as needed.
Multi-Tenant Architecture
- One Google Cloud project per product. Publish the consent screen.
- Rate limits are per-project.
- Per-agent folder selection is isolated — Customer A's folders never visible to B.
Related
Gmail Integration
Connect your agent to a Gmail inbox using a Google App Password for IMAP access.
Overview
The Gmail integration uses IMAP with Basic authentication backed by a Google App Password. Agents can monitor the inbox, parse incoming messages, and trigger actions.
Skills that use this integration:
gmail-check— Poll Gmail inbox on a schedule, parse messages, route to actions
Agents that typically enable this integration:
- Blog Content Editor (inbox-triggered workflows)
- Newsletter Manager (test sends)
- Support / App Review agents (operator notifications)
Availability
| Mode | Status | Notes |
|---|---|---|
| App Password | Available | Works on any Gmail account with 2-Step Verification enabled. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Gmail account with 2-Step Verification enabled.
Setup
Step 1: Enable 2-Step Verification
- Sign in to the Google account.
- myaccount.google.com/security.
- Under How you sign in to Google, turn on 2-Step Verification.
Step 2: Create an App Password
- Same Security page → App passwords (appears once 2-Step Verification is on).
- Create a new App Password, label it "Wiro agent".
- Copy the 16-character password (format:
xxxx xxxx xxxx xxxx). Spaces are cosmetic — Wiro accepts either form.
Step 3: Save to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"gmail": {
"account": "[email protected]",
"appPassword": "xxxx xxxx xxxx xxxx"
}
}
}
}'
Only account and appPassword are editable. credentials.gmail.interval (when present in some templates) is NOT used by start.sh and NOT wired to the runtime. The actual polling cadence comes from the scheduled skill cron-gmail-checker under custom_skills[] (a cron wrapper that invokes the built-in gmail-check platform skill). To change how often the inbox is polled, update
custom_skills[key="cron-gmail-checker"].interval via POST /UserAgent/Update — see Agent Skills.
Naming: the platform skill (the IMAP-speaking module loaded from skills/gmail-check/SKILL.md) is gmail-check. The cron wrapper (an entry in custom_skills[] that schedules inbox polling and references gmail-check internally) is cron-gmail-checker (prefixed with cron- to mark it as a scheduled task). When skills.gmail-check is
disabled on the template, the cron wrapper early-returns with HEARTBEAT_OK.
Step 4: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Credential Fields
| Field | Type | Editable | Description |
|---|---|---|---|
account |
string | Yes | Full Gmail address (e.g. [email protected]). |
appPassword |
string | Yes | 16-character Google App Password. Spaces allowed. |
interval |
cron string | No (template-controlled) | Polling frequency. Example: */8 * * * * (every 8 minutes). |
Credentials schema (as returned by POST /UserAgent/Detail)
"gmail": {
"optional": true,
"account": "",
"appPassword": "",
"_editable": {
"account": true,
"appPassword": true
}
}
Runtime Behavior
The gmail-check skill uses IMAP:
- Host:
imaps://imap.gmail.com:993/INBOX - Auth:
--user "$GMAIL_ACCOUNT:$GMAIL_APP_PASSWORD"(Basic-style) - Polls on the configured
interval, processes new messages per agent rules
Env vars inside the agent container (exported only when gmail-check skill is enabled and account is set):
GMAIL_ACCOUNT←credentials.gmail.accountGMAIL_APP_PASSWORD←credentials.gmail.appPassword
Troubleshooting
- "Invalid credentials" when IMAP connects: Wrong App Password, or 2-Step Verification was turned off (which invalidates all App Passwords). Regenerate.
- Agent can't see messages older than ~30 days: IMAP folder defaults. For broader scope, your agent may need to switch to "All Mail" — ask support.
- "Less Secure Apps" mentioned anywhere: Google removed that option in 2022. App Password is the only supported path for IMAP/SMTP.
Related
Telegram Integration
Connect your agent to a Telegram bot as an optional extra messaging channel.
Overview
Telegram is optional on every Wiro agent. By default your agents already support two built-in messaging channels out of the box:
- Web chat — chat with the agent directly from the wiro.ai dashboard with no extra setup.
- Messaging API — your own application sends and receives messages programmatically via Agent Messaging, with real-time streaming over Agent WebSocket or HTTP callbacks via Agent Webhooks.
Adding a Telegram bot gives you a third, complementary channel — useful when you want to:
- Push operator notifications (campaign alerts, new leads, error reports) to a private chat or team group on your phone.
- Give off-dashboard access to team members who don't log into wiro.ai but still want to message the agent.
- Pipe scheduled status reports from cron skills into a shared Telegram channel.
You can skip this integration entirely — agents keep working through web chat and the API regardless. When the bot token is missing or allowedUsers is empty, the Telegram plugin inside the agent is disabled automatically and no messages flow through it.
Availability
| Mode | Status | Notes |
|---|---|---|
| Bot Token | Available | Single Bot Token from BotFather. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Telegram account.
Setup
Step 1: Create a Telegram bot
- In Telegram, start a chat with @BotFather.
- Send
/newbot. - Choose a display name and a username (must end in
bot, e.g.@mycompany_agent_bot). - BotFather returns a Bot Token like
123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11. Copy it.
Step 2: Collect allowed user IDs
Each allowed user must message the bot first.
- User sends any message to the bot.
- Open
https://api.telegram.org/bot<BOT_TOKEN>/getUpdatesin a browser. - Find the
message.from.idvalue in the response — that's the Telegram user ID (numeric).
Alternative: each user can DM @userinfobot in Telegram to get their own ID.
Step 3: Save to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"telegram": {
"botToken": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11",
"allowedUsers": ["761381461", "987654321"],
"sessionMode": [
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
}
}
}
}'
Step 4: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Credential Fields
| Field | Type | Description |
|---|---|---|
botToken |
string | BotFather token (<bot_id>:<secret>). |
allowedUsers |
string[] | Array of Telegram user IDs (numeric strings) allowed to interact. Messages from IDs outside this list are ignored. |
sessionMode |
object[] | string | Session selection. See below. |
sessionMode format
sessionMode is an array of option objects, with exactly one having selected: true:
[
{
"value": "private",
"text": "Private — each user has their own conversation",
"selected": true
},
{
"value": "collaborative",
"text": "Collaborative — all users share the same conversation",
"selected": false
}
]
This matches Wiro's dropdown format. A simpler string form is also accepted at the backend ("private" or "collaborative") but the array form is what the dashboard UI produces.
| Mode | Behavior |
|---|---|
private (default) |
Each allowed user has an isolated conversation with the agent. Messages from user A are never visible to user B. Maps internally to session.dmScope = "per-channel-peer". |
collaborative |
All allowed users share one conversation. Any user sees and can respond to any message. Maps to session.dmScope = "main". |
Runtime Behavior
Env vars inside the agent container:
TELEGRAM_BOT_TOKEN←credentials.telegram.botTokenGATEWAY_TOKEN← internal gateway token
allowedUsers is not an env var. It's serialized two ways during container startup:
- JSON file
/.../credentials/telegram-allowFrom.json— the full array, used by the gateway to filter incoming messages. - Template substitution —
__TELEGRAM_ALLOW_FROM__inopenclaw.json(JSON array) and__TELEGRAM_CHAT_ID__inAGENTS.md(CSV) are replaced at runtime.
The Telegram integration plugin inside the agent is disabled automatically if allowedUsers is empty or botToken is missing — no messages flow.
Troubleshooting
- Bot doesn't respond: Verify
botTokenis correct and the sender's Telegram user ID is inallowedUsers. - "Unauthorized" (401) from Telegram API: BotFather regenerated the token, invalidating the old one. Create a new token and update.
- Rate limits: Telegram bots are limited to ~30 messages/second globally. For burst broadcasts, plan around this.
- Collaborative mode confusion: If users don't see each other's messages, re-save
sessionModewithcollaborativeasselected: trueand restart the agent.
Related
Firebase Integration
Connect your agent to Firebase Cloud Messaging (FCM) to send targeted push notifications to iOS and Android apps.
Overview
The Firebase integration uses FCM HTTP v1 with an Admin SDK service account. A single agent can manage notifications for multiple Firebase projects.
Skills that use this integration:
firebase-push— Send push notifications by topic, device token, or condition
Agents that typically enable this integration:
- Push Notification Manager
Availability
| Mode | Status | Notes |
|---|---|---|
| Service account JSON | Available | Admin SDK service account with FCM permissions. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Firebase project with your iOS and/or Android apps registered and FCM enabled.
Setup
Step 1: Generate a Firebase Admin SDK service account
- console.firebase.google.com → select project.
- Project settings → Service accounts → Firebase Admin SDK → Generate new private key.
- Save the JSON file.
Step 2: Base64-encode the JSON
# Linux
base64 -w 0 firebase-service-account.json > firebase-sa.b64
# macOS
base64 -b 0 firebase-service-account.json > firebase-sa.b64
Step 3: Save to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"firebase": {
"accounts": [
{
"appName": "My App",
"serviceAccountJsonBase64": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii...",
"apps": [
{
"platform": "ios",
"id": "6479306352"
},
{
"platform": "android",
"id": "com.example.app"
}
],
"topics": [
{
"topicKey": "locale_en",
"topicDesc": "English users"
},
{
"topicKey": "tier_paid",
"topicDesc": "Paid subscribers"
}
]
}
]
}
}
}
}'
Step 4: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Credential Fields
credentials.firebase.accounts[] is an array. Each account object:
| Field | Type | Editable | Description |
|---|---|---|---|
appName |
string | Yes | Display name for this project. |
serviceAccountJsonBase64 |
string | Yes | Base64-encoded service account JSON. |
apps |
object[] | Yes | { platform: "ios" | "android", id: string }. id is App Store ID for iOS, package name for Android. |
topics |
object[] | object | Yes | Either an array of { topicKey, topicDesc } or a flat object map { topicKey: topicDesc, ... }. Both are accepted; the runtime converts arrays into the map form. Topics you've subscribed clients to on the device side. |
projectId |
string | No (derived from service account) | Read from the decoded JSON. |
Multi-project setups
Add more entries to accounts[] to manage multiple Firebase projects from one agent:
{
"credentials": {
"firebase": {
"accounts": [
{
"appName": "Consumer App",
"serviceAccountJsonBase64": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii...",
"apps": [
{
"platform": "ios",
"id": "6479306352"
}
],
"topics": [
{
"topicKey": "locale_en",
"topicDesc": "English users"
}
]
},
{
"appName": "Business App",
"serviceAccountJsonBase64": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii...",
"apps": [
{
"platform": "android",
"id": "com.example.business"
}
],
"topics": [
{
"topicKey": "tier_paid",
"topicDesc": "Paid subscribers"
}
]
}
]
}
}
}
Wiro's merge logic uses positional indexes — sending accounts[2] while the template has 1 account creates a new account entry cloned from the template shape, populated with your editable fields.
Runtime Behavior
Env vars (exported only when firebase-push skill is enabled and accounts is non-empty) per account index idx:
FIREBASE_APP_COUNT— total accountsFIREBASE_{idx}_PROJECT_ID— from decoded service account JSONFIREBASE_{idx}_APP_NAMEFIREBASE_{idx}_TOPICS— JSON map of{ topicKey: topicDesc }FIREBASE_{idx}_APPS— JSON array of{ platform, id }
Secret files:
/run/secrets/firebase-sa-{idx}.json— decoded service account (not exposed as env)
Auth: FCM HTTP v1 Authorization: Bearer <token> — tokens minted from the service account.
Base URL: https://fcm.googleapis.com/v1/projects/<PROJECT_ID>/messages:send
Troubleshooting
- "invalid JWT signature": Service account JSON corrupt or truncated. Re-export from Firebase Console and re-encode.
- No devices receive notifications: Verify topics are subscribed on the client side and the topic name matches exactly. Check
FIREBASE_{idx}_TOPICSlogs. - Rate limits: FCM supports up to 600,000 messages/minute per project for HTTP v1 API (see the
firebase-pushskill for topic/condition specifics). Higher volumes require a support request to Google Cloud.
Related
WordPress Integration
Connect your agent to a WordPress site (self-hosted or WordPress.com Business+) to publish posts and pages.
Overview
The WordPress integration uses the WordPress REST API with Basic Authentication backed by a WordPress Application Password.
Skills that use this integration:
wordpress-post— Publish blog posts, pages, categories, tags; upload media
Agents that typically enable this integration:
- Blog Content Editor
Availability
| Mode | Status | Notes |
|---|---|---|
| Application Password | Available | WordPress 5.6+ built-in Application Passwords. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A WordPress site (self-hosted or WordPress.com Business/Commerce) running WordPress 5.6+.
- An admin or editor-level user on that site.
Setup
Step 1: Enable Application Passwords (if disabled)
Enabled by default in WP 5.6+. If your host or security plugin disabled them:
- In
wp-config.php, confirmWP_ENVIRONMENT_TYPEisn't restricting them. - Security plugins like Wordfence or iThemes Security sometimes disable Application Passwords — check their settings.
Step 2: Create an Application Password
- Log in as the user the agent will post as.
- Users → Profile (or Users → All Users → Edit another user if you're admin).
- Scroll to Application Passwords.
- Name it "Wiro agent", Add New Application Password.
- Copy the 24-character password (spaces are cosmetic; both forms work).
Step 3: Save to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"wordpress": {
"url": "https://blog.example.com",
"user": "WiroBlogAgent",
"appPassword": "xxxx xxxx xxxx xxxx xxxx xxxx"
}
}
}
}'
Step 4: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Credential Fields
| Field | Type | Description |
|---|---|---|
url |
string | Site URL with https://, no trailing slash. |
user |
string | WordPress username the Application Password belongs to. |
appPassword |
string | 24-character Application Password. |
Credentials schema (as returned by POST /UserAgent/Detail)
"wordpress": {
"optional": false,
"url": "",
"user": "",
"appPassword": "",
"_editable": {
"url": true,
"user": true,
"appPassword": true
}
}
Runtime Behavior
Env vars inside the agent container (exported only when wordpress-post skill is enabled and url is set):
WORDPRESS_URL←credentials.wordpress.urlWORDPRESS_USER←credentials.wordpress.userWORDPRESS_APP_PASSWORD←credentials.wordpress.appPassword
Auth: --user "$WORDPRESS_USER:$WORDPRESS_APP_PASSWORD" (Basic via Application Password).
Base URL: $WORDPRESS_URL/wp-json/wp/v2/...
Troubleshooting
- 401 Unauthorized on REST API: Username mismatch (case-sensitive on some setups), or Application Password invalid. Regenerate.
-
REST API returns 404 at
/wp-json/: Permalinks set to Plain. Go to Settings → Permalinks and pick any pretty-permalink option. - WordPress.com Business plan: REST API access must be on via Jetpack settings.
- Cloudflare/WAF blocking writes: Whitelist Wiro's outbound IPs (contact support) or allow
/wp-json/wp/v2/postsendpoints.
Related
App Store Connect Integration
Connect your agent to App Store Connect for review monitoring, metadata management, and in-app events.
Overview
The App Store Connect integration uses ES256-signed JWT authentication with App Store Connect API keys.
Skills that use this integration:
appstore-reviews— Monitor and reply to App Store reviewsappstore-metadata— Read/update app metadata, localizations, screenshotsappstore-events— Create and manage in-app events
Agents that typically enable this integration:
- App Review Support
- App Event Manager
- Meta Ads Manager (uses a simpler
appsarray shape — see below)
Availability
| Mode | Status | Notes |
|---|---|---|
| ES256 API Key | Available | Standard App Store Connect API keys. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- App Store Connect Admin access — only Admins can generate API keys.
Setup
Step 1: Create an API key
- Sign in to App Store Connect.
- Users and Access → Integrations → App Store Connect API.
- Click + to generate a new key.
-
Name (e.g. "Wiro agent") and role:
- Admin or App Manager for full capability
- Customer Support for reviews-only
- Download the
.p8file — only downloadable once. - Copy the Key ID (10-char like
ABC1234DEF) and Issuer ID (UUID at top).
Step 2: Base64-encode the private key
# Linux
base64 -w 0 AuthKey_ABC1234DEF.p8 > appstore-key.b64
# macOS
base64 -b 0 AuthKey_ABC1234DEF.p8 > appstore-key.b64
Step 3: Save to Wiro
The credential schema depends on which agent you're configuring. There are two valid shapes.
Shape A: Flat (review/events/metadata agents)
Used by App Review Support and App Event Manager:
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"appstore": {
"keyId": "ABC1234DEF",
"issuerId": "12345678-1234-1234-1234-123456789012",
"privateKeyBase64": "LS0tLS1CRUdJTi...",
"appIds": ["6479306352", "1234567890"],
"supportEmail": "[email protected]"
}
}
}
}'
| Field | Type | Description |
|---|---|---|
keyId |
string | 10-character App Store Connect Key ID. |
issuerId |
string | UUID issuer ID. |
privateKeyBase64 |
string | Base64-encoded .p8 private key. |
appIds |
string[] | App Store IDs the agent is scoped to. |
supportEmail |
string | Used when replying to reviews. |
Shape B: apps array (ads-manager agents)
Used by Meta Ads Manager and Google Ads Manager (for cross-platform creatives that reference app listings):
{
"credentials": {
"appstore": {
"apps": [
{
"appName": "My iOS App",
"appId": "6479306352"
}
]
}
}
}
Each app: { appName, appId }. No keyId/issuerId/privateKey — the ads agents only need the app listing IDs for attribution, not API access.
Step 4: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Runtime Behavior
Env vars are exported only when appstore-reviews OR appstore-events skill is enabled (not appstore-metadata alone):
APPSTORE_KEY_ID←credentials.appstore.keyIdAPPSTORE_ISSUER_ID←credentials.appstore.issuerIdAPPSTORE_APP_IDS←appIds.join(",")APPSTORE_SUPPORT_EMAIL←supportEmail
Secret file:
/run/secrets/appstore-key.p8— decoded private key (file, not env var)
Auth: JWT ES256 signed in-agent → Authorization: Bearer <TOKEN>.
Base URL: https://api.appstoreconnect.apple.com/v1/....
For ads-manager agents (Shape B), the apps array is serialized into METAADS_APPSTORE_APPS or GADS_APPSTORE_APPS env vars as JSON arrays.
Troubleshooting
- 401 Unauthorized on API: Wrong Key ID or Issuer ID, or base64 corrupted the
.p8key. Re-export and re-encode. Verify no newlines or whitespace were introduced. -
Key ID
NOT_ENABLED: The key was revoked. Generate a new one. - Reviews not appearing: Role of the API key lacks Customer Support permissions. Regenerate with a role that includes review access.
- Metadata updates fail: Role lacks Admin or App Manager permissions for the app in question.
Related
Google Play Integration
Connect your agent to the Google Play Developer API for review monitoring and app listing management.
Overview
The Google Play integration uses a Google Cloud service account with API access delegated from a Play Console project.
Skills that use this integration:
googleplay-reviews— Monitor and reply to Google Play reviewsgoogleplay-metadata— Read/update app listings and metadata
Agents that typically enable this integration:
- App Review Support
- Meta Ads Manager (uses the simpler
appsarray shape)
Availability
| Mode | Status | Notes |
|---|---|---|
| Service Account JSON | Available | Google Cloud service account with Play Developer Reporting access. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Google Play Console account with Admin access.
- A Google Cloud project to host the service account.
Setup
Step 1: Enable the Google Play Android Developer API
Google Cloud Console → select project → APIs & Services → Library → Google Play Android Developer API → Enable.
Step 2: Create a service account
- IAM & Admin → Service accounts → Create service account.
- Name (e.g. "wiro-play-agent").
- Grant role:
Service Account Token Creator. - Skip user permissions → Done.
- Open the service account → Keys → Add key → Create new key → JSON. Download.
Step 3: Link the service account to Play Console
- Google Play Console → Users and permissions → Invite new users.
- Email: the service account email (
[email protected]). - Grant at least: View app information, Reply to reviews, and any others needed by your agent.
- Send invite — Play Console auto-accepts for service accounts.
Step 4: Base64-encode the JSON
# Linux
base64 -w 0 play-service-account.json > play-sa.b64
# macOS
base64 -b 0 play-service-account.json > play-sa.b64
Step 5: Save to Wiro
Same two-shape approach as App Store Connect.
Shape A: Flat (reviews/metadata agents)
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"googleplay": {
"serviceAccountJsonBase64": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii...",
"packageNames": ["com.example.app"],
"supportEmail": "[email protected]"
}
}
}
}'
| Field | Type | Description |
|---|---|---|
serviceAccountJsonBase64 |
string | Base64-encoded JSON. |
packageNames |
string[] | Android package names the agent is scoped to. |
supportEmail |
string | Used when replying to reviews. |
Shape B: apps array (ads-manager agents)
{
"credentials": {
"googleplay": {
"apps": [
{
"appName": "My Android App",
"packageName": "com.example.app"
}
]
}
}
}
Step 6: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Runtime Behavior
Env vars exported only when googleplay-reviews skill is enabled (not metadata alone):
GOOGLE_PLAY_PACKAGE_NAMES←packageNames.join(",")GOOGLE_PLAY_SUPPORT_EMAIL←supportEmail
Secret file:
/run/secrets/gplay-sa.json— decoded service account (file, not env)
Auth: OAuth access token minted from the service account → Authorization: Bearer.
Base URL: https://androidpublisher.googleapis.com/androidpublisher/v3/....
For ads agents (Shape B): apps serialized into GADS_GPLAY_APPS or METAADS_GPLAY_APPS env vars as JSON.
Troubleshooting
- 403 "The caller does not have permission": Service account is in Google Cloud but hasn't been invited in Play Console, or lacks the required permission. Return to Play Console → Users and permissions and adjust.
- "Invalid JWT token": Service account JSON corrupt or truncated. Re-encode.
- Review reply fails silently: Some reviews are >1 year old and can't be replied to via API — Google enforces this at the platform level.
Related
Apollo.io Integration
Connect your agent to Apollo.io for lead generation, prospecting, and email sequence enrollment.
Overview
The Apollo integration uses Apollo's x-api-key header authentication.
Skills that use this integration:
apollo-sales— Lead search, enrichment, sequence enrollment, reply handling
Agents that typically enable this integration:
- Lead Generation Manager
Availability
| Mode | Status | Notes |
|---|---|---|
| API Key | Available | Apollo REST API keys. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- An Apollo.io account on a plan that includes API access.
Setup
Step 1: Get an Apollo API key
- app.apollo.io → Settings → Integrations → API (or Account settings → API keys).
- Create new key, name "Wiro agent", copy value.
Step 2 (optional): Get a Master API Key
Some Apollo plans require a separate Master API Key for the mixed_people/api_search endpoint (people search) and for sequence management. Find it in Admin → API keys (workspace admins only). Enrichment, lookups, and basic read operations use the standard apiKey.
Step 3: Save to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"apollo": {
"apiKey": "YOUR_APOLLO_API_KEY",
"masterApiKey": "YOUR_APOLLO_MASTER_API_KEY"
}
}
}
}'
masterApiKey is optional — omit if your agent only does enrichment and lookups. Required for people search (mixed_people/api_search) and sequence management.
Step 4: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Credential Fields
| Field | Type | Description |
|---|---|---|
apiKey |
string | Primary Apollo API key. |
masterApiKey |
string (optional) | Master API key for people search + sequence management. |
Credentials schema (as returned by POST /UserAgent/Detail)
"apollo": {
"optional": true,
"apiKey": "",
"masterApiKey": "",
"_editable": {
"apiKey": true,
"masterApiKey": true
}
}
Runtime Behavior
Env vars (exported only when apollo-sales skill is enabled and apiKey is set):
APOLLO_API_KEY←credentials.apollo.apiKeyAPOLLO_MASTER_KEY←credentials.apollo.masterApiKey(only if set)
Endpoint-based key selection — the apollo-sales skill picks the header per endpoint:
| Endpoint group | Header | Env var |
|---|---|---|
People Search (POST /mixed_people/api_search) |
x-api-key: $APOLLO_MASTER_KEY |
Requires masterApiKey
|
| Sequence management (create sequence, add contacts, start/pause) | x-api-key: $APOLLO_MASTER_KEY |
Requires masterApiKey
|
People enrichment (POST /people/match) |
x-api-key: $APOLLO_API_KEY |
apiKey sufficient |
| Organization lookup | x-api-key: $APOLLO_API_KEY |
apiKey sufficient |
| Email verification | x-api-key: $APOLLO_API_KEY |
apiKey sufficient |
Base URL: https://api.apollo.io/api/v1.
Rate limits: Apollo enforces strict per-key limits; 429 responses require 60s backoff.
If masterApiKey is missing: People search and sequence endpoints return 401 Unauthorized with "error": "Invalid Api Key". This is expected — even though apiKey works for other endpoints, Apollo's master-only endpoints reject the regular key. Add masterApiKey via POST /UserAgent/Update and the same agent can immediately use master-only endpoints (no restart needed if the cron
picks up env changes on next run).
Troubleshooting
- 403 Forbidden: Plan doesn't include API access. Upgrade to Professional tier or higher.
- 429 Too Many Requests: Rate limit hit. Space prospecting runs or request higher tier from Apollo support.
-
401 on
mixed_people/api_search: MissingmasterApiKey. Add it (workspace admins: Apollo → Admin → API keys). - Sequence enrollment fails: Missing
masterApiKey. Same fix — master key is required for write operations on sequences.
Related
Lemlist Integration
Connect your agent to Lemlist for cold email outreach and campaign orchestration.
Overview
The Lemlist integration uses HTTP Basic Authentication with an empty username and the API key as the password.
Skills that use this integration:
lemlist-outreach— Campaign creation, lead uploads, pause/resume
Agents that typically enable this integration:
- Lead Generation Manager
Availability
| Mode | Status | Notes |
|---|---|---|
| API Key | Available | Lemlist API key (Gold tier or higher). |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Lemlist account on Gold tier or higher for full API access.
Setup
Step 1: Get an API key
- app.lemlist.com → Settings → Integrations → API.
- Click Generate (or copy existing). Keys look like
AbCdEfGhIjKlMnOp.
Step 2: Save to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"lemlist": {
"apiKey": "YOUR_LEMLIST_API_KEY"
}
}
}
}'
Step 3: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Credential Fields
| Field | Type | Description |
|---|---|---|
apiKey |
string | Lemlist API key. |
Credentials schema (as returned by POST /UserAgent/Detail)
"lemlist": {
"optional": true,
"apiKey": "",
"_editable": { "apiKey": true }
}
Runtime Behavior
Env vars (exported only when lemlist-outreach skill is enabled and apiKey is set):
LEMLIST_API_KEY←credentials.lemlist.apiKey
Auth: Basic auth with empty username — --user ":$LEMLIST_API_KEY". (Lemlist treats the API key as the password, with no username.)
Base URL: https://api.lemlist.com/api.
Rate limits: ~20 requests per 2 seconds; 429 requires backoff.
Troubleshooting
- 401 Unauthorized: API key revoked in Lemlist settings. Regenerate.
- 403 on campaign operations: Plan tier lacks API write access. Upgrade.
- Email address not found: Lead must exist in at least one campaign before some endpoints work — upload leads first.
Related
Brevo Integration
Connect your agent to Brevo (formerly Sendinblue) for transactional and marketing email.
Overview
The Brevo integration uses Brevo API v3 with an api-key header (not Bearer).
Skills that use this integration:
brevo-email— Campaign, template, and contact management via Brevo API v3newsletter-compose— uses Brevo as the ESP when enabled alongside
Other: Custom agents can call the Brevo API via whatever skill invokes BREVO_API_KEY.
Agents that typically enable this integration:
- Newsletter Manager
Availability
| Mode | Status | Notes |
|---|---|---|
| API Key (v3) | Available | Standard Brevo API v3 keys. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Brevo account (free tier works for low volume).
Setup
Step 1: Get an API key
- app.brevo.com → profile (top right) → SMTP & API → API Keys.
- Generate a new API key, name "Wiro agent".
- Copy the key (starts with
xkeysib-).
Step 2: Save to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"brevo": {
"apiKey": "xkeysib-xxxxxxxxxxxxxxxxxxxx"
}
}
}
}'
Step 3: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Credential Fields
| Field | Type | Description |
|---|---|---|
apiKey |
string | Brevo v3 API key (starts with xkeysib-). |
Credentials schema (as returned by POST /UserAgent/Detail)
"brevo": {
"optional": true,
"apiKey": "",
"_editable": { "apiKey": true }
}
Runtime Behavior
Env vars (exported only when brevo-email skill is enabled and apiKey is set):
BREVO_API_KEY←credentials.brevo.apiKey
Auth: Header api-key: $BREVO_API_KEY — not Bearer. This is Brevo's documented auth pattern.
Base URL: https://api.brevo.com/v3/.
Rate limits: ~10 req/s on free plan; higher on paid tiers.
Troubleshooting
- 401 Unauthorized: Key revoked or deleted. Generate a new one.
- Emails go to spam: Verify sending domain under Brevo → Senders & IP → Domains. Set up SPF, DKIM, DMARC.
- Rate limit (429): Free tier is 300 emails/day. Upgrade plan.
Related
SendGrid Integration
Connect your agent to Twilio SendGrid for transactional and marketing email delivery.
Overview
The SendGrid integration uses standard Bearer authentication with a SendGrid API key.
Skills that use this integration:
sendgrid-email— Marketing and transactional email via SendGrid v3 APInewsletter-compose— uses SendGrid as the ESP when enabled alongside
Other: Custom agents can call the SendGrid API via whatever skill invokes SENDGRID_API_KEY.
Agents that typically enable this integration:
- Newsletter Manager
Availability
| Mode | Status | Notes |
|---|---|---|
| API Key | Available | Twilio SendGrid API keys. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A SendGrid account.
Setup
Step 1: Create an API key
- app.sendgrid.com → Settings → API Keys → Create API Key.
- Name "Wiro agent".
-
Permissions:
- Full Access for maximum capability, or
- Restricted Access + enable at least Mail Send (and Marketing Campaigns if used).
- Create & View, copy the key once (starts with
SG.) — cannot be retrieved later.
Step 2: Save to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/Update" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"guid": "your-useragent-guid",
"configuration": {
"credentials": {
"sendgrid": {
"apiKey": "SG.xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
}
}
}
}'
Step 3: Start the agent
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
Credential Fields
| Field | Type | Description |
|---|---|---|
apiKey |
string | SendGrid API key (starts with SG.). |
Credentials schema (as returned by POST /UserAgent/Detail)
"sendgrid": {
"optional": true,
"apiKey": "",
"_editable": { "apiKey": true }
}
Runtime Behavior
Env vars (exported only when sendgrid-email skill is enabled and apiKey is set):
SENDGRID_API_KEY←credentials.sendgrid.apiKey
Auth: Authorization: Bearer $SENDGRID_API_KEY.
Base URL: https://api.sendgrid.com/v3.
Rate limits: 600 req/min on most endpoints; higher on marketing endpoints.
Troubleshooting
- 401 Unauthorized: Key deleted or permissions changed. Create a new key with appropriate scopes.
- 403 Forbidden on send: Sender identity not verified. In SendGrid → Settings → Sender Authentication, verify single sender or domain.
- Emails flagged as spam: Complete Domain Authentication (SPF + DKIM + DMARC) in SendGrid sender settings.
Related
Agent Use Cases
Build products with autonomous AI agents using the Wiro API.
Two Deployment Patterns
Every product built on Wiro agents follows one of two patterns. Choosing the right one depends on whether your users need to connect their own third-party accounts.
Pattern 1: Instance Per Customer
Most agents interact with external services — posting to social media, managing ad campaigns, sending emails. These require OAuth tokens or API keys that belong to the end user. Deploy a separate agent instance for each of your customers.
Why: Each customer connects their own accounts. Credentials are bound to the instance, isolated from other customers.
How: Call POST /UserAgent/Deploy once per customer, then use the OAuth flow to connect their accounts.
Real-World Examples
| Your Product | Agent Type | Why Per-Customer |
|---|---|---|
| Digital marketing agency dashboard | Social Manager | Each client connects their own Twitter, Instagram, Facebook, TikTok, LinkedIn |
| Mobile app company | App Review Support | Each app has its own App Store / Google Play credentials |
| E-commerce platform | Google Ads Manager + Meta Ads Manager | Each advertiser connects their own ad accounts |
| Marketing SaaS | Newsletter Manager | Each customer connects their own Brevo/SendGrid/Mailchimp |
| Sales platform | Lead Generation Manager | Each sales team connects their own Apollo/Lemlist |
| Content agency tool | Blog Content Editor | Each client connects their own WordPress site |
| App publisher platform | App Event Manager | Each app has its own App Store credentials |
| Mobile app publisher | Push Notification Manager | Each app has its own Firebase service account |
| Customer engagement tool | Social Manager | Each brand manages their own social presence |
Pattern 2: Session Per User
For conversational agents that don't need per-user credentials. One agent instance serves many users, each identified by a unique sessionkey that isolates their conversation history.
Why: No third-party accounts to connect. The agent answers questions using its built-in knowledge or pre-configured data sources.
How: Deploy one instance via POST /UserAgent/Deploy, then send messages with different sessionkey values per user.
About sessionkey: This field is optional — if omitted, messages go into a shared "default" session. Reuse the same sessionkey to continue a conversation (the agent retrieves prior messages as context). Use a fresh UUID to start a new conversation. The common convention is one sessionkey per end-user (e.g. "user-456"), but you can also use it per-thread (e.g. "ticket-2025-0817")
for products that group conversations by topic. See the Agent Messaging guide for how sessions work.
Real-World Examples
| Your Product | Use Case | Why Sessions |
|---|---|---|
| Knowledge base chatbot | Answer questions from documentation | No per-user credentials needed |
| Product recommendation advisor | Suggest products based on conversation | Same catalog for all users |
| Internal company assistant | HR policies, IT help, onboarding | Shared knowledge base |
| Customer support bot | Handle common support questions | No external service connections |
When to Use Which
| Question | Instance Per Customer | Session Per User |
|---|---|---|
| Does each user connect their own social/ad/email accounts? | Yes | No |
| Do credentials differ between users? | Yes | No |
| Is conversation the primary interaction? | Sometimes | Always |
| Does the agent perform actions on behalf of the user? | Yes | Rarely |
| How many instances do you need? | One per customer | One total (or a few) |
Hybrid Pattern
A single product can combine both — a per-customer action agent plus a shared conversational agent sit side by side in the same frontend. For example:
- One Social Manager instance per customer (Pattern 1) to publish posts with their own OAuth-connected accounts.
- One shared knowledge-base chat agent (Pattern 2) that answers product, billing, or onboarding questions across all customers, using
sessionkeyto keep each user's chat separate.
The two agents are independent deployments with different useragentguid values. Route user actions to the per-customer instance, and chat questions to the shared instance. Your backend decides which agent handles each request.
Building Your Product
White-Label Chat
Build a fully branded chat experience with no Wiro UI visible to your users.
The deploy flow has three stages — Deploy, Setup (only when the agent needs third-party credentials), and Start:
- Deploy an agent via
POST /UserAgent/Deploy— returnsuseragents[0].guidand starts the instance instatus: 6(setup-required) orstatus: 0(ready) depending on whether the template needs credentials. -
Setup the credentials (only if the agent template requires them) via one or more
POST /UserAgent/Updatecalls and the matching OAuth flows — see Agent Credentials & OAuth. Checksetuprequiredon the response: while it'strue, the agent cannot start. Once all required credential slots are filled,setuprequiredflips tofalseandstatusbecomes0. For Pattern 2 (knowledge-base / chat-only agents) there are no third-party credentials, so this stage is skipped and the agent is ready to start right after Deploy. - Start the agent with
POST /UserAgent/Start— transitions throughstatus: 3(starting) →status: 4(running). - Build your own chat UI.
- Send messages via
POST /UserAgent/Message/Send. - Stream responses in real-time via Agent WebSocket using the
agenttoken. - Manage conversation history with
POST /UserAgent/Message/History.
See Agent Overview for the full lifecycle state table (status: 0, 1, 3, 4, 5, 6).
# Deploy — prepaid + plan are required for API users (credits debit from your wallet)
curl -X POST "https://api.wiro.ai/v1/UserAgent/Deploy" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"agentguid": "agent-template-guid",
"title": "Customer Support Bot",
"useprepaid": true,
"plan": "starter"
}'
# Pattern 1 only — fill any missing credentials, then verify setuprequired flipped to false
curl -X POST "https://api.wiro.ai/v1/UserAgent/Detail" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "deployed-useragent-guid" }'
# Start the agent (skipped if Deploy already returned status: 4 for cheap chat-only templates)
curl -X POST "https://api.wiro.ai/v1/UserAgent/Start" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "deployed-useragent-guid" }'
# Send a message
curl -X POST "https://api.wiro.ai/v1/UserAgent/Message/Send" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "deployed-useragent-guid",
"message": "How do I reset my password?",
"sessionkey": "user-456"
}'
Webhook-Driven Pipelines
For backend-to-backend integrations where you don't need real-time streaming.
- Send a message with a
callbackurl - Continue processing other work
- Receive the agent's response via HTTP POST to your webhook endpoint
- Chain the result into your next workflow step
See Agent Webhooks for payload format and retry policy.
Scheduled Automation
Combine agents with cron jobs for recurring tasks.
Cron (every Monday 9am)
→ POST /UserAgent/Message/Send (with callbackurl)
→ Agent processes the task
→ Webhook fires to your server
→ Your server emails the report / posts to Slack / updates dashboard
This pattern works well for weekly social media content planning, daily ad performance reviews, monthly newsletter generation, and automated lead enrichment pipelines.
Multi-Agent Orchestration
Deploy multiple specialized agents and coordinate them from your backend.
Your Backend
├── Research Agent → "Find trending topics in AI this week"
│ ↓ webhook response
├── Writing Agent → "Write a blog post about: {research results}"
│ ↓ webhook response
└── Publishing Agent → "Publish this post to WordPress and share on social media"
Each agent is an independent instance with its own credentials. Your backend passes output from one agent as input to the next.
Available Agents
Wiro provides pre-built agent templates you can deploy immediately. Each agent specializes in a specific domain and comes with the relevant skills and credential slots pre-configured.
| Agent | What It Does | Credentials |
|---|---|---|
| Social Manager | Create, schedule, and publish social media content | Twitter/X, Instagram, Facebook, TikTok, LinkedIn (OAuth) |
| Blog Content Editor | Write and publish blog posts (WordPress draft + publish workflow) | WordPress (App Password), Gmail (optional, for inbox requests) |
| Google Ads Manager | Create and optimize Google Ads campaigns, daily performance reports | Google Ads (OAuth), Calendarific (platform-managed), Google Drive (optional) |
| Meta Ads Manager | Manage Facebook and Instagram ad campaigns, audience analysis | Meta Ads (OAuth), Calendarific (platform-managed), Google Drive (optional) |
| Newsletter Manager | Design and send email newsletters to subscriber lists | Brevo, SendGrid, Mailchimp, HubSpot (any one — API key or OAuth) |
| Lead Generation Manager | Find and enrich leads, run multi-channel outreach, analyze replies | Apollo (API key), Lemlist (API key), HubSpot (optional, for CRM sync) |
| App Review Support | Monitor app store reviews, draft responses in operator's tone | App Store Connect (private key JWT), Google Play (service account) |
| App Event Manager | Scan global holidays, suggest and create App Store in-app events | App Store Connect (JWT), Calendarific (platform-managed) |
| Push Notification Manager | Craft locale- and timezone-aware push notifications, queue dispatch | Firebase (service account JSON per app), Calendarific (platform-managed) |
The list above matches the 9 agent templates currently deployed in production. The exact set can evolve over time; fetch POST /Agent/List for the live catalog.
Deploying an Agent
import requests
headers = {
"x-api-key": "YOUR_API_KEY",
"Content-Type": "application/json"
}
# List available agents
agents = requests.post(
"https://api.wiro.ai/v1/Agent/List",
headers=headers,
json={}
)
print(agents.json())
# Deploy an instance (API users always use prepaid — credits debit from your wallet)
deploy = requests.post(
"https://api.wiro.ai/v1/UserAgent/Deploy",
headers=headers,
json={
"agentguid": "social-manager-agent-guid",
"title": "Acme Corp Social Media",
"useprepaid": True,
"plan": "starter"
}
)
useragent_guid = deploy.json()["useragents"][0]["guid"]
# Connect Twitter via OAuth
connect = requests.post(
"https://api.wiro.ai/v1/UserAgentOAuth/XConnect",
headers=headers,
json={
"userAgentGuid": useragent_guid,
"redirectUrl": "https://your-app.com/settings?connected=twitter"
}
)
authorize_url = connect.json()["authorizeUrl"]
# Start the agent
requests.post(
"https://api.wiro.ai/v1/UserAgent/Start",
headers=headers,
json={"guid": useragent_guid}
)
# Send a message
message = requests.post(
"https://api.wiro.ai/v1/UserAgent/Message/Send",
headers=headers,
json={
"useragentguid": useragent_guid,
"message": "Create a thread about our new product launch",
"sessionkey": "campaign-q2"
}
)
print(message.json())
Browse available agents and their capabilities at Agent/List or in the Wiro dashboard.
Organizations & Teams
Collaborate with your team under a shared workspace with unified billing, access controls, and resource management.
Overview
Wiro supports three workspace contexts for organizing your resources:
- Personal — your default workspace. Projects, agents, and wallet are tied to your individual account.
- Organization — a parent entity that groups one or more teams. The organization owner controls the lifecycle of teams and their members.
- Team — a workspace under an organization with its own wallet, projects, agents, and member permissions. Team members share access to resources deployed within the team.
Personal Account
├── Personal Projects
├── Personal Agents
└── Personal Wallet
Organization (created by you)
├── Team A
│ ├── Team Wallet
│ ├── Team Projects
│ ├── Team Agents
│ └── Members (owner, admins, members)
├── Team B
│ ├── Team Wallet
│ ├── Team Projects
│ ├── Team Agents
│ └── Members
└── ...
Every user always has a personal workspace. Organizations and teams are optional — you can use Wiro entirely in personal mode without ever creating an organization.
Key Concepts
Workspaces and Context
When you make an API request or use the dashboard, you operate in one of two contexts:
| Context | Resources you see | Wallet charged | How to activate |
|---|---|---|---|
| Personal | Your personal projects, agents, tasks | Your personal wallet | Default — use a personal project API key |
| Team | Team projects, team agents, team tasks | Team wallet | Use a team project API key |
Switching context changes which projects, agents, and wallet you interact with. Resources in one context are isolated from the other — personal agents cannot see team projects, and team agents cannot access personal resources.
Resource Isolation
Each workspace is fully isolated:
- Projects belong to either your personal workspace or a specific team. A project's API key automatically resolves the correct context.
- Agents are deployed into a workspace. Team agents are visible to all team members; personal agents are visible only to you.
- Wallet transactions are recorded against the workspace that initiated them. Team tasks deduct from the team wallet; personal tasks deduct from your personal wallet.
- Tasks are tagged with the workspace context and only appear in the matching project usage and statistics views.
Transferring Resources
Projects and agents can be transferred between workspaces:
- Personal → Team — move a project or agent from your personal workspace into a team you have admin access to
- Team → Personal — move a project or agent from a team back to your personal workspace
- Team → Team — move a project or agent between teams you have admin access to
When a resource is transferred, its billing context changes immediately. Future tasks on a transferred project will be billed to the new workspace's wallet.
Important: Agents can only access projects in the same workspace. If you transfer a project out of a team, agents in that team can no longer use it.
Organizations vs Teams
An organization is a management container — it does not hold resources directly. All resources (projects, agents, wallets) live inside teams.
| Feature | Organization | Team |
|---|---|---|
| Holds projects and agents | No | Yes |
| Has a wallet | No | Yes |
| Has members | No (members belong to teams) | Yes |
| Can be created by | Any user | Organization owner |
| Can be deleted by | Organization owner | Organization owner |
| Can be restored | Yes (by owner) | Yes (when org is restored) |
Roles
| Role | Scope | Permissions |
|---|---|---|
| Owner | Organization | Create/delete teams, manage all team members, delete/restore organization, transfer agents and projects |
| Admin | Team | Manage team settings (spend limits, model access), invite/remove members, transfer agents and projects |
| Member | Team | Use team resources (run models, send agent messages), view spending summaries |
Getting Started
- Create an organization — go to your Dashboard and click "Create Organization"
- Create a team — inside the organization, create a team with a name
- Invite members — send email invitations to your teammates
- Fund the team wallet — deposit credits or redeem coupons in the team context
- Create projects — create API projects within the team to start running models
- Deploy agents — deploy agent instances within the team for shared access
For step-by-step instructions, see Managing Teams.
What's Next
- Managing Teams — Create organizations, invite members, manage roles and permissions
- Team Billing & Spending — Wallets, spend limits, model access controls, and budget alerts
- Team API Access — How workspace context works with API keys and context guards
Managing Teams
Create organizations, invite members, and manage roles and permissions.
POST /Organization/Create
Creates a new organization. The caller automatically becomes the organization owner — only the owner can create teams, delete the organization, or restore it after deletion.
| Parameter | Type | Required | Description |
|---|---|---|---|
name |
string | Yes | Organization name |
You can also create organizations from the Dashboard.
POST /Team/Create
Creates a team inside an organization. Only the organization owner can create teams. The team is created with its own wallet (starting at $0.00) and the caller is automatically added as an admin.
| Parameter | Type | Required | Description |
|---|---|---|---|
organizationguid |
string | Yes | Organization guid |
name |
string | Yes | Team name |
POST /Team/Member/Invite
Sends an email invitation to add a new member to the team. Invitations expire after 7 days and can be resent. Organization owners and team admins can invite members.
| Parameter | Type | Required | Description |
|---|---|---|---|
teamguid |
string | Yes | Team guid |
email |
string | Yes | Invitee email address |
role |
string | Yes | Role: "admin" or "member" |
Invitation States
| Status | Description |
|---|---|
pending |
Invitation sent, waiting for the user to accept |
active |
User accepted the invitation and is an active member |
removed |
Member was removed or invitation was cancelled |
Member Roles
| Role | Run models | Message agents | View spending | Manage settings | Invite members | Delete team |
|---|---|---|---|---|---|---|
| Owner | Yes | Yes | Yes | Yes | Yes | Yes |
| Admin | Yes | Yes | Yes | Yes | Yes | No |
| Member | Yes | Yes | Yes | No | No | No |
POST /Team/TransferAgent
Transfers an agent instance between workspaces — personal to team, team to personal, or team to team. Active subscriptions and credit purchases move with the agent. The agent is restarted with the new context.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | Agent instance guid |
targetteamguid |
string | Yes | Target team guid, or empty string "" for personal |
POST /Team/TransferProject
Transfers a project between workspaces. Future tasks on the project are billed to the new workspace's wallet.
| Parameter | Type | Required | Description |
|---|---|---|---|
projectapikey |
string | Yes | Project API key |
targetteamguid |
string | Yes | Target team guid, or empty string "" for personal |
Important: Agents can only access projects in the same workspace. Transferring a project may break agent workflows that depend on it.
POST /Organization/Restore
Restores a soft-deleted organization. Only the organization owner can delete or restore organizations.
Deleting an organization transfers all its teams' agents and projects to the owner's personal workspace. Restoring reactivates the organization, all its teams, and previously accepted members.
What's Next
- Organizations & Teams Overview — Concepts and workspace hierarchy
- Team Billing & Spending — Wallets, spend limits, and model access controls
- Team API Access — How context works in API requests
Team Billing & Spending
Manage team wallets, set spend limits, control model access, and track usage across members.
Team Wallets
Each team has its own wallet, independent of members' personal wallets. When a task runs in a team context, the cost is deducted from the team wallet — never from the individual member's personal wallet.
Team wallets are funded the same way as personal wallets: deposits, coupons, and auto-pay. Switch to the team context in the dashboard and navigate to Wallet.
Spend Limits
| Limit Type | Set by | Applies to | Effect when reached |
|---|---|---|---|
| Team spend limit | Admin / Owner | Entire team | All tasks rejected for all members |
| Member spend limit | Admin / Owner | Individual member | Tasks rejected for that member only |
When a team's total spending reaches 80% of the team spend limit, admins receive an email alert.
POST /Team/Update
Updates team settings, including model access controls. Team admins can restrict which AI models team members are allowed to run by setting modelaccess to one of three modes.
| Parameter | Type | Required | Description |
|---|---|---|---|
teamguid |
string | Yes | Team guid |
modelaccess |
string | No | Access mode: "all", "allowlist", or "blocklist". Default: "all" |
allowedmodelids |
array | No | List of model IDs that are allowed. Used when modelaccess is "allowlist". |
blockedmodelids |
array | No | List of model IDs that are blocked. Used when modelaccess is "blocklist". |
Access Modes
| Mode | modelaccess value |
Behavior |
|---|---|---|
| All Models | "all" |
No restrictions. Team members can run any model. This is the default. |
| Allowlist | "allowlist" |
Only models in allowedmodelids can be run. All others are blocked. |
| Blocklist | "blocklist" |
Models in blockedmodelids cannot be run. All others are allowed. |
You configure one mode at a time. Setting modelaccess back to "all" removes all restrictions.
Where Access Controls Are Enforced
Model access is checked at the /Run endpoint — when a team member submits a task using a team project API key. Access controls do not affect browsing the model catalog or personal projects.
Error Response
{
"result": false,
"errors": [
{
"code": 0,
"message": "This model is not allowed in your team. Contact your team admin."
}
]
}
POST /Team/SpendingSummary
Returns team totals, your individual spending, and limit information. All team members can view the spending summary.
Response
{
"result": true,
"teamTotal": 45.23,
"playgroundTotal": 32.10,
"apiTotal": 13.13,
"memberSpent": {
"total": 12.50,
"playground": 8.30,
"api": 4.20
},
"spendLimit": 500.00,
"memberSpendLimit": 100.00
}
POST /Team/TransferCredit
Transfers credit between your personal wallet and team wallets. Useful for moving team budgets around or recovering personal funds. Only organization owners and team admins can transfer credit, and the same user must control both source and target workspaces.
| Parameter | Type | Required | Description |
|---|---|---|---|
amount |
number | Yes | Transfer amount in USD |
sourceteamguid |
string | No | Source team guid. Empty/omit for personal wallet |
targetteamguid |
string | No | Target team guid. Empty/omit for personal wallet |
Response
{
"result": true,
"errors": [],
"transferred": {
"total": 100,
"gifted": 50,
"store": 0,
"amount": 50
}
}
How It Works
Transfers preserve the original deposit structure — expiry dates, coupon tracking, and store revenue are all maintained. Each deposit type is transferred as a separate transaction on the target wallet with its original expiry time.
Consumption order (matches task billing):
- Tracked coupons (model-specific first, then universal, FIFO)
- Untracked gifted (checklist rewards, pooled)
- Store revenue
- Regular amount (deposits)
When transferring a mixed amount, the target wallet receives multiple separate deposits — one per pool — each with its own expiry date.
Transaction History
Both wallets receive audit transactions: TRANSFER OUT on source, TRANSFER IN on target. These are for display only and don't affect balance calculations or expiry.
Important Behaviors
- Auto-pay may trigger if transferring reduces your personal
wallet.amountbelow the threshold. - Agent subscriptions may fail renewal if transferring leaves insufficient balance.
- Expired deposits are not transferred — only active deposits.
- Partial transfers preserve FIFO — original deposit amount is reduced, expiry works correctly.
Coupons
| Coupon Scope | Who can redeem | Wallet credited |
|---|---|---|
| Everyone | Any user | The redeemer's active wallet (personal or team) |
| Team | Only members of the specified team | The team wallet |
| User | Only the specified user | The user's personal wallet |
What's Next
- Organizations & Teams Overview — Concepts and workspace hierarchy
- Managing Teams — Create organizations, invite members, manage roles
- Team API Access — How context works in API requests
- Pricing — General pricing information
Team API Access
How workspace context is resolved in API requests, and how access controls protect cross-context operations.
Context Resolution
Every authenticated API request resolves to a workspace context — either personal or a specific team.
The context is determined automatically by the project's assignment. You do not need to send any additional headers — the API key carries the context implicitly.
# Team project API key — team context is automatic
curl -X POST "https://api.wiro.ai/v1/Run/google/nano-banana" \
-H "x-api-key: YOUR_TEAM_PROJECT_API_KEY" \
-d '{"prompt": "Hello"}'
# Personal project API key — personal context is automatic
curl -X POST "https://api.wiro.ai/v1/Run/google/nano-banana" \
-H "x-api-key: YOUR_PERSONAL_API_KEY" \
-d '{"prompt": "Hello"}'
Create a project inside a team to get a team API key, or use a personal project for personal context. The same x-api-key header works for both — no extra configuration needed.
What Gets Filtered by Context
| Endpoint | Personal context returns | Team context returns |
|---|---|---|
Project/List |
Personal projects only | Team projects only |
UserAgent/MyAgents |
Personal agents only | Team agents only |
Task/List |
Personal tasks only | Team tasks only |
Task/Stat |
Personal task statistics | Team task statistics |
Wallet/List |
Personal wallet | Team wallet |
Wallet/TransactionList |
Personal transactions | Team transactions |
Agent Context Guards
Wiro enforces strict context isolation for agent operations. Your current workspace context must match the agent's workspace:
| Your context | Agent's workspace | Result |
|---|---|---|
| Personal | Personal | Allowed |
| Team A | Team A | Allowed |
| Personal | Team A | Blocked |
| Team A | Personal | Blocked |
| Team A | Team B | Blocked |
Protected Endpoints
Context guards are enforced on: Message/Send, Message/History, Message/Sessions, Message/Delete, Deploy, CreateExtraCreditCheckout, CancelSubscription, RenewSubscription, UpgradePlan.
Error Response
{
"result": false,
"errors": [
{
"code": 0,
"message": "This agent belongs to a team. Switch to the team context to access it."
}
]
}
Wallet Billing Flow
When a task runs in team context:
API Key → Project (teamguid) → Task (teamguid) → Wallet Transaction (uuid=teamguid)
For personal context, teamguid is null and billing uses the user's personal UUID.
Best Practices
- Separate projects by environment — create distinct team projects for development, staging, and production. The team context is resolved automatically from the API key.
- Check agent context before messaging — ensure the project and agent belong to the same workspace
- Transfer resources carefully — agents can only access projects in the same workspace
What's Next
- Organizations & Teams Overview — Concepts and workspace hierarchy
- Managing Teams — Create organizations, invite members, manage roles
- Team Billing & Spending — Wallets, spend limits, and model access controls
- Authentication — API key setup and authentication methods
- Projects — Project management and API credentials