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.
Wiro Agents on the Web
The endpoints below cover the full programmatic surface. If you also want to see the web product alongside what you're building — landing pages, marketing copy, the visual catalog, and the no-code Build Your Own Agent flow — these are the public Wiro pages dedicated to agents:
| Page | URL | What it covers |
|---|---|---|
| AI Agents Home | wiro.ai/agents | Top-level landing — what Wiro Agents are, how they compare to traditional setups, and a featured selection from the marketplace. |
| Learn About Agents | wiro.ai/agents/learn | Concept primer — pricing model, security model, deployment lifecycle, and the difference between marketplace templates and custom builds. |
| Build Your Own Agent | wiro.ai/agents/build | The web wizard for the same custom-build flow exposed by POST /UserAgent/Deploy with custom: true. Useful for previewing the skill picker / pricing breakdown before scripting it. |
| Browse Agents | wiro.ai/browse-agents | Full marketplace catalog with categories, descriptions, screenshots, and per-agent tier prices — the visual mirror of POST /Agent/List. |
All four pages are public — no Wiro account required to browse. Sign-in becomes mandatory only when you click "Deploy" on a specific agent (which then drops you into the Wiro dashboard at wiro.ai/panel/agents).
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, Agent/Detail, Skills/List, Skills/Detail, Skills/Capabilities, Credentials/List, and Credentials/Detail are catalog endpoints and do not require authentication. You can browse available agents, the skill registry, and the credential schema without an API key.
Authenticated endpoints — All UserAgent/* endpoints 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), set them per-provider with
POST /UserAgent/CredentialUpsert, or per-skill withPOST /UserAgent/CustomSkillUpsert. 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 CredentialUpsert / CustomSkillUpsert / SkillsApply 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": [
{
"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"],
"tiermultiplier": 3,
"tiers": {
"starter": { "priceUsd": 9, "credits": 1000 },
"pro": { "priceUsd": 29, "credits": 5000 }
},
"status": 1,
"createdat": "1711929600",
"updatedat": "1714521600"
}
]
}
Agent/List returns only catalog-header columns plus inlined tiers (so the catalog card can render the price without a per-row Agent/Detail round-trip). The id / totalrun / activerun columns are stripped for the public role. Call POST /Agent/Detail with type: "full" when you need the full template (skills map, credential schema, custom skills, scheduled skills).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 also include customskills and scheduledskills arrays in the response. Without type: "full", the response carries the catalog header + tiers + peractioncosts + credentials + skills + skillsmeta only. |
guid or slug. If both are provided, slug takes priority. Pass type: "full" when you need to preview custom skill keys and system-prompt placeholders before deploying; omit it for a lightweight catalog listing.Response
{
"result": true,
"errors": [],
"agents": [
{
"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"],
"tiermultiplier": 3,
"tiers": {
"starter": { "priceUsd": 9, "credits": 1000 },
"pro": { "priceUsd": 29, "credits": 5000 }
},
"peractioncosts": {
"message": 10,
"create": 60,
"modify": 20,
"regenerate": 20
},
"skills": ["int-instagram-post", "int-wiro-generator"],
"skillsmeta": [
{ "name": "int-instagram-post", "title": "Instagram — Post & Stories", "icon": "https://wiro.ai/images/icons/skills/instagram.svg", "category": "int", "credential_key": "instagram" },
{ "name": "int-wiro-generator", "title": "Wiro Generator (platform-managed)", "icon": "https://wiro.ai/images/icons/skills/wiro.svg", "category": "int", "credential_key": "wiro" }
],
"credentials": {
"instagram": {
"optional": false,
"extra": false,
"_editable": { "appid": true, "appsecret": true, "igusername": false },
"_schema": {
"title": "Instagram",
"icon": "https://wiro.ai/images/icons/skills/instagram.svg",
"brand_color": "#e4405f",
"brand_text_color": "#ffffff",
"brand_logo_filter": "none",
"docs_url": "integration-instagram-skills",
"credential_mode": "oauth",
"connection_modes": ["wiro", "own"],
"wiro_connect_pending": true,
"oauth_provider": {
"auth_method_value": "wiro",
"connect_endpoint": "/UserAgentOAuth/IGConnect",
"disconnect_endpoint": "/UserAgentOAuth/IGDisconnect",
"status_endpoint": "/UserAgentOAuth/IGStatus",
"connect_button_label": "Connect with Instagram",
"connect_button_icon": "/images/icons/skills/instagram.svg",
"connect_button_brand_color": "#e4405f",
"connect_button_text_color": "#ffffff",
"connect_button_logo_filter": "none",
"username_field": "igusername",
"return_query_param": "ig_connected",
"return_error_param": "ig_error",
"return_error_detail_param": "ig_error_detail",
"account_picker": null,
"extra_step": null
},
"fields": [
{
"key": "appid",
"type": "text",
"label": "App ID",
"required": true,
"pattern": "^[0-9]+$",
"oauth_managed": true,
"only_in_modes": ["own"]
},
{
"key": "appsecret",
"type": "password",
"label": "App Secret",
"required": true,
"show_toggle": true,
"oauth_managed": true,
"only_in_modes": ["own"]
},
{
"key": "igusername",
"type": "text",
"label": "Connected Account",
"required": false,
"auto_filled_by_oauth": true,
"readonly_when_connected": true
}
]
}
}
},
"extracreditpacks": [],
"status": 1,
"createdat": "1711929600",
"updatedat": "1714521600"
}
]
}
Agent/Detail describes the template, so its credentials block only carries the optional / extra meta flags plus the registry-driven _schema (field labels / types) and _editable map. The _connected flag is per-instance and appears on UserAgent/Detail / UserAgent/MyAgents, not on the catalog endpoint. extracreditpacks is always [] here — packs are derived from the deployed instance's actual monthly allocation and only appear on UserAgent/Detail.Credential response flags (UserAgent/Detail / UserAgent/MyAgents)
Every credential object in the top-level credentials tree on the per-instance endpoints carries:
| Flag | Type | Meaning |
|---|---|---|
_connected |
boolean | OAuth readiness indicator. true when Wiro has a valid OAuth access token (reflected as connectedat / accesstoken on the credential) and every picker field declared by the template (customerid, merchantid, channelid, ad-account, page id, etc.) is populated. Non-OAuth credentials — API-key providers (Gmail, Apollo, Lemlist, Brevo, SendGrid, WordPress, Telegram) and service-account providers (Firebase, Google Drive, Google Play, App Store Connect) — do not raise _connected to true because they never write a connectedat; check setuprequired on UserAgent/Detail (or inspect the field values directly) to know when those are configured. |
optional |
boolean | true when the template marks this credential as not required for the agent to run. Agents can start even if optional: true credentials are empty. |
extra |
boolean | true when the template groups the credential under "extra integrations" in the UI (disabled by default until the user opts into the skill). |
Field redaction in responses
fieldstatus: "oauth_session"fields (accesstoken,refreshtoken,tokenexpiresat,pageAccessToken, etc.) — always stripped from every response, regardless of caller role.fieldstatus: "platform"fields (credentials.wiro.apiKey,credentials.openai.apiKey,credentials.calendarific.apiKey) — stripped for API callers (role =user). Only the agent runtime itself sees them.fieldstatus: "oauth_app"fields (clientsecret,appsecret) — visible to anyone who can read the credentials. History writes redactclientsecretto[REDACTED], but the live value is returned. Treat them as privileged values in your own UI layer (do not surface them to non-admin users).- Sentinel rows
_isoptional/_isextra— never appear as fields; they're folded into theoptionalandextraflags above.
API callers see the non-sensitive fields (public identifiers like clientid, display-only values like igusername, flags like authmethod, and the three status flags above).
POST /UserAgent/Deploy
Creates a new agent instance from a catalog template, or builds a brand-new custom agent (no template).
useprepaid: true + tier — the subscription cost is deducted from your prepaid wallet immediately and the instance is created server-side in one call. There is no API path that accepts a credit card directly; subscriptions from a card can only be created through the Wiro dashboard at wiro.ai/panel/agents. Top up your wallet on wiro.ai/panel/billing before calling Deploy.
Prepaid deploy (useprepaid: true + tier) charges your wallet for the chosen tier price immediately and inserts a 30-day subscription row (plan: "agent", provider: "prepaid"). The instance is created in status 6 (Setup Required) when the template has required credentials you didn't pass inline; otherwise it auto-transitions to 0 (Stopped) and is ready for UserAgent/Start.
If you pass credentials, skills, or customskills at the top level of the Deploy body they are applied server-side in the same call (one-shot deploy + initial setup) — note that inline credentials only accept flat string/number fields and can't populate nested arrays (firebase accounts, drive folders, app-store apps); use POST /UserAgent/CredentialUpsert afterwards for those. See Agent Credentials → Prepaid deploy — inline setup supported for the full rules.
| Parameter | Type | Required | Description |
|---|---|---|---|
agentguid |
string | Conditional | The guid of the agent template from the catalog. Required unless custom: true. |
custom |
boolean | Conditional | Pass true to deploy a custom-built agent (no marketplace template). Mutually exclusive with agentguid. See Agent Builder for the full builder flow. |
title |
string | Yes | Display name for your instance. |
description |
string | No | Optional description (custom builds: free-text describing what the agent does). |
cover |
string | No | Optional cover image URL. Custom builds only — template deploys clone the agent's cover automatically. |
useprepaid |
boolean | Yes | Must be true for API deploys. Pays the tier price from your wallet balance in a single server-side call. |
tier |
string | No | Tier selection: "starter" (default) or "pro". Determines the price + credits debited to your wallet at deploy time. |
pinned |
boolean | No | Whether the agent appears in the pinned agents list (defaults to true). Pass false when deploying agents programmatically for end users so they don't clutter your admin dashboard. |
credentials |
object | No | Inline credentials to apply server-side (flat fields only — see Agent Credentials). |
skills |
object | No | Inline skill toggles { "skillname": true | false }. For custom builds this seeds the initial skill set; for template deploys it overlays the template defaults. |
customskills |
array | No | Inline custom skill rows. Each entry: { key, value?, interval?, enabled?, description?, _user_created? }. |
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 team admin before writing the instance row.
Request body — template deploy (recommended API pattern)
{
"agentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "My Instagram Bot",
"useprepaid": true,
"tier": "starter",
"pinned": false
}
Request body — custom build (Build Your Own Agent)
{
"custom": true,
"title": "My Custom Sales Agent",
"description": "Watches inbound emails and posts a summary to Slack each morning.",
"useprepaid": true,
"tier": "pro",
"skills": {
"int-gmail-check": true,
"int-wordpress-post": true,
"int-wiro-generator": true
},
"credentials": {
"gmail": { "account": "[email protected]", "apppassword": "xxxx xxxx xxxx xxxx" },
"sys-telegram": { "bottoken": "123456:ABC-DEF...", "allowedusers": "[\"761381461\"]", "sessionmode": "private" }
}
}
POST /UserAgent/PricingPreview (with draft: true) to estimate the tier price before calling Deploy. Full walkthrough: Agent Builder.Response
{
"result": true,
"errors": [],
"useragents": [
{
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"uuid": "your-user-uuid",
"agentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"teamguid": null,
"title": "My Instagram Bot",
"description": null,
"tier": "starter",
"tiermultiplier": 3,
"credentials": {
"instagram": {
"_connected": false,
"optional": false,
"extra": false,
"_editable": { "authmethod": true, "igusername": true },
"_schema": {
"title": "Instagram",
"icon": "https://wiro.ai/images/icons/credentials/instagram.svg",
"brand_color": "#e4405f",
"brand_text_color": "#ffffff",
"brand_logo_filter": "brightness(0) invert(1)",
"docs_url": "integration-instagram-skills",
"credential_mode": "oauth_or_api_key",
"connection_modes": ["wiro", "own"],
"wiro_connect_pending": false,
"fields": [
{ "key": "authmethod", "label": "Authentication Method", "type": "select", "required": true, "options": [{ "label": "Wiro-managed", "value": "wiro" }, { "label": "Own OAuth app", "value": "own" }], "default": "wiro" },
{ "key": "igusername", "label": "Instagram Username", "type": "text", "required": true, "placeholder": "@yourbusiness" }
]
},
"_required_missing": true,
"authmethod": "",
"igusername": ""
}
},
"customskills": [],
"scheduledskills": [],
"skills": [
{ "name": "int-instagram-post", "enabled": true, "_edited": false, "_user_created": false },
{ "name": "int-wiro-generator", "enabled": true, "_edited": false, "_user_created": false }
],
"skillsmeta": {
"int-instagram-post": {
"name": "int-instagram-post",
"title": "Instagram Post",
"icon": "https://wiro.ai/images/icons/skills/instagram.svg",
"brand_color": "#e4405f",
"brand_text_color": "#ffffff",
"brand_logo_filter": "brightness(0) invert(1)",
"category": "int",
"docs_url": "integration-instagram-skills",
"credential_key": "instagram",
"additional_credential_keys": [],
"requires_credentials": true,
"user_invocable": true,
"deprecated": false,
"replacement": null,
"description": "Post carousel feed and multi-story via Meta Graph API."
},
"int-wiro-generator": {
"name": "int-wiro-generator",
"title": "Wiro Generator (platform-managed)",
"icon": "https://wiro.ai/images/icons/skills/wiro.svg",
"brand_color": "#33f2bc",
"brand_text_color": "#0f1a24",
"brand_logo_filter": null,
"category": "int",
"docs_url": null,
"credential_key": "wiro",
"additional_credential_keys": [],
"requires_credentials": true,
"user_invocable": false,
"deprecated": false,
"replacement": null,
"description": "Internal: lets the agent call Wiro's own image / video / LLM generation API. Other skills invoke it; users do not."
}
},
"monthlycredits": 1000,
"monthlypriceusd": 9,
"extracredits": 0,
"usedcredits": 0,
"remainingcredits": 1000,
"creditperiod": "2026-05",
"creditsyncat": null,
"peractioncosts": {
"message": 10,
"create": 60,
"modify": 20,
"regenerate": 20
},
"teamsessionmode": "",
"status": 6,
"setuprequired": true,
"pinned": false,
"agent": {
"guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Instagram Manager",
"slug": "instagram-manager",
"cover": "https://cdn.wiro.ai/uploads/agents/instagram-manager-cover.webp",
"categories": ["social-media", "marketing"],
"tiermultiplier": 3,
"tiers": {
"starter": { "priceUsd": 9, "credits": 1000 },
"pro": { "priceUsd": 29, "credits": 5000 }
},
"extracreditpacks": [
{ "packkey": "small", "credits": 5000, "priceusd": 45, "enabled": true },
{ "packkey": "medium", "credits": 10000, "priceusd": 80, "enabled": true },
{ "packkey": "large", "credits": 20000, "priceusd": 140, "enabled": true }
]
},
"createdat": 1714608000,
"updatedat": 1714608000
}
]
}
Status and setuprequired on deploy
The Deploy response reflects the same composed shape you get from UserAgent/Detail. Two status signals matter:
| Field | Meaning |
|---|---|
status: 6 |
Setup Required — agent cannot start yet, at least one non-optional credential is still empty. |
status: 0 |
Stopped — all required credentials are set, agent is ready to launch with UserAgent/Start. Happens immediately when the template has no required credentials, or you filled them inline in the Deploy body. |
setuprequired: true |
Mirrors the condition: any non-optional credential missing. Stays true while status: 6. |
setuprequired: false |
All non-optional credentials complete. Safe to call UserAgent/Start. |
setuprequired + agent summary. It does not include subscription — that field is assembled in UserAgent/Detail / UserAgent/MyAgents from the subscriptions table. A prepaid subscription row (agent-<plan>, provider prepaid) is inserted server-side during the Deploy call; call POST /UserAgent/Detail with the returned guid to see the subscription object.
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": [
{
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"uuid": "ada-uuid",
"agentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"teamguid": null,
"title": "My Instagram Bot",
"description": null,
"cover": null,
"categories": ["social-media", "marketing"],
"tier": "pro",
"tiermultiplier": 3,
"monthlypriceusd": 29,
"monthlycredits": 5000,
"extracredits": 2000,
"usedcredits": 1450,
"creditperiod": "2026-05",
"creditsyncat": 1714694410,
"status": 4,
"pinned": true,
"teamsessionmode": "",
"setuprequired": false,
"subscription": {
"plan": "agent",
"status": "active",
"amount": 29,
"currency": "usd",
"currentperiodend": 1717200000,
"renewaldate": "2026-06-01T00:00:00.000Z",
"daysremaining": 62,
"pendingdowngrade": null,
"provider": "prepaid"
},
"agent": {
"guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Instagram Manager",
"slug": "instagram-manager",
"description": "An autonomous agent that manages your Instagram Business account.",
"cover": "https://cdn.wiro.ai/uploads/agents/instagram-manager-cover.webp",
"categories": ["social-media", "marketing"],
"tiermultiplier": 3,
"tiers": {
"starter": { "priceUsd": 9, "credits": 1000 },
"pro": { "priceUsd": 29, "credits": 5000 }
}
},
"extracreditsexpiry": 1730419200,
"createdat": 1714608000,
"updatedat": 1714694400,
"queuedat": 1714694395,
"startedat": 1714694400,
"runningat": 1714694410,
"stoppingat": null,
"stopdat": null,
"errordat": null
}
]
}
MyAgents returns one row per useragent with its scalar fields, the trimmed agent template summary (no extracreditpacks), subscription, setuprequired (status-based check only), and resolved extracredits / extracreditsexpiry. Composed children (credentials, customskills, scheduledskills, skills, skillsmeta, peractioncosts, remainingcredits) are not included — call UserAgent/Detail on a single guid for the full composed shape.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
{
"result": true,
"errors": [],
"useragents": [
{
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"uuid": "ada-uuid",
"agentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"teamguid": null,
"title": "My Instagram Bot",
"description": null,
"cover": null,
"categories": ["social-media", "marketing"],
"tier": "pro",
"tiermultiplier": 3,
"status": 4,
"pinned": true,
"setuprequired": false,
"teamsessionmode": "",
"credentials": {
"instagram": {
"_connected": true,
"optional": false,
"extra": false,
"_editable": {
"authmethod": true,
"igusername": true
},
"_schema": {
"title": "Instagram",
"icon": "https://wiro.ai/images/icons/credentials/instagram.svg",
"brand_color": "#E4405F",
"brand_text_color": "#FFFFFF",
"brand_logo_filter": null,
"docs_url": "/docs/integration-instagram-skills",
"credential_mode": "oauth_or_api_key",
"connection_modes": ["wiro", "own"],
"wiro_connect_pending": true,
"oauth_provider": "instagram",
"fields": [
{ "key": "authmethod", "label": "Authentication Method", "type": "select", "required": true, "options": [{ "label": "Wiro-managed", "value": "wiro" }, { "label": "Own OAuth app", "value": "own" }], "default": "wiro" },
{ "key": "igusername", "label": "Instagram Username", "type": "text", "required": true, "placeholder": "@yourbusiness", "help": "Your Instagram Business account username (without the @)." }
]
},
"_required_missing": false,
"authmethod": "wiro",
"igusername": "mybrand",
"connectedat": "2026-05-01T12:00:00.000Z",
"tokenexpiresat": null
},
"sys-telegram": {
"_connected": false,
"optional": true,
"extra": false,
"_editable": {
"bottoken": true,
"allowedusers": true,
"sessionmode": true
},
"_schema": {
"title": "Telegram Bot",
"icon": "https://wiro.ai/images/icons/credentials/telegram.svg",
"brand_color": "#2AABEE",
"brand_text_color": "#FFFFFF",
"brand_logo_filter": null,
"docs_url": "/docs/integration-telegram-skills",
"credential_mode": "api_key",
"connection_modes": ["api_key"],
"wiro_connect_pending": false,
"oauth_provider": null,
"fields": [
{ "key": "bottoken", "label": "Bot Token", "type": "password", "required": true, "placeholder": "123456789:ABC-DEF1234ghIkl-zyx57W2v1u123ew11", "help": "Issued by @BotFather when you create the bot." },
{ "key": "allowedusers", "label": "Allowed Telegram User IDs", "type": "text", "required": false, "placeholder": "[\"761381461\", \"823901274\"]", "help": "JSON array of Telegram user / chat IDs that may DM the bot. Empty = open to anyone." },
{ "key": "sessionmode", "label": "Session Mode", "type": "select", "required": true, "options": [{ "label": "Private (one session per user)", "value": "private" }, { "label": "Group (one session per chat)", "value": "group" }], "default": "private" }
]
},
"_required_missing": false,
"bottoken": "",
"allowedusers": "",
"sessionmode": "private"
},
"wordpress": {
"_connected": false,
"optional": false,
"extra": false,
"_editable": {
"siteurl": true,
"username": true,
"applicationpass": true
},
"_schema": {
"title": "WordPress",
"icon": "https://wiro.ai/images/icons/credentials/wordpress.svg",
"brand_color": "#21759B",
"brand_text_color": "#FFFFFF",
"brand_logo_filter": null,
"docs_url": "/docs/integration-wordpress-skills",
"credential_mode": "api_key",
"connection_modes": ["api_key"],
"wiro_connect_pending": false,
"oauth_provider": null,
"fields": [
{ "key": "siteurl", "label": "Site URL", "type": "text", "required": true, "placeholder": "https://blog.example.com", "pattern": "^https?://.+" },
{ "key": "username", "label": "Username", "type": "text", "required": true, "placeholder": "wp-editor" },
{ "key": "applicationpass", "label": "Application Password", "type": "password", "required": true, "placeholder": "xxxx xxxx xxxx xxxx xxxx xxxx", "help": "Generate from WP Admin → Users → Profile → Application Passwords." }
]
},
"_required_missing": true,
"siteurl": "",
"username": "",
"applicationpass": ""
}
},
"customskills": [
{
"key": "cs-content-tone",
"description": "Brand voice + posting rules read by every content-generation skill on this agent.",
"value": "## Brand Voice\nTone: friendly, casual, never salesy.\nTarget Audience: indie devs and side-project builders.\n\n## Hashtag Strategy\nMax 3 per post. Always include #BuildInPublic and #Indie.\n\n## Platform Rules\n- Instagram: square images only, carousels for tutorials.\n- Twitter: thread format for posts longer than 200 chars.",
"enabled": true,
"interval": null,
"_source": "preset-strategy",
"_editable": true,
"_edited": true
},
{
"key": "cs-content-sources",
"description": "RSS / sitemap URLs the agent should poll for new content ideas.",
"value": "## Primary Sources\nhttps://blog.example.com/feed.xml\nhttps://news.ycombinator.com/rss\n\n## CTA URL Pattern\nhttps://example.com/posts/{slug}",
"enabled": true,
"interval": null,
"_source": "preset-strategy",
"_editable": true,
"_edited": false
}
],
"scheduledskills": [
{
"key": "cs-cron-content-scanner",
"description": "Polls the content sources every 4 hours and queues fresh ideas for the post generator.",
"value": "Scan the configured RSS / sitemap sources, dedupe against last 14 days, and queue up to 3 fresh items into the post pipeline.",
"enabled": true,
"interval": "0 */4 * * *",
"_source": "skill-bundle",
"_editable": true,
"_edited": false
},
{
"key": "cs-cron-weekly-roundup",
"description": "User-created weekly summary cron that publishes a Monday recap to WordPress.",
"value": "Every Monday at 09:00 UTC, summarize last week's published posts (titles + engagement) and publish the recap as a WordPress post.",
"enabled": true,
"interval": "0 9 * * 1",
"_source": "user-created",
"_editable": true,
"_edited": false,
"_user_created": true
}
],
"skills": [
{ "name": "int-instagram-post", "enabled": true, "_edited": false, "_user_created": false },
{ "name": "int-twitterx-post", "enabled": true, "_edited": true, "_user_created": false },
{ "name": "int-wiro-generator", "enabled": true, "_edited": false, "_user_created": false }
],
"skillsmeta": {
"int-instagram-post": {
"name": "int-instagram-post",
"title": "Instagram — Post & Stories",
"icon": "https://wiro.ai/images/icons/skills/instagram.svg",
"brand_color": "#E4405F",
"brand_text_color": "#FFFFFF",
"brand_logo_filter": null,
"category": "int",
"docs_url": "/docs/integration-instagram-skills",
"credential_key": "instagram",
"additional_credential_keys": [],
"requires_credentials": true,
"user_invocable": true,
"deprecated": false,
"replacement": null,
"description": "Publish single-image, carousel, and story posts to a connected Instagram Business account."
},
"int-twitterx-post": {
"name": "int-twitterx-post",
"title": "X (Twitter) — Post & Reply",
"icon": "https://wiro.ai/images/icons/skills/twitterx.svg",
"brand_color": "#000000",
"brand_text_color": "#FFFFFF",
"brand_logo_filter": null,
"category": "int",
"docs_url": "/docs/integration-twitter-skills",
"credential_key": "twitterx",
"additional_credential_keys": [],
"requires_credentials": true,
"user_invocable": true,
"deprecated": false,
"replacement": null,
"description": "Compose tweets, threads, and replies on a connected X (Twitter) account."
},
"int-wiro-generator": {
"name": "int-wiro-generator",
"title": "Wiro Generator (platform-managed)",
"icon": "https://wiro.ai/images/icons/skills/wiro.svg",
"brand_color": "#33F2BC",
"brand_text_color": "#0F1A24",
"brand_logo_filter": null,
"category": "int",
"docs_url": null,
"credential_key": "wiro",
"additional_credential_keys": [],
"requires_credentials": true,
"user_invocable": false,
"deprecated": false,
"replacement": null,
"description": "Internal: lets the agent call Wiro's own image / video / LLM generation API. Other skills invoke it; users do not."
}
},
"monthlycredits": 5000,
"monthlypriceusd": 29,
"extracredits": 2000,
"usedcredits": 1450,
"remainingcredits": 5550,
"creditperiod": "2026-05",
"creditsyncat": 1714694410,
"peractioncosts": {
"message": 10,
"create": 60,
"modify": 20,
"regenerate": 20
},
"subscription": {
"plan": "agent",
"status": "active",
"amount": 29,
"currency": "usd",
"currentperiodend": 1717200000,
"renewaldate": "2026-06-01T00:00:00.000Z",
"daysremaining": 28,
"pendingdowngrade": null,
"provider": "prepaid"
},
"agent": {
"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 Business account: drafts posts from your RSS feeds, schedules carousels, and replies to DMs.",
"cover": "https://cdn.wiro.ai/uploads/agents/instagram-manager-cover.webp",
"icon": "https://cdn.wiro.ai/uploads/agents/instagram-manager-icon.webp",
"categories": ["social-media", "marketing"],
"tiermultiplier": 3,
"tiers": {
"starter": { "priceUsd": 9, "credits": 1000 },
"pro": { "priceUsd": 29, "credits": 5000 }
},
"extracreditpacks": [
{ "packkey": "small", "credits": 25000, "priceusd": 130, "enabled": true },
{ "packkey": "medium", "credits": 50000, "priceusd": 240, "enabled": true },
{ "packkey": "large", "credits": 100000, "priceusd": 420, "enabled": true }
]
},
"extracreditsexpiry": 1730419200,
"createdat": 1714608000,
"updatedat": 1714694400,
"queuedat": 1714694395,
"startedat": 1714694400,
"runningat": 1714694410,
"stoppingat": null,
"stopdat": null,
"errordat": null
}
]
}
| Field | Type | Description |
|---|---|---|
guid |
string |
Unique identifier for this agent instance. |
agentid |
number|null |
The catalog agent ID this instance was deployed from. null for custom builds. |
title |
string |
Display name you gave this instance. |
tier |
string |
"starter" or "pro" — the active tier for this instance. |
tiermultiplier |
number |
Per-instance Pro multiplier — snapshotted at deploy from the template (default 3). Drives the Pro tier's price + credit allocation. |
status |
number |
Current status code (see UserAgent Statuses). |
setuprequired |
boolean |
true if credentials are missing or incomplete. |
credentials |
object |
Per-provider credential map assembled from the normalized tables. Sensitive fields (OAuth tokens, platform API keys) are hidden. Each credential carries _connected / optional / extra / _editable / _schema. |
customskills |
array |
Composed list of preset strategies and non-cron custom skills (writable instructions). Each entry carries _source (preset-strategy | user-created), _editable, and (only on user-created rows) _user_created: true. |
scheduledskills |
array |
Composed list of cron skills (cs-cron-*). Same field shape as customskills plus interval (cron expression). _source is skill-bundle for crons that ship with an integration skill, user-created for user-added crons. |
skills |
array<string> |
Currently-enabled integration skills (object array — each entry: { name, enabled, _edited, _user_created }). Toggle one or more skills with POST /UserAgent/SkillsApply. |
monthlycredits |
number |
Monthly credit allocation snapshotted at deploy / renewal. |
monthlypriceusd |
number |
Monthly USD price snapshotted at deploy / renewal. Mirrors subscription.amount. |
extracredits |
number |
Active extra-credit balance (sum of non-expired packs from POST /UserAgent/CreateExtraCreditCheckout). |
usedcredits |
number |
Credits consumed during the current billing period, reported by the agent runtime. |
remainingcredits |
number |
Computed: max(0, monthlycredits + extracredits - usedcredits). |
creditperiod |
string |
'YYYY-MM' tag of the current billing window. Rolls over on subscription renewal. |
creditsyncat |
number|null |
Unix seconds of the last agent → API usage sync (null before the first report). |
peractioncosts |
object |
{ message, create, modify, regenerate } — credits charged per action. Computed as max() across enabled skills' pricing.credit_costs. |
teamsessionmode |
string |
Session isolation mode for team agents ("collaborative" or "private" — empty string for solo agents). |
subscription |
object|null |
Active subscription info, or null if no subscription. subscription.plan is "agent", subscription.provider is "prepaid" for API-deployed instances. |
agent |
object |
Parent agent template info (title, slug, cover, tiers, tiermultiplier, extracreditpacks). For custom builds this is a synthesized placeholder containing the same shape with agent.custom: true. |
extracreditsexpiry |
number|null |
Unix timestamp when the earliest extra credit pack expires. |
POST /UserAgent/Update
Updates an agent instance's scalar fields only (title, description, categories, cover URL). If the agent is currently starting (status 3) or running (status 4), this triggers an automatic restart to apply the new settings (the agent is moved to Stopping with restartafter: true, and re-queued after it fully stops).
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
title |
string | No | New display name |
description |
string | No | New description (set to empty string or null to clear) |
categories |
array | No | Updated categories. Cannot be empty if provided. |
cover |
string | No | New cover image URL (set to empty string or null to clear). For multipart upload, use POST /UserAgent/Cover instead. |
| What you want to change | Use |
|---|---|
| Provider credentials (API keys, OAuth settings) | POST /UserAgent/CredentialUpsert |
| Custom skill values (strategy text, cron intervals, enabled flag) | POST /UserAgent/CustomSkillUpsert |
| Rename a user-created custom skill (key + optional description) | POST /UserAgent/CustomSkillRename |
| Delete a user-created cron skill | POST /UserAgent/CustomSkillDelete |
| Toggle one or more integration skills on/off (with optional tier change) | POST /UserAgent/SkillsApply |
| Upgrade Starter → Pro | POST /UserAgent/UpgradeTier |
| Upload a cover image (multipart) | POST /UserAgent/Cover |
6 (Setup Required) and a subsequent CredentialUpsert call completes all required credentials, the status automatically changes to 0 (Stopped), allowing you to start it.Response
{
"result": true,
"errors": [],
"useragents": [
{
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"uuid": "ada-uuid",
"agentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"teamguid": null,
"title": "My Instagram Bot",
"description": "Updated description after rename",
"cover": "https://cdn.wiro.ai/uploads/useragents/f8e7d6c5-cover.webp",
"categories": ["social-media", "marketing"],
"tier": "starter",
"tiermultiplier": 3,
"monthlypriceusd": 9,
"monthlycredits": 1000,
"extracredits": 0,
"usedcredits": 320,
"creditperiod": "2026-05",
"creditsyncat": 1714694400,
"status": 1,
"pinned": false,
"setuprequired": false,
"teamsessionmode": "",
"agent": {
"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 Business account.",
"cover": "https://cdn.wiro.ai/uploads/agents/instagram-manager-cover.webp",
"icon": "https://cdn.wiro.ai/uploads/agents/instagram-manager-icon.webp",
"categories": ["social-media", "marketing"],
"tiermultiplier": 3,
"tiers": {
"starter": { "priceUsd": 9, "credits": 1000 },
"pro": { "priceUsd": 29, "credits": 5000 }
},
"extracreditpacks": []
},
"createdat": 1714608000,
"updatedat": 1714694500,
"queuedat": 1714694498,
"startedat": null,
"runningat": null,
"stoppingat": 1714694500,
"stopdat": null,
"errordat": null
}
]
}
Update returns scalar useragent fields + the agent template summary + setuprequired only. It does not re-compose the heavy children (credentials, customskills, scheduledskills, skills, skillsmeta, peractioncosts, remainingcredits, subscription, extracreditsexpiry). Call UserAgent/Detail afterwards if you need the full composed shape — Update is optimised for fast scalar writes.3/4 agent, the response shows the lifecycle transition mid-flight: status: 1 (Stopping) with a fresh stoppingat timestamp, runningat cleared, and queuedat set to the same instant — Wiro auto-queues the agent so it picks the new settings up after the next stop cycle.POST /UserAgent/Cover
Uploads a new cover image for the agent instance via multipart. Mirrors the /User/Avatar pattern — accepts jpg, png, gif, jpeg, webp; converts to webp; uploads to S3; writes the resulting CDN URL into useragents.cover.
If you already have a hosted URL, use POST /UserAgent/Update with cover: "<url>" instead.
Multipart fields:
| Field | Type | Required | Description |
|---|---|---|---|
useragentguid | text | Yes | Your UserAgent instance guid |
image | file | Yes | The cover image file (jpg/png/gif/jpeg/webp, max ~10MB) |
Response
{
"result": true,
"errors": [],
"cover": "https://cdn.wiro.ai/uploads/useragents/f8e7d6c5-b4a3-2190-fedc-ba0987654321-cover.webp",
"useragents": [
{
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"uuid": "ada-uuid",
"agentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"teamguid": null,
"title": "My Instagram Bot",
"description": null,
"cover": "https://cdn.wiro.ai/uploads/useragents/f8e7d6c5-b4a3-2190-fedc-ba0987654321-cover.webp",
"categories": ["social-media", "marketing"],
"tier": "starter",
"tiermultiplier": 3,
"monthlypriceusd": 9,
"monthlycredits": 1000,
"extracredits": 0,
"usedcredits": 320,
"creditperiod": "2026-05",
"creditsyncat": 1714694400,
"status": 4,
"pinned": false,
"teamsessionmode": "",
"agent": {
"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 Business account.",
"cover": "https://cdn.wiro.ai/uploads/agents/instagram-manager-cover.webp",
"icon": "https://cdn.wiro.ai/uploads/agents/instagram-manager-icon.webp",
"categories": ["social-media", "marketing"],
"tiermultiplier": 3
},
"createdat": 1714608000,
"updatedat": 1714694500,
"queuedat": 1714694395,
"startedat": 1714694400,
"runningat": 1714694410,
"stoppingat": null,
"stopdat": null,
"errordat": null
}
]
}
agent template summary only. It does not include setuprequired, peractioncosts, remainingcredits, or any composed children (credentials, customskills, scheduledskills, skills, skillsmeta, subscription). The endpoint is optimised for the cover refresh round-trip; call UserAgent/Detail afterwards if you need the full composed shape.POST /UserAgent/CredentialUpsert
For credential field history — see CredentialFieldHistory on the Agent Credentials page.
Writes one or more credential fields for a single or multiple providers in one call.
If the agent is starting or running, completing the upsert triggers an automatic restart (restartafter: true).
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | Your UserAgent instance guid |
fields |
array | Yes | One or more field rows. Each row has { credentialkey, fieldname, fieldvalue, fieldstatus? }. You can write to more than one credentialkey in a single request. |
Each field row:
| Field | Type | Description |
|---|---|---|
credentialkey |
string | The provider key — e.g. "instagram", "google-ads", "wordpress", "telegram". Must match one of the credentials declared in credentials on /UserAgent/Detail. |
fieldname |
string | Field inside that credential — e.g. "apiKey", "clientid", "bottoken". Must not start with _ (reserved for internal sentinels such as _isoptional, _isextra). |
fieldvalue |
string | The value to write. Empty string is allowed (effectively clears the field). |
fieldstatus |
string | Optional. One of user, platform, oauth_app, oauth_picker, computed, control. Defaults to user for API callers (the only status user-role API keys may write). OAuth picker/session fields are written by Wiro's OAuth callback internally, not by your API. |
parentfield |
string | Optional. Dotted path when the credential has a nested array (e.g. "accounts.0.apps" for firebase.accounts[0].apps[*]). |
ordinal |
number | Optional. Array index inside parentfield. Defaults to 0. |
Request
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"fields": [
{ "credentialkey": "wordpress", "fieldname": "url", "fieldvalue": "https://myblog.com" },
{ "credentialkey": "wordpress", "fieldname": "user", "fieldvalue": "admin" },
{ "credentialkey": "wordpress", "fieldname": "apppassword", "fieldvalue": "abcd efgh ijkl mnop" }
]
}'
Response
{ "result": true, "applied": 3, "errors": [] }
applied is the number of rows actually written. Any row that fails validation (reserved fieldname, invalid fieldstatus for your role) is skipped and reported in errors without rolling back the others.
POST /UserAgent/CustomSkillUpsert
Writes a single custom skill value (strategy text) or cron setting (enabled / interval).
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | Your UserAgent instance guid |
skillkey |
string | Yes | The skill key. Preset strategies use bare names (e.g. "content-tone"). Bundled crons and user-created crons use the cron- prefix (e.g. "cron-content-scanner"). |
value |
string | No | Strategy body (SKILL.md content). Only persisted for editable preset strategies or user-created skills — sending value to a bundled cron is silently dropped. |
interval |
string | No | Cron expression (e.g. "0 */4 * * *"). Only persisted for cron-type skills. |
enabled |
boolean | No | Turn the cron on or off. Only persisted for cron-type skills. |
description |
string | No | Only persisted for user-created skills. |
usercreated |
boolean | No | Pass true to create a brand-new cron under the calling user's account. Server auto-prefixes skillkey with cron- if missing. |
Request
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"skillkey": "content-tone",
"value": "## Brand Voice\nTone: friendly\nTarget Audience: teens on Instagram"
}'
Response
{ "result": true, "errors": [] }
POST /UserAgent/CustomSkillRename
Renames a user-created custom skill, optionally updating its description in the same atomic write. Only rows with usercreated: true can be renamed through this endpoint — preset-owned rows reject with agent-customskill-rename-preset-forbidden (preset renames cascade through the admin /Agent/CustomSkillRename channel instead).
The skillkey flavour is preserved: a cs-cron-* scheduled task stays scheduled, a cs-* strategy stays a strategy. Cross-flavour renames are rejected — delete + re-create via CustomSkillUpsert with the right kind if you need to switch sections.
Version history is migrated under the new key so the full audit chain stays continuous after the rename, and a new rename entry is appended.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | Your UserAgent instance guid |
oldskillkey |
string | Yes | The skill's current canonical key (e.g. cs-my-strategy, cs-cron-daily-summary). |
newskillkey |
string | Yes | The new skill key. The server canonicalises bare slugs using the flavour of oldskillkey: if old is cs-cron-* the new slug lands on cs-cron-*; otherwise cs-*. |
description |
string | No | New description. Omit to leave unchanged; pass an empty string to clear. |
Restart. The renamed key surfaces in the container's settings.json, so the useragent is auto-restarted on success (same behaviour as CustomSkillUpsert functional edits).
Request
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillRename" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"oldskillkey": "cs-my-custom-strategy",
"newskillkey": "cs-my-renamed-strategy",
"description": "Updated note shown next to the name"
}'
Response
{ "result": true, "errors": [], "newskillkey": "cs-my-renamed-strategy" }
Error cases
| Error message key | When |
|---|---|
agent-customskill-rename-preset-forbidden |
Target row has usercreated: false (preset-owned). Preset renames go through the admin /Agent/CustomSkillRename cascade. |
agent-customskill-rename-flavour-mismatch |
Caller tried to rename cs-cron-* ↔ cs-*. Delete + re-create with the right kind instead. |
agent-customskill-rename-collision |
newskillkey already exists on this useragent. |
agent-customskill-rename-same-key |
oldskillkey equals canonicalised newskillkey — nothing to change. |
agent-customskill-not-found |
oldskillkey does not exist on the useragent. |
POST /UserAgent/CustomSkillDelete
Removes a user-created cron skill. Has no effect on preset strategies or bundled crons (those cannot be deleted through the API).
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | Your UserAgent instance guid |
skillkey |
string | Yes | The skill key to remove (cron-*). |
Response
{ "result": true, "errors": [] }
POST /UserAgent/CustomSkillHistory
Returns the version history for one custom skill on a useragent — a per-write audit trail with diff metadata + the resolved actor for each entry. Useful for building audit / change-log views in your dashboard.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Your UserAgent instance guid |
skillkey | string | Yes | Canonical skill key (e.g. "cs-content-tone") |
startdate | number | No | UTC epoch seconds — return entries on/after this time |
enddate | number | No | UTC epoch seconds — return entries on/before this time |
Response
{
"result": true,
"errors": [],
"preset_default": {
"value": "## Brand Voice\nTone: friendly...",
"intervalexpr": null,
"enabled": true,
"description": "How the agent should sound across all generated copy."
},
"entries": [
{
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"skillkey": "cs-content-tone",
"operation": "upsert",
"value": "## Brand Voice\nTone: friendly\n...",
"intervalexpr": null,
"enabled": true,
"usercreated": false,
"prev_value": "## Brand Voice\nTone: professional\n...",
"prev_interval": null,
"prev_enabled": true,
"after_value": "## Brand Voice\nTone: friendly\n...",
"after_interval": null,
"after_enabled": true,
"changed_fields": ["value"],
"changedby": "ada-uuid",
"changedby_user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"changedat": 1714694410
}
]
}
after_* and prev_* fields give you the resolved AFTER + previous-version snapshots without you having to walk the list manually:
prev_*— value as of the immediately-older entry (nullfor the oldest row).after_*— value the row was left with after this action committed: pulled from the next-newer entry's BEFORE state, or from the live custom-skill row for the newest entry.nullwhenoperation: "delete-user"(row was removed).changed_fields[]— names of fields whose value differs from the immediately-older entry. Subset of["value", "interval", "enabled"].changedby_user— resolved actor object (full shape:uuid,firstname,lastname,email,username,avatar,avatarinitials).nullwhenchangedbyis"system"(automation / cron) or when the user record was deleted.
POST /UserAgent/CustomSkillRevert
Reverts a custom skill to either (a) the agent template's preset default or (b) a specific historical version. Auto-triggers a restart on success unless the action is a no-op.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Your UserAgent instance guid |
skillkey | string | Yes | Canonical skill key |
source | string | Yes | "preset" (reset to template default) or "history" (jump to a versionguid) |
versionguid | string | When source=history | The entries[].guid from CustomSkillHistory |
Response
{
"result": true,
"errors": [],
"action": "reverted",
"current_value": "## Brand Voice\nTone: ...",
"current_interval": null,
"current_enabled": true
}
action is "reverted", "reset-to-preset", "deleted" (preset removed upstream), or "no-op" (already matches target).
POST /UserAgent/CredentialFieldHistory
Returns the per-field write history for one credential group on a useragent. Sensitive values are redacted at read time as a belt-and-suspenders guard:
oauth_sessionfields (access / refresh tokens) never appear in history at all — they're stripped before persisting.clientsecretand similaroauth_appsecret fields are stored as[REDACTED]in history rows (the live row carries the real secret — read it viaUserAgent/Detail).- Platform-only credentials (
wiro,calendarific,sys-openai) short-circuit to an emptyentries[](no user-facing history to show).
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Your UserAgent instance guid |
credentialkey | string | Yes | Provider key (e.g. "instagram", "wordpress") |
startdate | number | No | UTC epoch seconds — return entries on/after this time |
enddate | number | No | UTC epoch seconds — return entries on/before this time |
Response
{
"result": true,
"errors": [],
"entries": [
{
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"credentialkey": "instagram",
"fieldname": "igusername",
"fieldvalue": "myaccount",
"fieldstatus": "user",
"parentfield": null,
"ordinal": 0,
"operation": "upsert",
"changedby": "ada-uuid",
"changedby_user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"changedat": 1714694410
},
{
"guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"credentialkey": "instagram",
"fieldname": "clientsecret",
"fieldvalue": "[REDACTED]",
"fieldstatus": "oauth_app",
"parentfield": null,
"ordinal": 0,
"operation": "upsert",
"changedby": "ada-uuid",
"changedby_user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"changedat": 1714600000
}
]
}
changedby_user is the resolved actor object (uuid, firstname, lastname, email, username, avatar, avatarinitials). It is null when changedby is "system" (automation / cron) or when the user record has been deleted.
POST /UserAgent/SkillsApply
Applies a batch of skill toggles + an optional tier change in a single transactional unit. This is the only skill-toggle endpoint API consumers should use — even when you're flipping a single skill, send it as a one-entry skills map. The single-skill alternative (SkillToggle) is not part of the public API surface.
Designed for both the Skill Editor modal (multiple toggles in one save) and one-off API mutations:
- Charges the wallet (or proration) once, not N times.
- Triggers exactly one container restart after all toggles land.
- Uses idempotency + a UA-level mutex — concurrent calls or FE retries can't double-apply.
- Atomic — a payment-side failure rolls back to the pre-call skill set.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Your UserAgent instance guid (custom build only for non-admin callers) |
skills | object | Yes | Map of { "skillname": true | false } for every skill you want to set. Skills not in the map keep their current state. |
tier | string | No | "starter" or "pro" — change the tier in the same call. Pro → Starter downgrade is rejected (cancel + re-subscribe instead). |
idempotencyKey | string | Yes | Caller-generated unique key (UUID recommended). Replays of the same key return the cached response without re-running the saga. |
Request
curl -X POST "https://api.wiro.ai/v1/UserAgent/SkillsApply" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"tier": "pro",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"skills": {
"int-instagram-post": true,
"int-twitterx-post": true,
"int-wordpress-post": false
}
}'
Response (success)
{
"result": true,
"errors": [],
"tier": "pro",
"stripeProrationApplied": false,
"prepaidWalletDelta": 19.99,
"restartTriggered": true,
"restartedAt": 1714694520,
"pricing": {
"previousPriceUsd": 9,
"newPriceUsd": 39,
"deltaUsd": 30,
"previousMonthlyCredits": 1000,
"newMonthlyCredits": 6000,
"deltaCredits": 5000,
"enabledSkills": ["int-instagram-post", "int-twitterx-post", "int-wiro-generator"],
"peractioncosts": { "message": 10, "create": 60, "modify": 20, "regenerate": 20 }
}
}
| Field | Type | Description |
|---|---|---|
tier | string | The tier in effect after the call ("starter" or "pro"). Mirrors body.tier — or the unchanged current tier if the request omitted it. |
stripeProrationApplied | boolean | Always false for prepaid subscriptions (the prorated charge is taken from the wallet as prepaidWalletDelta instead). |
prepaidWalletDelta | number | USD debited (positive) or credited (negative) from the wallet for the prorated price diff. 0 when the toggle batch is a no-op or the agent has no active subscription yet. |
restartTriggered | boolean | true when the agent runtime was restarted to pick up the new skill set. false for stopped agents (no restart needed) or no-op batches. |
restartedAt | number|null | Unix seconds when the restart trigger fired. null when restartTriggered is false. |
pricing.previousPriceUsd / newPriceUsd / deltaUsd | number | USD totals before / after the apply, plus the signed delta. |
pricing.previousMonthlyCredits / newMonthlyCredits / deltaCredits | number | Monthly credit allocation before / after, plus the signed delta. |
pricing.enabledSkills | array<string> | Final enabled skill set including transitive depends_on closure — same skill names you'd see on useragent.skills[].name in the next Detail call. |
pricing.peractioncosts | object | The { message, create, modify, regenerate } per-action burn rates that follow from the new skill set. |
Common error codes
| Code | Meaning |
|---|---|
100 | skillsapply-template-only — non-admin caller tried to mutate a template-deploy useragent. |
101 | skillsapply-deps-violation — a final enabled skill has unsatisfied depends_on. Body includes deps[]. |
102 | skillsapply-conflict — two finally-enabled skills are mutually exclusive. Body includes conflicts[]. |
103 | skillsapply-insufficient-wallet — wallet can't cover the prorated charge. |
104 | skillsapply-in-progress — another SkillsApply is already running on this useragent. Wait and retry. |
105 | skillsapply-tier-downgrade — Pro → Starter rejected. |
106 | skillsapply-subscription-inactive — subscription must be active to mutate. |
108 | skillsapply-db-tx-failed — DB transaction rolled back; safe to retry with a new idempotency key. |
POST /UserAgent/PricingPreview
Live tier-pricing preview. Drives the Build Your Agent / Skill Editor UI: as the user toggles skills on/off, the frontend POSTs here with skillOverrides to see "if I save this, my new monthly price would be $X with Y monthly credits" without writing any state.
Two body shapes:
A. Draft preview (custom builder, no useragent yet)
{
"draft": true,
"tier": "pro",
"skills": ["int-instagram-post", "int-wiro-generator"]
}
B. Existing useragent preview
{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"tier": "pro",
"skillOverrides": {
"int-instagram-post": true,
"int-wordpress-post": false
}
}
| Parameter | Type | Required | Description |
|---|---|---|---|
draft | boolean | A | When true, no useragent lookup happens; the resolver synthesises pricing from the skills array directly. Used by Build Your Agent. |
skills | array<string> | A | Draft mode only — the proposed enabled skill set. |
useragentguid | string | B | Required for non-draft previews. |
skillOverrides | object | No | Override the persisted state with { skillname: boolean } for the preview. Omit for "current state" preview. |
tier | string | No | "starter" or "pro" — preview a specific tier. The response also includes both tiers under tiers.{starter,pro}. |
Response
{
"result": true,
"errors": [],
"tier": "pro",
"tiermultiplier": 3,
"totalPriceUsd": 14,
"totalMonthlyCredits": 1500,
"peractioncosts": { "message": 10, "create": 60, "modify": 20, "regenerate": 20 },
"skillBreakdown": [
{ "skill": "int-instagram-post", "priceUsd": 5, "credits": 500, "actionCostOverrides": { "create": 60 } },
{ "skill": "int-wiro-generator", "priceUsd": 0, "credits": 0, "actionCostOverrides": {} }
],
"enabledSkills": ["int-instagram-post", "int-wiro-generator"],
"directSkills": ["int-instagram-post"],
"agentBase": { "priceUsd": 9, "credits": 1000 },
"tiers": {
"starter": { "priceUsd": 9, "credits": 1000 },
"pro": { "priceUsd": 14, "credits": 1500 }
}
}
enabledSkills is the full closure (including transitive depends_on); directSkills is just what the user explicitly toggled.
POST /UserAgent/CreateSubscriptionCheckout
Subscribes a useragent that doesn't yet have an active subscription. Wallet is debited, a prepaid subscription row is inserted, and the agent is auto-queued — same single-call behaviour as Deploy with useprepaid: true. Always pass useprepaid: true from the API.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Your UserAgent instance guid |
useprepaid | boolean | Yes | Must be true for API consumers. |
tier | string | No | "starter" or "pro" — overrides the useragent's persisted tier so the user can re-subscribe at a different tier without redeploying. |
Response
{
"result": true,
"errors": [],
"subscriptionId": 8421,
"monthlypriceusd": 29,
"monthlycredits": 5000
}
If the useragent already has an active subscription, the call rejects with Subscription already active for this useragent. Cancel or modify the existing subscription instead.
POST /UserAgent/Start
Starts a stopped agent instance. The agent is moved to Queued (status 2) and picked up by a worker. Also valid for agents in Error state (5) — Start re-queues them for another launch attempt.
| 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/Pin
Pins or unpins an agent instance from the user's pinned-agents quick list. Pinned agents appear in the global header dropdown (PinnedAgents) and get an unread badge from PinnedUnread.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Your UserAgent instance guid |
pinned | boolean | Yes | true to pin, false to unpin |
Response
{ "result": true, "errors": [] }
POST /UserAgent/PinnedAgents
Lists the user's pinned agents. Returns the same composed shape as MyAgents — every field that appears on a MyAgents row appears here, just filtered to pinned: true.
No request body fields are required — the caller's tokenUUID (or active teamGUID header) scopes the response.
Response
{
"result": true,
"errors": [],
"useragents": [
{
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"uuid": "ada-uuid",
"agentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"teamguid": null,
"title": "My Instagram Bot",
"description": null,
"cover": null,
"categories": ["social-media", "marketing"],
"tier": "pro",
"tiermultiplier": 3,
"monthlypriceusd": 29,
"monthlycredits": 5000,
"extracredits": 2000,
"usedcredits": 1450,
"creditperiod": "2026-05",
"creditsyncat": 1714694410,
"status": 4,
"pinned": true,
"teamsessionmode": "",
"setuprequired": false,
"subscription": {
"plan": "agent",
"status": "active",
"amount": 29,
"currency": "usd",
"currentperiodend": 1717200000,
"renewaldate": "2026-06-01T00:00:00.000Z",
"daysremaining": 28,
"pendingdowngrade": null,
"provider": "prepaid"
},
"agent": {
"guid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"title": "Instagram Manager",
"slug": "instagram-manager",
"description": "An autonomous agent that manages your Instagram Business account.",
"cover": "https://cdn.wiro.ai/uploads/agents/instagram-manager-cover.webp",
"categories": ["social-media", "marketing"],
"tiermultiplier": 3,
"tiers": {
"starter": { "priceUsd": 9, "credits": 1000 },
"pro": { "priceUsd": 29, "credits": 5000 }
}
},
"extracreditsexpiry": 1730419200,
"createdat": 1714608000,
"updatedat": 1714694400,
"queuedat": 1714694395,
"startedat": 1714694400,
"runningat": 1714694410,
"stoppingat": null,
"stopdat": null,
"errordat": null
}
]
}
POST /UserAgent/PinnedUnread
Returns the latest message GUID per pinned agent. Compare each lastmessageguid against the last value you saw client-side to draw an unread badge.
Response
{
"result": true,
"errors": [],
"data": [
{ "useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321", "lastmessageguid": "5c41dabf-f2be-4aa8-a5a4-8c9e3d2f3f11" },
{ "useragentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890", "lastmessageguid": null }
]
}
Extra Credit Packs
Pro-tier instances can buy additional credits at any time. Pack catalogs are derived per-useragent (5x / 10x / 20x of the instance's monthly allocation), so the catalog auto-scales when the user upgrades from Starter → Pro or toggles paid skills.
The catalog appears at agent.extracreditpacks on UserAgent/Detail:
"extracreditpacks": [
{ "packkey": "small", "credits": 25000, "priceusd": 130, "enabled": true },
{ "packkey": "medium", "credits": 50000, "priceusd": 240, "enabled": true },
{ "packkey": "large", "credits": 100000, "priceusd": 420, "enabled": true }
]
POST /UserAgent/CreateExtraCreditCheckout
Purchases additional credits for a useragent. The wallet is debited the pack price and credits are added to the instance immediately.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | Your UserAgent instance guid |
pack |
string | Yes | Pack key from agent.extracreditpacks[].packkey — "small", "medium", or "large". |
useprepaid |
boolean | Yes | Must be true for API use. Pays the pack price from your wallet balance. |
Request
{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"pack": "medium",
"useprepaid": true
}
Response (prepaid)
{
"result": true,
"errors": []
}
The pack price is deducted from your wallet and credits are added to the instance immediately. Credits expire 6 months after purchase. The transaction is appended to the agent ledger (type: "purchase", action: "<pack>", provider: "prepaid") and surfaced on POST /UserAgent/TransactionList.
POST /UserAgent/CancelSubscription
Schedules the subscription to end at the current billing period's expiry — a cancel-at-period-end pattern. The agent keeps running with the full plan allowance until that moment. No wallet refund is issued for the remaining days; you're paying for the full period up front.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
Headers: Pass teamGUID: <team-guid> when the target agent belongs to a team project (the endpoint validates team context explicitly for all billing mutations).
Response
{
"result": true,
"cancelsAt": 1717200000,
"errors": []
}
cancelsAtis the Unix timestamp when the subscription will finish.- Server-side,
subscription.pendingdowngradeis flipped to"cancel"andsubscription.statusstays"active"until the period end. SubsequentUserAgent/Detailresponses reflect this:subscription.pendingdowngrade = "cancel"and the usualcurrentperiodend. - To reverse the cancellation before the period ends, call
POST /UserAgent/RenewSubscription— it clears the flag without charging the wallet again. - When the period actually ends, the daily cron marks the subscription
"expired"and stops the agent (status: 0). At that point the user must callPOST /UserAgent/RenewSubscriptionto pay for a new billing period (wallet charge), or redeploy from scratch.
Common errors
| Error | When |
|---|---|
No active subscription found for this agent |
status != "active" on the subscription row |
Stripe subscriptions must be cancelled via Stripe portal |
The active subscription was set up through the Wiro web dashboard with a card (e.g. by a teammate) — manage it from the Wiro dashboard instead. API-managed prepaid subscriptions never hit this path. |
User agent not found |
Caller doesn't own the useragent and isn't a team member |
POST /UserAgent/UpgradeTier
Upgrades the active subscription from Starter → Pro. The capability surface stays the same; the tier change just scales the credit pool and price by tiermultiplier.
Upgrade-only. Pro → Starter downgrades are rejected with an explicit error — cancel and redeploy at the lower tier instead.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
targetTier |
string | Yes | Must be "pro" ("starter" is rejected with the downgrade error). |
Headers: Pass teamGUID: <team-guid> when the target agent belongs to a team project.
When called against a prepaid subscription, the wallet is debited the prorated upgrade fee Math.max(0, ((P_pro − P_starter) / totalDays) × remainingDays) synchronously, the subscription + useragent are updated to Pro pricing / credits, and the agent is restarted to apply the new tier. Existing extra credits and usedcredits are preserved.
Response
{
"result": true,
"tier": "pro",
"previousTier": "starter",
"newPriceUsd": 29,
"newMonthlyCredits": 5000,
"proratedCharge": 11.33,
"stripeProrationApplied": false,
"errors": []
}
stripeProrationApplied is part of the standard contract and is always false for prepaid subscriptions (the prorated charge is taken from the wallet as proratedCharge instead).Common errors
| Error | When |
|---|---|
targetTier must be 'starter' or 'pro' |
targetTier not in ["starter", "pro"] |
Downgrading to Starter is not supported. /UserAgent/UpgradeTier is upgrade-only (Starter → Pro). |
targetTier: "starter" requested |
Already on {tier} tier |
Current tier matches the target — no-op |
No active subscription — use /UserAgent/CreateSubscriptionCheckout to subscribe at the chosen tier |
No active subscription on the useragent |
Failed to compute target-tier pricing |
Resolver couldn't compute the new tier's price (skill-registry drift) |
Insufficient wallet balance for proration. Required: $X.XX, available: $Y.YY |
Wallet (personal or team) can't cover the prorated charge |
POST /UserAgent/RenewSubscription
Two distinct operations share this endpoint depending on the subscription's current state:
- Undo-cancel — active subscription with
pendingdowngrade: "cancel"→ clears the flag, no wallet charge. - Renew — subscription in
"expired"status → creates a brand-new 30-day subscription, charges the wallet for the full plan price, resets the useragent tostatus: 0(Stopped).
The endpoint inspects the current subscription state and picks the right operation; the caller doesn't choose.
| Parameter | Type | Required | Description |
|---|---|---|---|
guid |
string | Yes | Your UserAgent instance guid |
Headers: Pass teamGUID: <team-guid> when the target agent belongs to a team project.
monthlypriceusd and monthlycredits are read straight off the useragent row (last touched by Deploy, SkillsApply, or UpgradeTier). Skill-registry weight changes between renewals do not propagate; the user keeps the price they last agreed to. To pick up new pricing, the user must explicitly call SkillsApply or UpgradeTier (both re-snapshot in the same transaction).
Response — undo-cancel
Called while the subscription is still "active" with a pending cancel flag set by CancelSubscription:
{
"result": true,
"action": "undo-cancel",
"errors": []
}
- Clears
subscription.pendingdowngradeback tonull. - No wallet charge — you've already paid for the full period.
- Agent status is unchanged (still running / stopped / whatever it was).
Response — renew
Called after the subscription has expired (daily cron flipped it to status: "expired" and stopped the agent). The renewal rolls the existing expired subscription forward into a fresh 30-day active row instead of inserting a new one:
{
"result": true,
"action": "renewed",
"plan": "agent",
"amount": 9,
"monthlycredits": 1000,
"errors": []
}
- Updates the
subscriptionsrow in place:status: "active",type: "renewal",currentperiodstart: now,currentperiodend: now + 30 days,pendingdowngrade: null. - Debits
amount(the snapshotmonthlypriceusd) from the wallet (caller's personal wallet, or the agent's team wallet if the agent is team-scoped). - Zeroes
usedcreditsand stamps the newcreditperiod. Existing extra credits are preserved. - If the agent was in
status: 0(Stopped) or5(Error), it's auto-queued back to2(Queued) — the runtime picks it up on the next cycle, no extraStartcall needed. Statuses1/2/3/4are mid-flight and settle on their own.
Common errors
| Error | When |
|---|---|
Subscription is already active |
Called on an active subscription with no pending cancel (nothing to do) |
No expired subscription found to renew |
No active sub and no expired sub — agent was never subscribed, or data is gone |
Stripe subscriptions must be managed via Stripe portal |
Active subscription was set up through the Wiro web dashboard with a card — undo a pending cancel from the Wiro dashboard. API-managed prepaid subscriptions never hit this path. |
Stripe subscriptions must be renewed via Stripe checkout |
Expired subscription was originally a card-based dashboard sub — re-subscribe from the Wiro dashboard, or call CreateSubscriptionCheckout with useprepaid: true to switch to wallet billing. |
Renewal pricing must be greater than $0. Add at least one paid skill or set agent base price. |
The persisted monthlypriceusd snapshot is $0 (custom build with all-free skills); add a paid skill via SkillsApply before renewing |
Insufficient wallet balance. Required: $X.XX, Available: $Y.YY |
Wallet (personal or team) can't cover the renewal price |
Wallet deduction failed: ... |
Wallet service returned an error during the debit |
Full subscription lifecycle (prepaid)
Deploy (wallet charged, period starts)
│
▼
┌────────────────────────┐
│ status: "active" │
│ pendingdowngrade: null │◄──────── RenewSubscription
└───────────┬────────────┘ (undo-cancel, no charge)
│ ▲
│ CancelSubscription │
▼ │
┌────────────────────────┐ │
│ status: "active" │──────────┘
│ pendingdowngrade: │
│ "cancel" │
└───────────┬────────────┘
│ currentperiodend reached
│ (daily cron)
▼
┌────────────────────────┐
│ status: "expired" │
│ useragent.status: 0 │◄───┐
└───────────┬────────────┘ │
│ │
│ RenewSubscription (wallet charged,
│ new 30-day period, useragent stays 0)
▼ │
┌────────────────────────┐ │
│ NEW subscription row │────┘
│ status: "active" │
│ type: "renewal" │
└────────────────────────┘
Credit Transactions
Every credit balance change (agent usage, subscription renewal, extra-credit purchase, refund, admin adjustment, cancellation) is appended to an immutable ledger. Full reference + extended examples on the dedicated Agent Transactions page; the endpoint is summarised below for convenience.
POST /UserAgent/TransactionList
Returns the ledger rows for a single useragent sorted newest-first plus a snapshot summary of the current balance.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string |
Yes | The useragent instance guid. |
limit |
number |
No | Max rows to return. Default 50, max 500. |
start |
number |
No | Offset for pagination. Default 0. |
Response
{
"result": true,
"errors": [],
"total": 128,
"summary": {
"monthlycredits": 5000,
"extracredits": 2000,
"usedcredits": 1450,
"remainingcredits": 5550,
"creditperiod": "2026-05",
"creditsyncat": 1714694410
},
"transactions": [
{
"guid": "7f3e8c21-1be1-4f5a-96e8-2b1a9e2a6a01",
"type": "deduct",
"action": "message",
"amount": -10,
"balanceafter": 5550,
"description": "Agent action: message",
"sessionkey": "default",
"messageguid": "5c41dabf-f2be-4aa8-a5a4-8c9e3d2f3f11",
"provider": "agent",
"providerref": null,
"metadata": null,
"uuid": "ada-uuid",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"createdat": 1714694400
},
{
"guid": "a40b5473-aedb-47d7-966b-7b038eed30dc",
"type": "purchase",
"action": "small",
"amount": 5000,
"balanceafter": 5560,
"description": "Extra credits — small pack",
"provider": "prepaid",
"providerref": null,
"metadata": { "pack": "small", "priceUsd": 45 },
"uuid": "ada-uuid",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"createdat": 1714692800
},
{
"guid": "312e2a5f-1002-4c9a-af7d-70571e8b1739",
"type": "renewal",
"action": "monthly",
"amount": 5000,
"balanceafter": 3560,
"description": "Subscription renewal",
"provider": "prepaid",
"providerref": null,
"metadata": { "period": "2026-05", "priceUsd": 29 },
"uuid": "system",
"user": null,
"createdat": 1714608000
}
]
}
The user object is the resolved actor that triggered the ledger entry — uuid, firstname, lastname, email, username, avatar, avatarinitials. It is null when uuid is a sentinel like "system" (cron renewals, subscription expiry sweeps, automated platform writes) or when the user record has been deleted. The renewal row above was written by the daily renewal cron, so uuid: "system" and user: null.
Summary fields mirror the flat useragent balance: monthlycredits, extracredits, usedcredits, remainingcredits = max(0, monthlycredits + extracredits - usedcredits), creditperiod (the 'YYYY-MM' billing window), and creditsyncat (Unix seconds of the last agent → API usage sync, null before the first report).
Transaction fields
| Field | Type | Description |
|---|---|---|
guid |
string |
Stable id of the ledger row. |
type |
string |
"deduct", "renewal", "purchase", "refund", "grant", or "cancel". |
action |
string|null |
Fine-grained detail. Values depend on type: "message", "create", "modify", "regenerate" (deduct); "monthly" (renewal); "small", "medium", "large" (purchase); "subscription" (cancel / refund); "upgrade", "admin", "skill-toggle" (grant). |
amount |
number |
Signed credit delta — negative for deductions, positive for grants. |
balanceafter |
number|null |
Remaining credit balance snapshot written at the time of the event. |
description |
string|null |
Human-readable label. |
sessionkey |
string|null |
Session the deduct belongs to (deduct rows only). |
messageguid |
string|null |
Agent message that triggered the deduct (deduct rows only). |
provider |
string|null |
"agent", "stripe", or "prepaid". |
providerref |
string|null |
Stripe session / invoice / payment-intent id (for Stripe-originated events). |
metadata |
object|null |
Arbitrary JSON context attached at write time (plan, period, price, etc.). |
createdat |
number |
Unix seconds. |
Access: owner uuid or any team member of the useragent's team. Admin callers bypass the uuid check. Events are append-only — there is no delete endpoint, and the ledger guid is used server-side to deduplicate retries.
Pricing Summary
| Component | Source | Notes |
|---|---|---|
| Tier price | agent.tiers.{starter,pro}.price |
Snapshotted on the useragent at deploy as monthlypriceusd. Pro = Starter × tiermultiplier. |
| Monthly credits | agent.tiers.{starter,pro}.credits |
Snapshotted on the useragent at deploy as monthlycredits. Pro = Starter × tiermultiplier. |
| Per-action costs | peractioncosts (computed) |
max() across enabled skills' pricing.credit_costs. Same value for Starter and Pro. |
| Tier multiplier | agent.tiermultiplier |
Default 3. Snapshotted on the useragent at deploy so admin tweaks don't retroactively change live instances. |
| Extra credit packs | agent.extracreditpacks[] |
Per-useragent — derived as 5x / 10x / 20x of monthlycredits. Pro tier only (Starter has empty array). |
Payment Method
All API subscriptions use your prepaid wallet balance. The cost is deducted immediately when you deploy, renew, upgrade, or buy an extra credit pack. Always pass useprepaid: true on Deploy, CreateSubscriptionCheckout, and CreateExtraCreditCheckout — that's the only path designed for API consumers. Make sure your wallet balance covers the upfront tier price before calling these endpoints; otherwise you'll get an Insufficient wallet balance error.
Credit Consumption
Credits are consumed by the agent runtime (container) as it processes messages and generates content — not at Message/Send time. The peractioncosts object on the useragent declares how many credits each action burns; the container reports usage back to Wiro asynchronously through POST /UserAgent/CreditSync (and per-deduction via POST /UserAgent/TransactionInsert), which updates usedcredits and derives the live remainingcredits = max(0, monthlycredits + extracredits - usedcredits).
The API-side check is gating only: POST /UserAgent/Start and POST /UserAgent/Message/Send refuse to launch / accept messages when remainingcredits <= 0. During an active session, monthly credits are consumed first; once usedcredits >= monthlycredits, extra credits (purchased via CreateExtraCreditCheckout) absorb the rest. When both pools are empty, the agent responds [SYSTEM_CREDIT_LIMIT_REACHED] and stops processing further actions.
On each billing cycle (subscription renewal) usedcredits is reset to zero and creditperiod advances to the new 'YYYY-MM' window; extra-credit purchases simply bump extracredits without resetting usage. For the full audit trail (every credit deduct / grant / purchase / renewal / cancel) call POST /UserAgent/TransactionList — see Agent Transactions.
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 |
Subscription price must be greater than $0. Add at least one paid skill or set agent base price. |
Custom build with a $0 skill set — toggle on at least one paid skill |
Agent setup is not complete. Please fill in your credentials before starting. |
Status is 6 — call CredentialUpsert / SkillsApply / CustomSkillUpsert to provide required values |
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 |
Skill changes are only available for custom-built agents. |
SkillsApply called on a template-deploy useragent. Skills are inherited from the template — to change them, deploy a custom build (Deploy with custom: true) instead. |
Subscription already active for this useragent. Cancel or modify the existing subscription instead. |
CreateSubscriptionCheckout called when a sub already exists |
Insufficient wallet balance for proration. Required: $X.XX, available: $Y.YY |
SkillsApply — wallet can't cover the prorated charge |
Downgrading to Starter is not supported. /UserAgent/UpgradeTier is upgrade-only (Starter → Pro). |
UpgradeTier with targetTier: "starter" |
Already on {tier} tier |
UpgradeTier when current tier matches the target — no-op |
No active subscription — use /UserAgent/CreateSubscriptionCheckout to subscribe at the chosen tier |
UpgradeTier called without an active sub |
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. Response includes agentstatus (the integer) so the FE can branch. |
Agent has no remaining credits. Renew your subscription or buy a credit pack to continue. |
Message/Send while remainingcredits <= 0. Response includes agentbalance for the FE to render a "Buy credits" CTA. |
Message not found |
Detail / Cancel with invalid messageguid |
Message cannot be cancelled (status: {status}) |
Cancel on a message that's already in a terminal state |
Invalid redirect URL |
OAuth Connect with non-HTTPS URL |
No active subscription found for this agent |
CancelSubscription called with no active subscription |
Stripe subscriptions must be cancelled via Stripe portal |
CancelSubscription on a card-based dashboard subscription — manage from the Wiro web dashboard. API-managed prepaid subscriptions never hit this path. |
Stripe subscriptions must be managed via Stripe portal |
RenewSubscription undo-cancel on a card-based dashboard subscription — undo from the Wiro web dashboard. |
Stripe subscriptions must be renewed via Stripe checkout |
RenewSubscription on a card-based dashboard subscription — re-subscribe from the Wiro dashboard, or call CreateSubscriptionCheckout with useprepaid: true to switch to wallet billing. |
Subscription is already active |
RenewSubscription called on an active subscription without pending cancel |
No expired subscription found to renew |
RenewSubscription called with no expired subscription |
Insufficient wallet balance. Required: $X.XX, Available: $Y.YY |
RenewSubscription — wallet can't cover the renewal price |
Wallet deduction failed: ... |
Wallet service returned an error during the debit |
Pack key required |
CreateExtraCreditCheckout without pack |
Could not retrieve wallet balance |
Wallet service lookup failed while checking balance |
Renewal pricing not available |
Resolver couldn't recompute price (skill registry drift); fix the agent's skill set or contact support |
What's Next
- Agent Builder — Build custom agents from scratch (
custom: true) with live pricing previews and skill picker - Agent Skills — Configure preferences, scheduled tasks, and skill toggles. Includes the registry browser endpoints (
Skills/List,Skills/Detail,Skills/Capabilities) - Agent Credentials — Integration catalog hub plus the registry endpoints (
Credentials/List,Credentials/Detail) - Agent Messaging — Send messages and receive responses from running agents
- Agent WebSocket — Real-time response streaming
- Agent Webhooks — Receive agent responses via HTTP callbacks
- Agent Transactions — Per-instance credit ledger (deductions, renewals, purchases, grants, refunds)
- Agent Logs — Per-instance activity feed (tool calls, cron runs, message exchanges)
- Authentication — API key setup and authentication methods
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 runtime. 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": "ada-uuid",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"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. |
agenttoken |
string|null |
The same token issued by Message/Send for this message. Lets callers that arrived at the row through Message/Detail (or Message/History) subscribe to the Agent WebSocket and pick up an in-flight response — useful when a chat UI rehydrates after a reload and finds a message still in agent_queue / agent_start / agent_output status. Only null for very old rows that pre-date the column. |
user |
object|null |
Resolved sender info: { uuid, firstname, lastname, email, username, avatar, avatarinitials }. Decorated server-side from agentmessages.uuid so the chat bubble can render avatar / hover-tooltip without an extra User/Detail round-trip. null when the row was written by automation (sentinel uuid like "system") or when the user record was deleted. |
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",
"uuid": "ada-uuid",
"agenttoken": "aB3xK9mR2pLqWzVn7tYhCd5sFgJkNb",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"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",
"uuid": "ada-uuid",
"agenttoken": "tQ4nL8vY1zMkRpWdH7cXjBg6sFhPmA",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"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
}
}
Each message row carries uuid (sender) and a server-decorated user object with the full sender shape — same as Message/Detail. user is null for system-inserted rows (e.g. Message/SystemInsert writes when uuid is "system") or when the underlying account has been deleted.
Resuming an in-flight stream. Every row carries the original agenttoken from Message/Send. If a returned message's status is non-terminal (agent_queue, agent_start, or agent_output), the agent is still processing it server-side. Hand the agenttoken to the Agent WebSocket (agent_info frame) and you'll receive the remaining agent_output chunks plus the eventual agent_end event — no need to re-send the message. This is exactly how a chat UI can rehydrate live streams after a page reload.
| Field | Type | Description |
|---|---|---|
messages |
array |
Array of message objects, newest first. Each row has the same shape as Message/Detail — including the uuid sender, the agenttoken (so you can resubscribe over WebSocket if status is non-terminal), and the decorated user object. |
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": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"sessionkey": "default",
"limit": 50
}
Page 2 — pass the last (oldest, smallest createdat) message's guid from page 1 as the before cursor:
{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"sessionkey": "default",
"limit": 50,
"before": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
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/Delete
Bulk-deletes one or more messages from a session, with per-side soft-delete semantics. Each item lets you choose whether to hide the user side of the bubble, the agent side, or both — supporting "delete just my message" / "delete only the response" / "delete the whole bubble" UX without losing the underlying record.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | The agent instance GUID. |
items | array | Yes | One or more { messageguid, side } rows. |
Each item:
| Field | Type | Description |
|---|---|---|
messageguid | string | The message to delete. Must belong to the agent identified by useragentguid. |
side | string | One of "user", "agent", or "both". Maps to a bitmask (user=1, agent=2, both=3) OR'd into the row's deletestatus column. |
deletestatus flag set so the chat history can re-render the redacted bubble. Message/History filters out fully-deleted (deletestatus < 3) rows so users don't see the gaps. To wipe the whole conversation hard-and-fast, use Message/DeleteSession instead.Request
curl -X POST "https://api.wiro.ai/v1/UserAgent/Message/Delete" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"items": [
{ "messageguid": "c3d4e5f6-...", "side": "user" },
{ "messageguid": "d4e5f6a7-...", "side": "agent" },
{ "messageguid": "e5f6a7b8-...", "side": "both" }
]
}'
Response
{ "result": true, "errors": [] }
The endpoint is best-effort across the items array — invalid messageguid rows or invalid side values are silently skipped. The call returns result: true even when zero rows were updated, so callers should refetch Message/History to confirm the new state.
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).
POST /UserAgent/Message/SystemInsert
Inserts a finished "system" message into the conversation history without running it through the agent. Used by the agent runtime, scheduled cron skills, and external chat-platform bridges (Telegram bot, Slack relay, push-notification webhooks) to drop a pre-rendered message into a session as if the agent had produced it. The endpoint never triggers a model call — the supplied content is written verbatim to agentmessages.response with status: "agent_end" and metadata: {"type":"system"}.
Message/Send instead, which goes through the standard auth flow + queue + model call path.| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | The target useragent guid. |
uuid |
string | Yes | The useragent owner uuid. Must match the row stored in useragents.uuid for useragentguid. |
content |
string | Yes | The message body to insert. Stored verbatim in response and debugoutput. |
sessionkey |
string | No | Conversation thread the message belongs to. Defaults to "auto" — the endpoint resolves it to the most recent session for this useragent (falls back to "default" for empty histories). Pass an explicit value to insert into a specific named session. |
Response
{
"result": true,
"messageguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"sessionkey": "default",
"errors": []
}
messageguidis the inserted row's guid — use it to address the message later viaMessage/Detailor to thread replies.sessionkeyechoes the resolved session (matters when the caller passed"auto"or omitted the field).- The inserted row carries
status: "agent_end"andmetadata: {"type":"system"}so the chat UI renders it as a non-interactive system bubble (no retry / cancel affordances).
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": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"message": "Hello!",
"sessionkey": "user-alice"
}
User B's separate conversation with the same agent:
{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"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. Internal failure messages are recorded in debugoutput (retrievable via POST /UserAgent/Message/Detail) but replaced with the generic sentence above before being pushed to subscribed clients. Log the raw debugoutput for your own debugging; show the sanitized string from the WebSocket event to end users.
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 message field 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. Use type === "agent_cancel" as the canonical signal. Subscribers that cancel from a queued state receive no event at all (the message is simply marked agent_cancel in the database; check with
POST /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. Browse the platform's credential registry, set API keys + OAuth, and audit credential edits over time.
| Operation | Endpoint |
|---|---|
| Browse all credentials in the registry | POST /Credentials/List (public) |
| Inspect a single credential schema | POST /Credentials/Detail (public) |
| Find the credential a skill needs | POST /Skills/CredentialSchema (public) |
| Write one or more credential fields | POST /UserAgent/CredentialUpsert |
| Read version history of a credential | POST /UserAgent/CredentialFieldHistory |
Credential Registry Endpoints
These endpoints are public — no authentication required. They return data straight from the credential registry — useful when you want to render a "Connect this provider" form without a deployed useragent (e.g. inside an onboarding wizard).
POST /Credentials/List
Lists all credentials in the registry.
| Parameter | Type | Required | Description |
|---|---|---|---|
credential_mode | string | No | Filter by mode: "oauth", "sa" (service account), "api_key", "multi_api_key", "hybrid", "imap_credentials", "jwt_sa", "rule_only". |
wiro_connect_pending | boolean | No | Filter by the "Wiro mode coming soon" flag. |
Response
{
"result": true,
"errors": [],
"total": 22,
"credentials": [
{
"key": "instagram",
"title": "Instagram",
"icon": "/images/icons/skills/instagram.svg",
"brand_color": "#e4405f",
"brand_text_color": "#ffffff",
"brand_logo_filter": "none",
"docs_url": "integration-instagram-skills",
"credential_mode": "oauth",
"connection_modes": ["wiro", "own"],
"wiro_connect_pending": true,
"credential_schema": [
{
"key": "appid",
"type": "text",
"label": "App ID",
"required": true,
"pattern": "^[0-9]+$",
"help": "Meta for Developers → My Apps → Create App → copy App ID. Add Facebook Login product and OAuth Redirect URI: https://api.wiro.ai/v1/UserAgentOAuth/IGCallback",
"oauth_managed": true,
"only_in_modes": ["own"]
},
{
"key": "appsecret",
"type": "password",
"label": "App Secret",
"required": true,
"show_toggle": true,
"help": "Meta for Developers → App Settings → Basic → Show next to App Secret.",
"oauth_managed": true,
"only_in_modes": ["own"]
},
{
"key": "igusername",
"type": "text",
"label": "Connected Account",
"required": false,
"auto_filled_by_oauth": true,
"readonly_when_connected": true
}
],
"oauth_provider": {
"auth_method_value": "wiro",
"connect_endpoint": "/UserAgentOAuth/IGConnect",
"disconnect_endpoint": "/UserAgentOAuth/IGDisconnect",
"status_endpoint": "/UserAgentOAuth/IGStatus",
"connect_button_label": "Connect with Instagram",
"connect_button_icon": "/images/icons/skills/instagram.svg",
"connect_button_brand_color": "#e4405f",
"connect_button_text_color": "#ffffff",
"connect_button_logo_filter": "none",
"username_field": "igusername",
"return_query_param": "ig_connected",
"return_error_param": "ig_error",
"return_error_detail_param": "ig_error_detail",
"account_picker": null,
"extra_step": null
},
"used_by_skills": ["int-instagram-post"]
}
]
}
fieldstatus values
fieldstatus is not part of the registry schema — it's the runtime classification applied to each credential field at write time. It controls who can see the value:
| Value | Who writes it | Visible to API caller? |
|---|---|---|
user | API callers + UI users | Yes |
oauth_app | API callers (own-mode only) | Yes (live), redacted to [REDACTED] in history |
oauth_session | OAuth callback (server-only) | No — always stripped from responses |
oauth_picker | OAuth callback / Set* picker endpoints | Yes |
platform | Internal (platform-managed) | No — stripped for user-role callers |
computed | Server-derived | Yes |
control | Internal (platform-managed) | No — stripped for user-role callers |
POST /Credentials/Detail
Returns a single credential entry by key. The response is the same full credential entry as a row from Credentials/List — every field above is present.
| Parameter | Type | Required | Description |
|---|---|---|---|
key | string | Yes | Canonical credential key (e.g. "instagram", "google-ads", "sys-telegram", "var-website"). |
Response
{
"result": true,
"errors": [],
"credential": {
"key": "instagram",
"title": "Instagram",
"icon": "/images/icons/skills/instagram.svg",
"brand_color": "#e4405f",
"brand_text_color": "#ffffff",
"brand_logo_filter": "none",
"docs_url": "integration-instagram-skills",
"credential_mode": "oauth",
"connection_modes": ["wiro", "own"],
"wiro_connect_pending": true,
"credential_schema": [
{
"key": "appid",
"type": "text",
"label": "App ID",
"required": true,
"pattern": "^[0-9]+$",
"help": "Meta for Developers → My Apps → Create App → copy App ID.",
"oauth_managed": true,
"only_in_modes": ["own"]
},
{
"key": "appsecret",
"type": "password",
"label": "App Secret",
"required": true,
"show_toggle": true,
"help": "Meta for Developers → App Settings → Basic → Show next to App Secret.",
"oauth_managed": true,
"only_in_modes": ["own"]
},
{
"key": "igusername",
"type": "text",
"label": "Connected Account",
"required": false,
"auto_filled_by_oauth": true,
"readonly_when_connected": true
}
],
"oauth_provider": {
"auth_method_value": "wiro",
"connect_endpoint": "/UserAgentOAuth/IGConnect",
"disconnect_endpoint": "/UserAgentOAuth/IGDisconnect",
"status_endpoint": "/UserAgentOAuth/IGStatus",
"connect_button_label": "Connect with Instagram",
"connect_button_icon": "/images/icons/skills/instagram.svg",
"connect_button_brand_color": "#e4405f",
"connect_button_text_color": "#ffffff",
"connect_button_logo_filter": "none",
"username_field": "igusername",
"return_query_param": "ig_connected",
"return_error_param": "ig_error",
"return_error_detail_param": "ig_error_detail",
"account_picker": null,
"extra_step": null
},
"used_by_skills": ["int-instagram-post"]
}
}
Returns { "result": false, "errors": [{ "code": 404, "message": "Credential not found: <key>" }] } if the key is unknown.
Auditing Credential Changes — CredentialFieldHistory
Every credential field write is appended to a versioned history. Use POST /UserAgent/CredentialFieldHistory to read it — same idea as CustomSkillHistory for skills.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialFieldHistory" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"credentialkey": "instagram"
}'
Response
{
"result": true,
"errors": [],
"entries": [
{
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"useragentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"credentialkey": "instagram",
"fieldname": "igusername",
"fieldvalue": "myaccount",
"fieldstatus": "user",
"parentfield": null,
"ordinal": 0,
"operation": "upsert",
"changedby": "ada-uuid",
"changedby_user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"changedat": 1714694410
},
{
"guid": "b2c3d4e5-f6a7-8901-bcde-f23456789012",
"useragentguid": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"credentialkey": "instagram",
"fieldname": "clientsecret",
"fieldvalue": "[REDACTED]",
"fieldstatus": "oauth_app",
"parentfield": null,
"ordinal": 0,
"operation": "upsert",
"changedby": "ada-uuid",
"changedby_user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"changedat": 1714600000
}
]
}
changedby_user is the resolved actor object (uuid, firstname, lastname, email, username, avatar, avatarinitials). It is null when changedby is "system" (automation / cron) or when the user record has been deleted.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Your UserAgent instance guid |
credentialkey | string | Yes | Provider key (e.g. "instagram", "wordpress") |
startdate | number | No | UTC epoch seconds — return entries on/after this time |
enddate | number | No | UTC epoch seconds — return entries on/before this time |
oauth_session rows (access/refresh tokens) never appear in history at all (they're stripped before persisting). clientsecret is stored as [REDACTED] in history rows; the live row carries the real secret. Use POST /UserAgent/Detail to read the current live values; CredentialFieldHistory only shows the audit trail.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.
- YouTube — Wiro mode + own mode. Shares OAuth client with Google Ads. See Google Ads Skills.
- Google Analytics 4 — Wiro mode + own mode. Shares OAuth client with Google Ads. See Google Ads Skills.
- Merchant Center — Wiro mode + own mode. Shares OAuth client with Google Ads. See Google Ads Skills.
- HubSpot — Wiro mode + own mode. CRM object management.
- Mailchimp — Wiro mode + own mode + direct API key.
Service Account Integrations
- Google Drive — Per-user service account + scoped folder sharing.
- Google Play — Per-user service account + Play Console permissions.
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.
- 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:
credentials— which services the agent connects to and their readiness flags (_connected,optional,extra)customskills— 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 write credentials via POST /UserAgent/CredentialUpsert (flat fields[] array), update custom skill values via POST /UserAgent/CustomSkillUpsert, and toggle integration skills via POST /UserAgent/SkillsApply.
Two Types of Credentials
- API Key credentials — set directly via
POST /UserAgent/CredentialUpsert(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/CredentialUpsert with a flat fields[] array to write credentials. Use POST /UserAgent/CustomSkillUpsert to update custom skill values (strategy text or cron intervals). Use POST /UserAgent/SkillsApply to enable/disable integration skills.
Request Format
{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "<service>", "fieldname": "<field>", "fieldvalue": "value" }
]
}
POST /UserAgent/Detail first to see which fields each credential exposes, and the _connected / optional / extra flags. Only fields with fieldstatus: "user" may be written by API callers — attempts to write OAuth-session or platform fields are rejected with agent-fieldstatus-not-allowed-for-role.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/CredentialUpsert 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-pages |
OAuth | Connected via FBConnect. |
linkedin |
OAuth | Connected via LIConnect. Also set organizationid. |
tiktok |
OAuth | Connected via TikTokConnect. |
gmail |
API Key | account, apppassword |
sys-telegram |
API Key | bottoken, allowedusers, sessionmode |
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "gmail", "fieldname": "account", "fieldvalue": "[email protected]" },
{ "credentialkey": "gmail", "fieldname": "apppassword", "fieldvalue": "xxxx xxxx xxxx xxxx" },
{ "credentialkey": "sys-telegram", "fieldname": "bottoken", "fieldvalue": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" },
{ "credentialkey": "sys-telegram", "fieldname": "allowedusers", "fieldvalue": "[\"761381461\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "sessionmode", "fieldvalue": "private" }
]
}'
gmail and sys-telegram.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 |
sys-telegram |
bottoken, allowedusers, sessionmode |
Telegram bot for operator notifications |
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "wordpress", "fieldname": "url", "fieldvalue": "https://blog.example.com" },
{ "credentialkey": "wordpress", "fieldname": "user", "fieldvalue": "WiroBlogAgent" },
{ "credentialkey": "wordpress", "fieldname": "apppassword", "fieldvalue": "xxxx xxxx xxxx xxxx" },
{ "credentialkey": "gmail", "fieldname": "account", "fieldvalue": "[email protected]" },
{ "credentialkey": "gmail", "fieldname": "apppassword", "fieldvalue": "xxxx xxxx xxxx xxxx" },
{ "credentialkey": "sys-telegram", "fieldname": "bottoken", "fieldvalue": "123456:ABC-DEF1234ghIkl" },
{ "credentialkey": "sys-telegram", "fieldname": "allowedusers", "fieldvalue": "[\"761381461\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "sessionmode", "fieldvalue": "private" }
]
}'
Monitors and replies to App Store and Google Play reviews.
| Service | Fields | Description |
|---|---|---|
apple-appstore |
keyid, issuerid, privatekeybase64, appids, supportemail |
App Store Connect API credentials |
google-play |
serviceaccountjsonbase64, packagenames, supportemail |
Google Play service account |
sys-telegram |
bottoken, allowedusers, sessionmode |
Telegram bot for operator notifications |
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "apple-appstore", "fieldname": "keyid", "fieldvalue": "ABC1234DEF" },
{ "credentialkey": "apple-appstore", "fieldname": "issuerid", "fieldvalue": "12345678-1234-1234-1234-123456789012" },
{ "credentialkey": "apple-appstore", "fieldname": "privatekeybase64", "fieldvalue": "LS0tLS1CRUdJTi..." },
{ "credentialkey": "apple-appstore", "fieldname": "appids", "fieldvalue": "[\"6479306352\"]" },
{ "credentialkey": "apple-appstore", "fieldname": "supportemail", "fieldvalue": "[email protected]" },
{ "credentialkey": "google-play", "fieldname": "serviceaccountjsonbase64", "fieldvalue": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii..." },
{ "credentialkey": "google-play", "fieldname": "packagenames", "fieldvalue": "[\"com.example.app\"]" },
{ "credentialkey": "google-play", "fieldname": "supportemail", "fieldvalue": "[email protected]" },
{ "credentialkey": "sys-telegram", "fieldname": "bottoken", "fieldvalue": "123456:ABC-DEF1234ghIkl" },
{ "credentialkey": "sys-telegram", "fieldname": "allowedusers", "fieldvalue": "[\"761381461\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "sessionmode", "fieldvalue": "private" }
]
}'
Suggests and creates App Store in-app events based on holidays and trends.
| Service | Fields | Description |
|---|---|---|
apple-appstore |
keyid, issuerid, privatekeybase64, appids |
App Store Connect API credentials |
sys-telegram |
bottoken, allowedusers, sessionmode |
Telegram bot for operator notifications |
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "apple-appstore", "fieldname": "keyid", "fieldvalue": "ABC1234DEF" },
{ "credentialkey": "apple-appstore", "fieldname": "issuerid", "fieldvalue": "12345678-1234-1234-1234-123456789012" },
{ "credentialkey": "apple-appstore", "fieldname": "privatekeybase64", "fieldvalue": "LS0tLS1CRUdJTi..." },
{ "credentialkey": "apple-appstore", "fieldname": "appids", "fieldvalue": "[\"6479306352\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "bottoken", "fieldvalue": "123456:ABC-DEF1234ghIkl" },
{ "credentialkey": "sys-telegram", "fieldname": "allowedusers", "fieldvalue": "[\"761381461\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "sessionmode", "fieldvalue": "private" }
]
}'
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}) |
sys-telegram |
bottoken, allowedusers, sessionmode |
Telegram bot for operator notifications |
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "firebase", "parentfield": "accounts", "ordinal": 0, "fieldname": "appname", "fieldvalue": "My App" },
{ "credentialkey": "firebase", "parentfield": "accounts", "ordinal": 0, "fieldname": "serviceaccountjsonbase64", "fieldvalue": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii..." },
{ "credentialkey": "firebase", "parentfield": "accounts.0.apps", "ordinal": 0, "fieldname": "platform", "fieldvalue": "ios" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.apps", "ordinal": 0, "fieldname": "id", "fieldvalue": "6479306352" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.apps", "ordinal": 1, "fieldname": "platform", "fieldvalue": "android" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.apps", "ordinal": 1, "fieldname": "id", "fieldvalue": "com.example.app" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.topics", "ordinal": 0, "fieldname": "topickey", "fieldvalue": "locale_en" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.topics", "ordinal": 0, "fieldname": "topicdesc", "fieldvalue": "English-speaking users" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.topics", "ordinal": 1, "fieldname": "topickey", "fieldvalue": "tier_paid" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.topics", "ordinal": 1, "fieldname": "topicdesc", "fieldvalue": "Paid subscribers" },
{ "credentialkey": "sys-telegram", "fieldname": "bottoken", "fieldvalue": "123456:ABC-DEF1234ghIkl" },
{ "credentialkey": "sys-telegram", "fieldname": "allowedusers", "fieldvalue": "[\"761381461\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "sessionmode", "fieldvalue": "private" }
]
}'
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 |
sys-telegram |
API Key | bottoken, allowedusers, sessionmode |
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "brevo", "fieldname": "apiKey", "fieldvalue": "xkeysib-abc123..." },
{ "credentialkey": "sendgrid", "fieldname": "apiKey", "fieldvalue": "SG.xxxx..." },
{ "credentialkey": "newsletter", "fieldname": "testemail", "fieldvalue": "[email protected]" },
{ "credentialkey": "sys-telegram", "fieldname": "bottoken", "fieldvalue": "123456:ABC-DEF1234ghIkl" },
{ "credentialkey": "sys-telegram", "fieldname": "allowedusers", "fieldvalue": "[\"761381461\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "sessionmode", "fieldvalue": "private" }
]
}'
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 |
sys-telegram |
API Key | bottoken, allowedusers, sessionmode |
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "apollo", "fieldname": "apiKey", "fieldvalue": "your-apollo-api-key" },
{ "credentialkey": "apollo", "fieldname": "masterapikey", "fieldvalue": "your-master-key" },
{ "credentialkey": "lemlist", "fieldname": "apiKey", "fieldvalue": "your-lemlist-key" },
{ "credentialkey": "sys-telegram", "fieldname": "bottoken", "fieldvalue": "123456:ABC-DEF1234ghIkl" },
{ "credentialkey": "sys-telegram", "fieldname": "allowedusers", "fieldvalue": "[\"761381461\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "sessionmode", "fieldvalue": "private" }
]
}'
Manages Google Ads campaigns, keywords, and ad copy.
| Service | Type | Fields |
|---|---|---|
google-ads |
OAuth + Config | OAuth via GAdsConnect, then set customerid via GAdsSetCustomerId. Also set developertoken and managercustomerid for "own" mode. |
var-website |
Config | urls — array of { websitename, url } |
apple-appstore |
Config | apps — array of { appname, appid } |
google-play |
Config | apps — array of { appname, packagename } |
sys-telegram |
API Key | bottoken, allowedusers, sessionmode |
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "var-website", "parentfield": "urls", "ordinal": 0, "fieldname": "websitename", "fieldvalue": "Main Site" },
{ "credentialkey": "var-website", "parentfield": "urls", "ordinal": 0, "fieldname": "url", "fieldvalue": "https://example.com" },
{ "credentialkey": "apple-appstore-apps", "parentfield": "apps", "ordinal": 0, "fieldname": "appname", "fieldvalue": "My iOS App" },
{ "credentialkey": "apple-appstore-apps", "parentfield": "apps", "ordinal": 0, "fieldname": "appid", "fieldvalue": "6479306352" },
{ "credentialkey": "google-play-apps", "parentfield": "apps", "ordinal": 0, "fieldname": "appname", "fieldvalue": "My Android App" },
{ "credentialkey": "google-play-apps", "parentfield": "apps", "ordinal": 0, "fieldname": "packagename", "fieldvalue": "com.example.app" },
{ "credentialkey": "sys-telegram", "fieldname": "bottoken", "fieldvalue": "123456:ABC-DEF1234ghIkl" },
{ "credentialkey": "sys-telegram", "fieldname": "allowedusers", "fieldvalue": "[\"761381461\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "sessionmode", "fieldvalue": "private" }
]
}'
POST /UserAgentOAuth/GAdsSetCustomerId.Manages Meta (Facebook/Instagram) ad campaigns and creatives.
| Service | Type | Fields |
|---|---|---|
meta-ads |
OAuth + Config | OAuth via MetaAdsConnect, then set ad account via MetaAdsSetAdAccount. Also set pageid for Facebook page association. |
var-website |
Config | urls — array of { websitename, url } |
apple-appstore |
Config | apps — array of { appname, appid } |
google-play |
Config | apps — array of { appname, packagename } |
sys-telegram |
API Key | bottoken, allowedusers, sessionmode |
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "var-website", "parentfield": "urls", "ordinal": 0, "fieldname": "websitename", "fieldvalue": "Landing Page" },
{ "credentialkey": "var-website", "parentfield": "urls", "ordinal": 0, "fieldname": "url", "fieldvalue": "https://example.com" },
{ "credentialkey": "apple-appstore-apps", "parentfield": "apps", "ordinal": 0, "fieldname": "appname", "fieldvalue": "My iOS App" },
{ "credentialkey": "apple-appstore-apps", "parentfield": "apps", "ordinal": 0, "fieldname": "appid", "fieldvalue": "6479306352" },
{ "credentialkey": "google-play-apps", "parentfield": "apps", "ordinal": 0, "fieldname": "appname", "fieldvalue": "My Android App" },
{ "credentialkey": "google-play-apps", "parentfield": "apps", "ordinal": 0, "fieldname": "packagename", "fieldvalue": "com.example.app" },
{ "credentialkey": "sys-telegram", "fieldname": "bottoken", "fieldvalue": "123456:ABC-DEF1234ghIkl" },
{ "credentialkey": "sys-telegram", "fieldname": "allowedusers", "fieldvalue": "[\"761381461\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "sessionmode", "fieldvalue": "private" }
]
}'
POST /UserAgentOAuth/MetaAdsSetAdAccount.Credential Field Reference
Quick reference for all credential field names across services:
| Service Key | Editable Fields |
|---|---|
sys-telegram |
bottoken, allowedusers, sessionmode |
wordpress |
url, user, apppassword |
gmail |
account, apppassword |
brevo |
apiKey |
sendgrid |
apiKey |
apollo |
apiKey, masterapikey |
lemlist |
apiKey |
newsletter |
testemail |
apple-appstore |
keyid, issuerid, privatekeybase64, appids — or apps array for ads agents |
google-play |
serviceaccountjsonbase64, packagenames — or apps array for ads agents |
firebase |
accounts[]: appname, serviceaccountjsonbase64, apps [{platform,id}], topics [{topickey,topicdesc}] |
var-website |
urls array of { websitename, url } |
twitter |
OAuth — authmethod (own: + clientid, clientsecret) |
instagram |
OAuth — authmethod (own: + appid, appsecret) |
facebook-pages |
OAuth — authmethod (own: + appid, appsecret) |
linkedin |
OAuth — authmethod, organizationid (own: + clientid, clientsecret) |
tiktok |
OAuth — authmethod (own: + clientkey, clientsecret) |
google-ads |
OAuth — authmethod, customerid, developertoken, managercustomerid (own: + clientid, clientsecret) |
meta-ads |
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/CredentialUpsert (include { credentialkey, fieldname: "authmethod", fieldvalue: "own" } in the fields[] array), 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 Pages | facebook-pages |
appid, appsecret |
|
linkedin |
clientid, clientsecret, organizationid |
|
|
| Google Ads | google-ads |
clientid, clientsecret, developertoken, managercustomerid |
|
| Meta Ads | meta-ads |
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 via CredentialUpsert (include authmethod: "own" in the same call):
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "twitter", "fieldname": "clientid", "fieldvalue": "your-twitter-client-id" },
{ "credentialkey": "twitter", "fieldname": "clientsecret", "fieldvalue": "your-twitter-client-secret" },
{ "credentialkey": "twitter", "fieldname": "authmethod", "fieldvalue": "own" }
]
}'
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
The shape is consistent across providers — only the identity field name changes (some providers expose the connected account under username, others under a provider-specific key like linkedinname, customerid, merchantid):
{
"result": true,
"errors": [],
"connected": true,
"username": "yourbrand",
"connectedat": "1714694410",
"tokenexpiresat": "1719878410",
"refreshtokenexpiresat": "1730073610"
}
| Field | Type | Description | Providers |
|---|---|---|---|
connected |
boolean |
true when Wiro has a valid access token AND every required picker field (ad-account id, page id, customer id, channel id, merchant id, GA4 property id, …) is populated. |
All |
connectedat |
string |
Unix-seconds timestamp of the last successful Connect / token refresh. Empty string when never connected. | All |
tokenexpiresat |
string |
Unix-seconds when the current access token expires. Empty for Mailchimp (no expiry). | All except Mailchimp |
refreshtokenexpiresat |
string |
Unix-seconds when the refresh token expires. Only set for providers that issue refresh tokens. | Twitter / X, TikTok, LinkedIn |
username |
string |
Connected account identifier. Replaced by a provider-specific key on some providers (see below). | Twitter / X, TikTok, Instagram, Facebook Pages, HubSpot, Mailchimp |
linkedinname |
string |
LinkedIn profile name (replaces username). |
|
customerid / customerdescriptivename |
string |
Google Ads customer id + human-readable label. | Google Ads |
merchantid |
string |
Merchant Center merchant id. | Google Merchant Center |
channelid / channelname |
string |
YouTube channel id + display name. | YouTube |
propertyid / propertyname |
string |
GA4 property id + human-readable label. | GA4 |
adaccountid / adaccountname |
string |
Meta Ads account id (without act_ prefix) + label. |
Meta Ads |
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-pages, linkedin, google-ads, meta-ads, 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. |
customerdescriptivename |
string | No | Human-readable account label (e.g. "Acme Corp — Production"). Shown in the dashboard next to the customer ID. If omitted, only the ID is displayed. |
Response
{
"result": true,
"customerid": "1234567890",
"customerdescriptivename": "Acme Corp — Production",
"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 by Wiro (one row per field).
TokenRefreshreturns new tokens in its own response. oauth_sessionfields are always stripped from Status, Detail,MyAgents, andCredentialUpsertresponses —accesstoken,refreshtoken,tokenexpiresat,pageAccessTokenand any similar rows never leave the server.platformfields are stripped foruserrole callers (default for API keys without ADMIN scope). OpenAI / Wiro / Calendarific keys are invisible in the response.oauth_appfields (clientsecret,appsecret) are visible in Detail responses after an "own mode" OAuth setup writes them. If you build a customer-facing UI on top of this API, treat them as privileged values (do not surface them to non-admin users). The append-only credential history redactsclientsecretto[REDACTED]and always redactsoauth_sessionrows; only the live row can be read.fieldstatusenforces least-privilege writes. API callers only holduserrole — they cannot writeoauth_app,oauth_session,oauth_picker,platform,computed, orcontrolfields. Attempts to do so returnagent-fieldstatus-not-allowed-for-rolein theerrors[]array without altering data.- The
redirecturlreceives only connection status parameters — no tokens, no secrets - 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, scheduled automation tasks, and skill toggles. Browse the platform's skill registry and inspect every skill's pricing, capabilities, and credential requirements.
| Operation | Endpoint |
|---|---|
| Browse all skills (registry) | POST /Skills/List (public) |
| Inspect a single skill | POST /Skills/Detail (public) |
| Find the credential a skill needs | POST /Skills/CredentialSchema (public) |
| List the closed-set capability vocabulary | POST /Skills/Capabilities (public) |
Edit a preference skill's value or a cron's interval/enabled |
POST /UserAgent/CustomSkillUpsert |
| Rename a user-created custom skill (key + optional description) | POST /UserAgent/CustomSkillRename |
| Delete a user-created cron skill | POST /UserAgent/CustomSkillDelete |
| Read version history of a custom skill | POST /UserAgent/CustomSkillHistory |
| Revert a custom skill to preset / a historical version | POST /UserAgent/CustomSkillRevert |
Toggle one or more integration skills (the top-level skills array, e.g. int-instagram-post) on/off, with optional tier change |
POST /UserAgent/SkillsApply |
| Live tier-pricing preview for a hypothetical skill set | POST /UserAgent/PricingPreview |
POST /UserAgent/Detail expose the composed customskills[] array (preference skills + non-cron user-created skills), the scheduledskills[] array (cron skills), and the skills[] array of enabled skill names as top-level fields on the useragent object.
Skill Registry vs Custom Skills
Two concepts share the word "skill" in the agent system. Keep them straight:
| Concept | What it is | Where it lives | API surface |
|---|---|---|---|
| Registry skills | Platform-shipped capabilities — Wiro defines them, they have pricing recipes, credential requirements, and runtime tools. Examples: instagram-post, gmail-check, wordpress-post, wiro-generator. |
The Wiro skill catalog. | POST /Skills/List / POST /Skills/Detail (read), POST /UserAgent/SkillsApply (toggle on/off per useragent). |
| Custom skills | User-editable preference text (cs-content-tone) or scheduled tasks (cs-cron-blog-scanner) that live ON a useragent. They reference registry skills (e.g. a cron skill calls into wordpress-post) but are scoped to one instance. |
Per useragent on the Wiro platform. | POST /UserAgent/CustomSkillUpsert / POST /UserAgent/CustomSkillRename / POST /UserAgent/CustomSkillDelete / POST /UserAgent/CustomSkillHistory / POST /UserAgent/CustomSkillRevert. |
Skill Registry Endpoints
These four endpoints are public — no authentication required. They return data straight from the skill registry.
POST /Skills/List
Lists all skills in the registry.
| Parameter | Type | Required | Description |
|---|---|---|---|
category | string | No | Filter by "int" (integration with a third-party service) or "rule" (rule-only — no credential, no external API). |
capability | string | No | Filter by capability key (see POST /Skills/Capabilities). |
user_invocable | boolean | No | When true, only return skills end users can call directly through chat. |
requires_credentials | boolean | No | Filter by whether the skill requires a credential. |
wiro_connect_pending | boolean | No | Filter by the "Wiro mode coming soon" flag. |
name_in | array<string> | No | Restrict the response to a specific list of skill names. |
Response
{
"result": true,
"errors": [],
"total": 87,
"skills": [
{
"name": "int-instagram-post",
"category": "int",
"version": "1.0.0",
"title": "Instagram Post",
"description": "Post carousel feed and multi-story via Meta Graph API.",
"icon": "/images/icons/skills/instagram.svg",
"brand_color": "#e4405f",
"brand_text_color": "#ffffff",
"brand_logo_filter": "brightness(0) invert(1)",
"docs_url": "integration-instagram-skills",
"requires_credentials": true,
"credential_key": "instagram",
"additional_credential_keys": [],
"capabilities": ["social_publishing"],
"depends_on": [],
"conflicts_with": [],
"user_invocable": true,
"deprecated": false,
"replacement": null,
"pricing": {
"monthly_price_weight_usd": 5.5,
"monthly_credits_weight": 200,
"credit_costs": {
"message": 6,
"create": 58,
"modify": 29,
"regenerate": 58
}
}
}
]
}
Skill names always carry their registry prefix (int-* integration, util-* utility / rule-only, cs-cron-* bundled cron). Use the exact name value in SkillsApply and Deploy.body.skills.
POST /Skills/Detail
Returns a single skill by name.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Canonical skill name with prefix (e.g. "int-instagram-post", "util-html-strip", "cs-cron-content-scanner"). |
Response
{
"result": true,
"errors": [],
"skill": {
"name": "int-instagram-post",
"category": "int",
"version": "1.0.0",
"title": "Instagram Post",
"description": "Post carousel feed and multi-story via Meta Graph API.",
"icon": "/images/icons/skills/instagram.svg",
"brand_color": "#e4405f",
"brand_text_color": "#ffffff",
"brand_logo_filter": "brightness(0) invert(1)",
"docs_url": "integration-instagram-skills",
"requires_credentials": true,
"credential_key": "instagram",
"additional_credential_keys": [],
"capabilities": ["social_publishing"],
"depends_on": [],
"conflicts_with": [],
"user_invocable": true,
"deprecated": false,
"replacement": null,
"pricing": {
"monthly_price_weight_usd": 5.5,
"monthly_credits_weight": 200,
"credit_costs": {
"message": 6,
"create": 58,
"modify": 29,
"regenerate": 58
}
}
}
}
Returns the full registry entry — same shape as a row from Skills/List. Returns { "result": false, "errors": [{ "code": 404, "message": "Skill not found: <name>" }] } if the name is unknown.
POST /Skills/CredentialSchema
Convenience endpoint — returns the credential entry for a given skill name in one call. Useful when you've discovered a skill via Skills/List and want to render the credential form without a separate Credentials/Detail round-trip.
| Parameter | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Skill name with the registry prefix (e.g. "int-instagram-post"). |
Response
{
"result": true,
"errors": [],
"credential": {
"key": "instagram",
"title": "Instagram",
"icon": "/images/icons/skills/instagram.svg",
"brand_color": "#e4405f",
"brand_text_color": "#ffffff",
"brand_logo_filter": "none",
"docs_url": "integration-instagram-skills",
"credential_mode": "oauth",
"connection_modes": ["wiro", "own"],
"wiro_connect_pending": true,
"credential_schema": [
{ "key": "appid", "type": "text", "label": "App ID", "required": true, "pattern": "^[0-9]+$", "oauth_managed": true, "only_in_modes": ["own"] },
{ "key": "appsecret", "type": "password", "label": "App Secret", "required": true, "show_toggle": true, "oauth_managed": true, "only_in_modes": ["own"] },
{ "key": "igusername", "type": "text", "label": "Connected Account", "required": false, "auto_filled_by_oauth": true, "readonly_when_connected": true }
],
"oauth_provider": {
"auth_method_value": "wiro",
"connect_endpoint": "/UserAgentOAuth/IGConnect",
"disconnect_endpoint": "/UserAgentOAuth/IGDisconnect",
"status_endpoint": "/UserAgentOAuth/IGStatus",
"connect_button_label": "Connect with Instagram",
"connect_button_icon": "/images/icons/skills/instagram.svg",
"connect_button_brand_color": "#e4405f",
"connect_button_text_color": "#ffffff",
"connect_button_logo_filter": "none",
"username_field": "igusername",
"return_query_param": "ig_connected",
"return_error_param": "ig_error",
"return_error_detail_param": "ig_error_detail",
"account_picker": null,
"extra_step": null
},
"used_by_skills": ["int-instagram-post"]
}
}
Returns the full registry credential entry — same shape as POST /Credentials/Detail. Returns { "result": false, "errors": [{ "code": 404, "message": "No credential associated with skill: <name>" }] } if the skill exists but has credential_key: null (rule-only / platform-managed) or if the skill is unknown.
POST /Skills/Capabilities
Returns the closed-set vocabulary of high-level capability tags. Use this to build a "Find skills that can: ___" picker in your UI, or to filter Skills/List via the capability parameter.
Response
{
"result": true,
"errors": [],
"capabilities": [
{ "name": "direct_reporting", "description": "Skill generates direct reports on operator request" },
{ "name": "attribution_cross_check", "description": "Cross-check platform conversions vs GA4 paid traffic" },
{ "name": "cross_check", "description": "Cross-system data validation" },
{ "name": "campaign_management", "description": "Create/pause/modify ad campaigns" },
{ "name": "campaign_reporting", "description": "Campaign performance reports" },
{ "name": "approval_flow", "description": "Approval command grammar for operator-gated actions" },
{ "name": "recommendation_ledger", "description": "Recommendation queue/log management" },
{ "name": "recommendation_execution", "description": "Execute approved recommendations" },
{ "name": "content_generation", "description": "Generate content (text, media)" },
{ "name": "creative_generation", "description": "Generate ad creatives" },
{ "name": "image_generation", "description": "Generate images" },
{ "name": "video_generation", "description": "Generate videos" },
{ "name": "human_copywriting", "description": "Enforce anti-AI human copy rules" },
{ "name": "anti_ai_tone", "description": "Reject AI-generated tone patterns" },
{ "name": "memory_management", "description": "Manage memory/*.json hygiene (size limits, trim, dedupe, schema)" },
{ "name": "markdown_reporting", "description": "Structured markdown reports (tables, code blocks, splits)" },
{ "name": "store_review_response", "description": "Craft App Store / Google Play review responses" },
{ "name": "outreach_compliance", "description": "Outreach PII/opt-out/consent rules (GDPR, CAN-SPAM)" },
{ "name": "web_publishing", "description": "Publish web content (WordPress, etc.)" },
{ "name": "email_publishing", "description": "Send emails / newsletters" },
{ "name": "social_publishing", "description": "Post to social platforms (Twitter, IG, FB, LI, TikTok)" },
{ "name": "push_notification", "description": "Send push notifications (FCM)" },
{ "name": "lead_enrichment", "description": "Enrich prospect data (Apollo, HubSpot)" },
{ "name": "sequence_automation", "description": "Run outreach sequences (Lemlist, Apollo)" },
{ "name": "multi_platform_detection", "description": "Auto-detect available platforms" },
{ "name": "holiday_discovery", "description": "Discover global holidays (Calendarific)" },
{ "name": "product_feed_management", "description": "Manage product feeds (Merchant Center)" },
{ "name": "review_monitoring", "description": "Monitor store reviews (App Store, Google Play)" },
{ "name": "app_metadata", "description": "Fetch app store metadata (description, pricing, features)" },
{ "name": "event_management", "description": "Manage app events (App Store in-app events)" },
{ "name": "file_asset_management", "description": "Manage asset files from cloud storage (Drive)" }
]
}
Capability names are snake_case strings. Pass any of them as the capability parameter to Skills/List to filter (e.g. { "capability": "social_publishing" } returns every skill that publishes to a social platform).
Overview
Every agent has two kinds of behavior customization:
| Type | Location in response | Purpose | Endpoint to update |
|---|---|---|---|
| Custom skills | customskills[] (top level) |
Preferences (brand voice, ICP, rules) and scheduled crons (scanning, reporting) | POST /UserAgent/CustomSkillUpsert |
| Integration skill toggles | skills[] (top level, array of enabled names) |
Turn an integration skill (e.g. int-instagram-post, int-googleads-manage) on or off |
POST /UserAgent/SkillsApply |
Call POST /UserAgent/Detail to discover both lists for any deployed instance.
Custom Skills — Discovery
Call POST /UserAgent/Detail. The merged skill array lives at the top level under customskills.
Request:
{
"guid": "your-useragent-guid"
}
Response (top-level customskills excerpt):
[
{
"key": "content-tone",
"value": "## Brand Voice\nTone: friendly\nTarget Audience: ...\n\n## Content Sources\nPrimary Source: https://your-site.com/feed.xml\n...",
"description": "Content strategy, brand voice, and posting rules",
"enabled": true,
"interval": null,
"_source": "preset-strategy",
"_editable": true
},
{
"key": "cron-content-scanner",
"value": "",
"description": "Content discovery with rotating strategies",
"enabled": true,
"interval": "0 */4 * * *",
"_source": "skill-bundle",
"_editable": false
},
{
"key": "cron-weekly-health-check",
"value": "Every Monday, check the inbox and publish a recap to WordPress.",
"description": "User-created cron",
"enabled": true,
"interval": "0 9 * * 1",
"_source": "user-created",
"_editable": true,
"_user_created": true
}
]
The exact key names depend on the agent template. Always fetch POST /UserAgent/Detail to see the real list for your deployed instance — skill keys, default cron schedules, and even the set of skills can evolve as templates are updated.
| Field | Type | Description |
|---|---|---|
key |
string | Unique skill identifier. Use this in upsert/delete requests. |
value |
string | Skill instructions/content. Populated only for editable preference skills and user-created crons; bundled crons have it empty. |
description |
string | Human-readable description of what the skill does. Read-only — set by the agent template or by the user at create time, never accepted in Upsert payloads for preset skills. |
enabled |
boolean | Whether the cron is active. Writable on cron skills; ignored on preference skills. |
interval |
string | null | Cron expression for scheduled execution, or null for preference skills. Writable on cron skills; ignored on preferences. |
_source |
string | preset-strategy (editable preference), skill-bundle (cron owned by an integration skill), or user-created (cron added via Custom Agent Builder). |
_editable |
boolean | Convenience flag: true for preset strategies and user-created crons (you can write value), false for skill-bundled crons (you can only write enabled / interval). |
Understanding _source
Agent responses merge three sources into a single customskills[] array:
- Preset strategies (
_source: "preset-strategy") — preferences defined by the agent preset (e.g.ad-strategy,content-tone). You can writevalue. - Bundled crons (
_source: "skill-bundle") — cron tasks shipped inside a specific integration skill. Automatically materialized when the integration is enabled. You can writeintervalandenabled, but notvalue(the skill owns the cron instruction text). - User-created crons (
_source: "user-created") — cron tasks added by the user via the Custom Agent Builder or by passingusercreated: truetoCustomSkillUpsert. You can write all fields.
Updating Preference Skills
Preference skills (_source: "preset-strategy", _editable: true) let you customize the agent's behavior by editing its instructions. Send one CustomSkillUpsert call per skill. Unspecified skills are untouched. Select your agent below to see its preference skill and a complete update request.
Send only value to a preset preference skill. enabled, interval, and description are silently dropped — they have no runtime effect because preset strategies are read on-demand by cron tasks via cs-<slug>; they're never scheduled themselves. If you need the skill's display description, read it from POST /UserAgent/Detail.
Skill key: content-tone — Controls brand voice, hashtags, and posting style per platform.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-social-manager-guid",
"skillkey": "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.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-blog-editor-guid",
"skillkey": "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.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-app-review-guid",
"skillkey": "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.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-app-event-guid",
"skillkey": "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.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-push-agent-guid",
"skillkey": "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.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-newsletter-guid",
"skillkey": "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.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-leadgen-guid",
"skillkey": "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.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-google-ads-guid",
"skillkey": "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.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-meta-ads-guid",
"skillkey": "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 are cron skills — bundled ones (_source: "skill-bundle") ship with integration skills and run automatically when the integration is enabled. User-created crons (_source: "user-created") are added at any time.
For bundled crons, only enabled and interval are writable. The task body (value) is owned by the integration skill and re-materialised on every container restart. To change what a bundled scheduled task does, edit the paired preference skill (e.g. content-tone is read by cron-content-scanner at runtime). For user-created crons, all three (value, interval, enabled) are writable.
Example: Change a bundled cron's frequency
Each cron is written via its own CustomSkillUpsert call, so updating multiple crons is simply a loop of independent requests.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "cron-review-scanner",
"enabled": true,
"interval": "0 */4 * * *"
}'
Example: Disable a bundled cron
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "cron-content-scanner",
"enabled": false
}'
Example: Create a user-defined cron
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "weekly-health-check",
"value": "Every Monday, check the inbox and publish a digest count to WordPress.",
"interval": "0 9 * * 1",
"enabled": true,
"usercreated": true,
"description": "Weekly inbox health check"
}'
The server auto-prefixes skillkey with cron- on user-created crons, so the row above is stored with key: "cron-weekly-health-check". Subsequent upserts can use either form — "weekly-health-check" or "cron-weekly-health-check".
Example: Delete a user-created cron
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillDelete" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "cron-weekly-health-check"
}'
Deleting preset strategies or bundled crons is a no-op — they are controlled by the template/integration and always re-materialized on reconciliation.
Example: Rename a user-created custom skill
Only user-created rows (_source: "user-created") can be renamed through CustomSkillRename. The skillkey flavour is preserved — a cs-cron-* scheduled task cannot be renamed to a cs-* strategy (delete + re-create with the right kind instead). The full version-history chain follows the rename under the new key, and a rename audit event is appended.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillRename" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"oldskillkey": "cs-cron-weekly-health-check",
"newskillkey": "cs-cron-weekly-system-check",
"description": "Weekly system health probe"
}'
Successful responses include the canonical newskillkey the server ended up writing (after slug normalisation), so follow-up calls that reference the skill should use that value.
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 |
Toggling Integration Skills
The top-level skills[] array on UserAgent/Detail lists the integration skills currently enabled on the instance (e.g. [{ "name": "int-instagram-post" }, { "name": "int-googleads-manage" }]). To enable, disable, or change tier in one shot, use POST /UserAgent/SkillsApply — even when you only want to flip a single skill, send it as a one-entry skills map. The endpoint:
- Charges the wallet (or prorates) once, not N times.
- Triggers exactly one container restart after the new skill set lands.
- Uses idempotency + a UA-level mutex — concurrent calls or FE retries can't double-apply.
- Is atomic — a payment failure rolls back to the pre-call skill set.
Single-skill enable:
curl -X POST "https://api.wiro.ai/v1/UserAgent/SkillsApply" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440001",
"skills": {
"int-instagram-post": true
}
}'
Batch toggle + tier upgrade in one call:
curl -X POST "https://api.wiro.ai/v1/UserAgent/SkillsApply" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"tier": "pro",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"skills": {
"int-instagram-post": true,
"int-twitterx-post": true,
"int-wordpress-post": false
}
}'
idempotencyKey is required — use a UUID per "user clicks Save" event. The full response shape and error-code table live on SkillsApply.
SkillsApply with Skill changes are only available for custom-built agents. — skills are inherited from the marketplace agent template and changing them would diverge the instance. To run a different skill set, deploy a custom build (POST /UserAgent/Deploy with custom: true) and configure its skills there.
int-instagram-post), every bundled cron it owns (_source: "skill-bundle") is hidden from the top-level customskills / scheduledskills lists until the integration is re-enabled — you don't need to touch them individually.
Full Example: Push Notification Manager
Complete flow — fetch skills, then update preferences and schedules with three separate calls.
Step 1 — Discover skills. Call POST /UserAgent/Detail with the agent instance GUID:
{
"guid": "your-push-agent-guid"
}
Response excerpt (top-level customskills):
[
{
"key": "push-preferences",
"value": "## Push Tone\nWrite like a mobile growth expert...",
"description": "Push notification style, language, and targeting preferences",
"enabled": true,
"interval": null,
"_source": "preset-strategy",
"_editable": true
},
{
"key": "cron-push-scanner",
"value": "",
"description": "Scan holidays and craft push notification suggestions",
"enabled": true,
"interval": "0 9 * * *",
"_source": "skill-bundle",
"_editable": false
},
{
"key": "cron-push-dispatcher",
"value": "",
"description": "Send queued push notifications on schedule",
"enabled": true,
"interval": "0 * * * *",
"_source": "skill-bundle",
"_editable": false
}
]
Step 2 — Update each skill with its own CustomSkillUpsert call:
# 1. Rewrite preference value
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" -H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-push-agent-guid",
"skillkey": "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."
}'
# 2. Change scanner schedule from daily to Mondays only
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" -H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-push-agent-guid",
"skillkey": "cron-push-scanner",
"enabled": true,
"interval": "0 9 * * 1"
}'
# 3. Change dispatcher schedule from hourly to every 2 hours
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" -H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-push-agent-guid",
"skillkey": "cron-push-dispatcher",
"interval": "0 */2 * * *"
}'
All three requests succeed independently. The agent restarts once — the server coalesces the restart trigger after all three succeed within the same request window.
- 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
Integration setup guides: Every skill below that needs a third-party connection links to a dedicated integration page with the full OAuth / API key walkthrough, required scopes or permissions, callback URL, troubleshooting, and multi-tenant architecture notes. See the Integration Catalog for the full list.
Discovery is canonical. Skill keys evolve as agent templates are updated. To see the exact keys for a specific agent instance, always fetch POST /UserAgent/Detail first — the top-level customskills array is the source of truth. The tables below reflect current intended keys but may lag behind the latest agent template revisions; CustomSkillUpsert silently accepts keys that exist on the instance and returns skill-not-found for unknown keys without altering other state.
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
Scheduled task keys are defined per agent template and may be updated over time. The table below reflects the current keys and default cron expressions shipped with Wiro's built-in agent templates, but the source of truth for any specific deployed agent is always POST /UserAgent/Detail → top-level customskills.
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 |
Skill → Integration Mapping
Skills that depend on third-party credentials. Follow the linked integration page for provider setup, OAuth walkthrough, and troubleshooting.
| Skill | Credential Key | Integration Guide |
|---|---|---|
int-metaads-manage |
meta-ads (OAuth) |
Meta Ads Skills |
int-facebookpage-post |
facebook-pages (OAuth) |
Facebook Page Skills |
int-instagram-post |
instagram (OAuth) |
Instagram Skills |
int-linkedin-post |
linkedin (OAuth) |
LinkedIn Skills |
int-twitterx-post |
twitter (OAuth) |
Twitter / X Skills |
int-tiktok-post |
tiktok (OAuth) |
TikTok Skills |
int-youtube-manage |
youtube (OAuth) |
YouTube Skills |
int-googleads-manage |
google-ads (OAuth) |
Google Ads Skills |
int-merchant-center |
google-merchant-center (OAuth) |
Merchant Center Skills |
int-ga4-analytics |
ga4 (OAuth) |
GA4 Skills |
int-hubspot-crm |
hubspot (OAuth) |
HubSpot Skills |
int-mailchimp-email |
mailchimp (OAuth or API key) |
Mailchimp Skills |
int-google-drive |
google-drive (Service Account) |
Google Drive Skills |
int-gmail-check |
gmail (App Password) |
Gmail Skills |
int-firebase-push |
firebase (Service Account) |
Firebase Skills |
int-wordpress-post |
wordpress (App Password) |
WordPress Skills |
int-brevo-email |
brevo (API key) |
Brevo Skills |
int-sendgrid-email |
sendgrid (API key) |
SendGrid Skills |
int-appstore-reviews, int-appstore-metadata, int-appstore-events |
apple-appstore (JWT / Service Account) |
App Store Skills |
int-googleplay-reviews, int-googleplay-metadata, int-googleplay-events |
google-play (Service Account) |
Google Play Skills |
int-apollo-sales |
apollo (API key) |
Apollo Skills |
int-lemlist-outreach |
lemlist (API key) |
Lemlist Skills |
int-wiro-generator |
Platform-managed (Wiro internal key) | See Using Wiro AI Models from Your Agent |
int-calendarific |
Platform-managed (no user key) | Platform-Managed Credentials |
Agents can optionally forward operator notifications to a Telegram bot via the sys-telegram credential — see Telegram Skills. This is never required; every agent remains fully usable over web chat and the Messaging API without a bot configured.
Restart behavior: Calls to CustomSkillUpsert, CustomSkillDelete, CustomSkillRevert, and SkillsApply on a running agent (status 3 or 4) each trigger an automatic restart so the new skill configuration is picked up. Same as credential updates.
Using Wiro AI Models from Your Agent
int-wiro-generator is a platform built-in skill that lets an agent call Wiro's own AI models (image/video/audio/LLM generation, cover image creation, model discovery) using Wiro's internal API. When it's enabled on an agent:
credentials.wiro.apiKeyis filled in automatically by Wiro (platform-managed —fieldstatus: "platform"). You don't set this key yourself.- The agent container gets
WIRO_API_KEYas an env var only when bothint-wiro-generatorskill is enabled and the key is present in the template. int-wiro-generatoris markeduser_invocable: false— it isn't called directly by end-user messages; other skills and scheduled tasks invoke it internally when they need to generate content.
Most Wiro-provided agent templates (Social Manager, Blog Content, Push, App Event, Meta Ads, Google Ads, Newsletter) ship with int-wiro-generator: true and the platform-managed wiro credential pre-filled. Templates that don't need AI generation (App Review Support, Lead Generation Manager) ship with int-wiro-generator: false.
To check whether your deployed agent has it:
curl -X POST "https://api.wiro.ai/v1/UserAgent/Detail" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "guid": "your-useragent-guid" }'
# Look under the top-level skills — "int-wiro-generator" should be true
API user-specific note
wiro-generator does not mean "your custom skill can call Wiro's Run API with your own API key". It's scoped to the agent template's internal skills and uses Wiro's pre-filled platform key. If you're building on top of Wiro programmatically and want to call the Run / Task / LLM APIs directly from your own backend (not from inside an agent container), use your standard Wiro API key against the public API — see Run a Model and LLM & Chat Streaming.
Update Rules Summary
| Operation | Endpoint | Preset strategy ( _source: preset-strategy) |
Skill-bundled cron ( _source: skill-bundle) |
User-created cron ( _source: user-created) |
|---|---|---|---|---|
Write value |
CustomSkillUpsert |
Yes | No — silently dropped | Yes |
Write interval |
CustomSkillUpsert |
No — silently dropped | Yes | Yes |
Write enabled |
CustomSkillUpsert |
No — silently dropped | Yes | Yes |
Write description |
CustomSkillUpsert |
No — silently dropped | No — silently dropped | Yes |
| Create | CustomSkillUpsert with usercreated: true |
n/a | n/a | Yes |
| Delete | CustomSkillDelete |
No-op | No-op | Yes |
- Send only the fields you want to change — omitted fields keep their current values.
- Unknown skill keys return
skill-not-foundin theerrors[]without altering other state. - To clear a cron schedule, call
CustomSkillUpsertwithenabled: false. Settinginterval: ""ornullalso clears it. - Integration toggles (top-level
skills[]) are a separate endpoint — see Toggling Integration Skills.
What happens when Wiro updates an agent template
Skills occasionally evolve on Wiro's side — new preset strategies, new bundled crons, improved instructions. Deployed instances are reconciled with the latest template without destroying your edits:
| Row type | Behavior on template update |
|---|---|
Preset strategy (_source: "preset-strategy") |
Your value is preserved. New placeholder structure in the template appears only on fresh deploys. |
Skill-bundled cron (_source: "skill-bundle") |
The value (cron instructions) is re-materialized from the new skill. Your custom interval and enabled are preserved. |
| New preset strategy upstream | Added to your instance with the template's default value. |
| Preset strategy removed upstream | Removed from your instance on the next reconciliation. |
| User-created cron | Always preserved. |
This means your CustomSkillUpsert edits are durable across template upgrades, while Wiro can push improvements to the scanning/reporting workflows without you having to redeploy.
Agent Builder
Build a custom agent from scratch — no marketplace template required. Pick your own skill set, preview the live tier price, and deploy in one call.
Overview
Wiro's agent runtime supports two deploy paths:
| Path | When to use | Endpoint |
|---|---|---|
| Template deploy | The marketplace already has an agent that does what you need (Instagram Manager, Push Notifications, App Review Support, …). You inherit the template's default skill set + configuration. | POST /UserAgent/Deploy with agentguid |
| Custom build | You want a unique skill combination — for example a custom support bot that watches Gmail, publishes a daily digest to WordPress, and uses Wiro's image generator. No marketplace template fits. | POST /UserAgent/Deploy with custom: true |
The two paths produce the same useragent shape at the end (same customskills, scheduledskills, credentials, subscription, peractioncosts, etc.). The only differences are:
| Aspect | Template deploy | Custom build |
|---|---|---|
useragents.agentid |
The catalog row id | null |
| Pricing recipe | Computed from the template's default skill set | Computed from the skills you toggled on |
agent.tiers on the response |
Template's tier numbers | Live-resolved per-instance tier numbers |
agent.cover on the response |
Template's cover image | The cover you sent at Deploy (or a placeholder) |
| Cascade updates | When admin pushes a preset edit, your useragent reconciles | No template — the agent is fully owned by you |
/Agent/List or /Agent/Detail. Discovery happens through your own product surface.Step 1 — Browse Available Skills
Custom builds start from the skill registry — the catalogue of every skill the platform supports. Browse it with POST /Skills/List and inspect details with POST /Skills/Detail. Both endpoints are public — no authentication required.
curl -X POST "https://api.wiro.ai/v1/Skills/List" \
-H "Content-Type: application/json" \
-d '{ "category": "int" }'
Pick the skill names you want to enable. Each skill descriptor tells you:
name— the canonical skill key you'll send inskills/skillOverridescategory—int(integration with a third-party service) orrule(rule-only, no credential)credential_key— which credential the skill needs (nullfor rule-only or platform-managed skills)requires_credentials— boolean, whether the user must supply a credential before the skill can rundepends_on— array of skill names that must also be enabled (Wiro auto-enables transitive deps)conflicts_with— array of skill names that cannot coexist with this onepricing— the per-skill pricing recipe (base_price_usd,base_credits,credit_costs.{message,create,modify,regenerate})
Use POST /Skills/Capabilities to discover the closed-set capability vocabulary (the high-level tasks skills can perform). Useful when you want to find every skill that can "post-content" or "send-email".
Step 2 — Live Pricing Preview
Before you commit, fetch the live tier price for the proposed skill set with POST /UserAgent/PricingPreview — the draft mode runs without touching any DB state.
curl -X POST "https://api.wiro.ai/v1/UserAgent/PricingPreview" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"draft": true,
"tier": "pro",
"skills": ["int-gmail-check", "int-wordpress-post", "int-wiro-generator"]
}'
Response
{
"result": true,
"errors": [],
"tier": "pro",
"tiermultiplier": 3,
"totalPriceUsd": 27,
"totalMonthlyCredits": 3000,
"peractioncosts": { "message": 10, "create": 60, "modify": 20, "regenerate": 20 },
"skillBreakdown": [
{ "skill": "int-gmail-check", "priceUsd": 4, "credits": 400, "actionCostOverrides": {} },
{ "skill": "int-wordpress-post", "priceUsd": 5, "credits": 500, "actionCostOverrides": { "create": 60 } },
{ "skill": "int-wiro-generator", "priceUsd": 0, "credits": 0, "actionCostOverrides": {} }
],
"enabledSkills": ["int-gmail-check", "int-wordpress-post", "int-wiro-generator"],
"directSkills": ["int-gmail-check", "int-wordpress-post", "int-wiro-generator"],
"agentBase": { "priceUsd": 9, "credits": 1000 },
"tiers": {
"starter": { "priceUsd": 9, "credits": 1000 },
"pro": { "priceUsd": 27, "credits": 3000 }
}
}
Read the response:
tiers.starter.priceUsdandtiers.pro.priceUsd→ what your wallet will be charged for each tier.tiers.starter.creditsandtiers.pro.credits→ monthly credit allocation.peractioncosts→ how many credits each agent action burns.skillBreakdown[]→ per-skill contribution to the total. Use this to see which skill is the most expensive.enabledSkills→ final closure (includes any transitively-enableddepends_on).
agentBase is the platform-wide floor ($9 / 1000 credits) — every agent has this base, regardless of skill set. The skill weights stack on top.Step 3 — Deploy the Custom Agent
Send the same skill set you previewed to POST /UserAgent/Deploy with custom: true and useprepaid: true. The selected tier is debited immediately.
curl -X POST "https://api.wiro.ai/v1/UserAgent/Deploy" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"custom": true,
"title": "Inbox-to-Blog Bot",
"description": "Watches inbound Gmail and publishes a digest post to WordPress every 4 hours.",
"useprepaid": true,
"tier": "pro",
"skills": {
"int-gmail-check": true,
"int-wordpress-post": true,
"int-wiro-generator": true
},
"credentials": {
"gmail": { "account": "[email protected]", "apppassword": "xxxx xxxx xxxx xxxx" },
"wordpress": { "url": "https://blog.example.com", "user": "agent", "apppassword": "xxxx xxxx xxxx xxxx" }
},
"customskills": [
{
"key": "cron-summarize-inbox",
"value": "Every 4 hours, scan all unread Gmail messages from the past 4 hours, summarize each in 1 sentence, and publish the digest as a WordPress draft post.",
"interval": "0 */4 * * *",
"enabled": true,
"_user_created": true,
"description": "Inbox summary digest"
}
]
}'
Response (excerpt)
{
"result": true,
"errors": [],
"useragents": [
{
"guid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"agentguid": null,
"title": "Inbox-to-Blog Bot",
"description": "Watches inbound Gmail and publishes a digest post to WordPress every 4 hours.",
"tier": "pro",
"tiermultiplier": 3,
"status": 0,
"setuprequired": false,
"monthlycredits": 3000,
"monthlypriceusd": 27,
"remainingcredits": 3000,
"creditperiod": "2026-05",
"peractioncosts": { "message": 10, "create": 60, "modify": 20, "regenerate": 20 },
"skills": ["int-gmail-check", "int-wordpress-post", "int-wiro-generator"],
"customskills": [],
"scheduledskills": [
{
"key": "cs-cron-summarize-inbox",
"value": "Every 4 hours, scan all unread Gmail messages from the past 4 hours, summarize each in 1 sentence, and publish the digest as a WordPress draft post.",
"interval": "0 */4 * * *",
"enabled": true,
"_source": "user-created",
"_editable": true,
"_user_created": true
}
],
"credentials": {
"gmail": { "_connected": false, "optional": false, "extra": false, "account": "[email protected]", "apppassword": "[present]" },
"wordpress": { "_connected": false, "optional": false, "extra": false, "url": "https://blog.example.com", "user": "agent", "apppassword": "[present]" }
},
"agent": {
"custom": true,
"title": "Inbox-to-Blog Bot",
"tiermultiplier": 3,
"tiers": {
"starter": { "priceUsd": 9, "credits": 1000 },
"pro": { "priceUsd": 27, "credits": 3000 }
},
"extracreditpacks": [
{ "packkey": "small", "credits": 15000, "priceusd": 120, "enabled": true },
{ "packkey": "medium", "credits": 30000, "priceusd": 220, "enabled": true },
{ "packkey": "large", "credits": 60000, "priceusd": 380, "enabled": true }
]
}
}
]
}
Notes on the response:
agentid: null— confirms this is a custom build with no marketplace template.agent.custom: true— the synthesized template placeholder. Same shape as a template'sagentblock but noslug/cover/categories(custom builds aren't in the marketplace).scheduledskillsalready contains the cron from the Deploy body, with the canonicalcs-cron-prefix added by the server.status: 0— Deploy auto-transitioned to Stopped because all required credentials were inline. CallPOST /UserAgent/Startnext.
Step 4 — Refine Skills After Deploy
Custom builds are the only path where end users can toggle skills on/off after deploy. Use POST /UserAgent/SkillsApply — even when you only want to flip a single skill, send it as a one-entry skills map. The endpoint charges the wallet once, triggers exactly one container restart, and rolls back atomically on payment failure.
SkillsApply with Skill changes are only available for custom-built agents. Skills are inherited from the marketplace agent template and changing them would diverge the instance. To run a different skill set, deploy a custom build (Deploy with custom: true) instead.Single-skill enable
curl -X POST "https://api.wiro.ai/v1/UserAgent/SkillsApply" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440001",
"skills": {
"int-instagram-post": true
}
}'
The response includes the new pricing snapshot and the prorated wallet debit:
{
"result": true,
"errors": [],
"tier": "pro",
"stripeProrationApplied": false,
"prepaidWalletDelta": 2.75,
"restartTriggered": true,
"restartedAt": 1714694520,
"pricing": {
"previousPriceUsd": 27,
"newPriceUsd": 32.5,
"deltaUsd": 5.5,
"previousMonthlyCredits": 3000,
"newMonthlyCredits": 3200,
"deltaCredits": 200,
"enabledSkills": ["int-gmail-check", "int-wordpress-post", "int-wiro-generator", "int-instagram-post"],
"peractioncosts": { "message": 10, "create": 60, "modify": 20, "regenerate": 20 }
}
}
prepaidWalletDelta is the prorated USD amount debited from your wallet for the remaining days of the current period. The same wallet is credited if you disable a paid skill mid-period (negative delta).
Batch toggle + tier upgrade in one call
When the user stages many toggles in your UI and commits them all together, send everything in one SkillsApply call:
curl -X POST "https://api.wiro.ai/v1/UserAgent/SkillsApply" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321",
"tier": "pro",
"idempotencyKey": "550e8400-e29b-41d4-a716-446655440000",
"skills": {
"int-gmail-check": true,
"int-wordpress-post": true,
"int-wiro-generator": true,
"int-instagram-post": true,
"int-metaads-manage": false
}
}'
idempotencyKey is required — use a UUID per "user clicks Save" event. A retry of the same key returns the cached response without re-running the saga.
Common Recipes
A. Inbox-to-newsletter digest (Brevo)
{
"custom": true,
"title": "Inbox Digest Newsletter",
"useprepaid": true,
"tier": "starter",
"skills": { "int-gmail-check": true, "int-brevo-email": true },
"credentials": {
"gmail": { "account": "[email protected]", "apppassword": "xxxx xxxx xxxx xxxx" },
"brevo": { "apikey": "xkeysib-XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX-XXXXXXXXXXXX" }
},
"customskills": [
{
"key": "cron-email-digest",
"value": "Twice a day, summarize unread Gmail messages and send a 1-paragraph digest to the 'team-internal' Brevo list as a transactional email.",
"interval": "0 9,17 * * *",
"enabled": true,
"_user_created": true,
"description": "Daily email digest"
}
]
}
B. WordPress publisher with weekly research
{
"custom": true,
"title": "Blog Research & Publish",
"useprepaid": true,
"tier": "pro",
"skills": { "int-wordpress-post": true, "website": true, "int-wiro-generator": true },
"credentials": {
"wordpress": { "url": "https://blog.example.com", "user": "admin", "apppassword": "xxxx xxxx xxxx xxxx" },
"var-website": { "urls": "[{\"websitename\":\"Wired\",\"url\":\"https://www.wired.com/feed/rss\"}]" }
},
"customskills": [
{
"key": "content-strategy",
"value": "## Writing Style\nShort, punchy, technical. Always include code examples.\n\n## Sources\nWired, ArsTechnica, Hacker News.\n\n## Cadence\nOne 600-word article every Monday.",
"_user_created": false
},
{
"key": "cron-weekly-blog",
"value": "Every Monday morning, scan the websites listed under your var-website credential, draft one 600-word article, and publish to WordPress as a draft.",
"interval": "0 9 * * 1",
"enabled": true,
"_user_created": true,
"description": "Weekly WordPress publish"
}
]
}
C. Multi-channel social poster (Pro tier)
{
"custom": true,
"title": "Cross-Channel Social",
"useprepaid": true,
"tier": "pro",
"skills": {
"int-twitterx-post": true,
"int-instagram-post": true,
"int-linkedin-post": true,
"int-facebookpage-post": true,
"int-wiro-generator": true
},
"credentials": {}
}
OAuth providers are connected later via POST /UserAgentOAuth/{Provider}Connect — see Agent Credentials.
Subscription & Billing for Custom Builds
Subscriptions for custom builds work exactly like template deploys:
- A 30-day prepaid subscription row (
plan: "agent",tier: <starter|pro>,provider: "prepaid") is inserted at Deploy. POST /UserAgent/CancelSubscriptionschedules cancel-at-period-end.POST /UserAgent/RenewSubscriptioneither undoes a pending cancel (no charge) or creates a fresh 30-day period (wallet charged).POST /UserAgent/UpgradeTierupgrades Starter → Pro with a prorated wallet debit.
When you toggle a skill on or off, the active subscription is automatically prorated (see Step 4 above). The new monthly amount becomes your charge at next renewal.
Limits & Notes
- Skill set must produce a
> $0price. Custom builds with no paid skills are rejected withSubscription price must be greater than $0. Add at least one paid skill or set agent base price.TheagentBasefloor ($9 / 1000 credits) is the implicit minimum unless every enabled skill is free / utility. - Conflict / dependency violations are surfaced eagerly. If you toggle on two mutually-exclusive skills,
SkillsApplyreturns code102with aconflicts[]array; if adepends_onis missing, code101withdeps[]. Resolve in the UI before committing. - Custom builds receive the same auto-restart on configuration changes as template deploys (status
3/4→ status1withrestartafter: true). - Cover image: custom builds can ship a
coverURL in the Deploy body, or upload one later viaPOST /UserAgent/Cover. - The agent's persona is editable via the standard
customskillsflow. Add acs-personastrategy viaCustomSkillUpsertto set the agent's voice, role, and constraints.
What's Next
- Agent Skills — Discover the skill registry, browse credentials per skill, and configure preferences + scheduled tasks
- Agent Credentials — Connect OAuth providers and set API-key credentials
- Agent Messaging — Chat with your custom agent
- Agent Transactions — Audit credit deductions, renewals, and grants
Agent Transactions
Per-instance credit ledger — every credit deduction, renewal, purchase, refund, grant, and cancel for a useragent in a single immutable feed.
Overview
Wiro keeps a complete, append-only agent transaction ledger for every UserAgent instance. Whenever credits move — the agent runtime burns them on a message, a subscription renews, a Pro user buys an extra-credit pack, an admin tops up, the user disables a paid skill mid-period and gets a refund — a row is inserted into the ledger and surfaced through POST /UserAgent/TransactionList.
The ledger is the single source of truth for "where did my credits go?" and powers the Transactions view in your dashboard.
Transaction type |
Source | When it fires |
|---|---|---|
deduct |
Agent runtime | Every successful agent action (message, create, modify, regenerate). Negative amount. |
renewal |
Subscription cron | At each 30-day rollover when the wallet successfully covers the renewal. Positive amount = monthly credits granted. |
purchase |
Extra-credit checkout | When POST /UserAgent/CreateExtraCreditCheckout (useprepaid: true) succeeds. Positive amount = pack credits. |
grant |
Skill toggle / extra credits | When a skill is enabled mid-period and the credit pool grows, or when a tier upgrade lifts the monthly allocation. Positive amount. |
expired |
Skill toggle | When a skill is disabled mid-period and the credit pool shrinks. Negative amount (signed delta). |
refund |
Server-side refund | When a Stripe refund webhook arrives for an agent purchase. Positive amount. |
cancel |
Subscription end policy | When a subscription expires and the policy revokes any monthly leftover. Negative amount. |
guid that the server uses for deduplication (so a duplicate retry of the same deduct event is a no-op).POST /UserAgent/TransactionList
Returns the ledger rows for a single useragent sorted newest-first, plus a snapshot summary of the current balance.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | The useragent instance guid. |
limit |
number | No | Max rows to return. Default 50, max 500. |
start |
number | No | Offset for pagination. Default 0. |
Authorization: owner uuid OR any team member of the useragent's team. Admin callers bypass the uuid check. Pass teamGUID: <team-guid> as a header for team agents.
Request
curl -X POST "https://api.wiro.ai/v1/UserAgent/TransactionList" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321", "limit": 50 }'
Response
{
"result": true,
"errors": [],
"total": 128,
"summary": {
"monthlycredits": 5000,
"extracredits": 2000,
"usedcredits": 1450,
"remainingcredits": 5550,
"creditperiod": "2026-05",
"creditsyncat": 1714694410
},
"transactions": [
{
"guid": "7f3e8c21-1be1-4f5a-96e8-2b1a9e2a6a01",
"type": "deduct",
"action": "message",
"amount": -10,
"balanceafter": 5550,
"description": "Agent action: message",
"sessionkey": "default",
"messageguid": "5c41dabf-f2be-4aa8-a5a4-8c9e3d2f3f11",
"provider": "agent",
"providerref": null,
"metadata": null,
"uuid": "ada-uuid",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"createdat": 1714694400
},
{
"guid": "a40b5473-aedb-47d7-966b-7b038eed30dc",
"type": "purchase",
"action": "small",
"amount": 5000,
"balanceafter": 5560,
"description": "Extra credits — small pack",
"provider": "prepaid",
"providerref": null,
"metadata": { "pack": "small", "priceUsd": 45 },
"uuid": "ada-uuid",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"createdat": 1714692800
},
{
"guid": "312e2a5f-1002-4c9a-af7d-70571e8b1739",
"type": "renewal",
"action": "monthly",
"amount": 5000,
"balanceafter": 3560,
"description": "Subscription renewal",
"provider": "prepaid",
"providerref": null,
"metadata": { "period": "2026-05", "priceUsd": 29 },
"uuid": "system",
"user": null,
"createdat": 1714608000
},
{
"guid": "98a14c2e-ee0f-4a2d-b73b-d33fe8e57cf2",
"type": "grant",
"action": "skill-toggle",
"amount": 500,
"balanceafter": 5050,
"description": "Skill enabled: int-instagram-post",
"provider": "prepaid",
"providerref": null,
"metadata": {
"skill": "int-instagram-post",
"enabled": true,
"previousPriceUsd": 27,
"newPriceUsd": 32,
"proratedCharge": 3.33
},
"uuid": "ada-uuid",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"createdat": 1714604000
}
]
}
The user object is the resolved actor that triggered the ledger entry — uuid, firstname, lastname, email, username, avatar, avatarinitials. It is null when uuid is a sentinel like "system" (cron renewals, subscription expiry sweeps, automated platform writes) or when the user record has been deleted. The renewal row above was written by the daily renewal cron, so uuid: "system" and user: null.
Summary fields
| Field | Type | Description |
|---|---|---|
monthlycredits | number | Current monthly allocation. |
extracredits | number | Active extra-credit balance (sum of non-expired packs). |
usedcredits | number | Consumed during the current period (reported by the runtime). |
remainingcredits | number | max(0, monthlycredits + extracredits - usedcredits). |
creditperiod | string | 'YYYY-MM' billing window. Rolls over on subscription renewal. |
creditsyncat | number|null | Last runtime → API usage sync in Unix seconds. |
Transaction fields
| Field | Type | Description |
|---|---|---|
guid | string | Stable id of the ledger row. Daemon retries reuse this guid for idempotency. |
type | string | "deduct", "renewal", "purchase", "grant", "expired", "refund", or "cancel". |
action | string|null | Fine-grained detail. Examples: "message" (deduct), "monthly" (renewal), "small"|"medium"|"large" (purchase), "skill-toggle"|"admin"|"upgrade" (grant), "subscription" (cancel/refund). |
amount | number | Signed credit delta — negative for deductions, positive for grants. |
balanceafter | number|null | Remaining credit balance snapshot written at the time of the event. |
description | string|null | Human-readable label. |
sessionkey | string|null | Session the deduct belongs to (deduct rows only). |
messageguid | string|null | Agent message that triggered the deduct (deduct rows only). |
provider | string|null | "agent" (runtime deduct), "prepaid" (wallet-backed change), or "stripe" (Stripe-backed refund / renewal). |
providerref | string|null | Provider-side reference (Stripe session / invoice id, etc.). |
metadata | object|null | Arbitrary JSON context (skill, tier, period, prorated charge, …). |
uuid | string|null | UUID of the user who triggered the event. "system" for cron-driven events. |
user | object|null | Resolved actor: { uuid, firstname, lastname, email, username, avatar, avatarinitials }. null when uuid is "system" (automation / cron renewals) or when the user record was deleted. |
createdat | number | Unix seconds. |
total so you can render a precise paginator. Default page size is 50, max is 500. Use start to skip rows.Reading the Ledger — Patterns
Daily / weekly reports
Filter client-side on createdat to bucket transactions per day or per skill:
import requests
from collections import defaultdict
from datetime import datetime
resp = requests.post(
"https://api.wiro.ai/v1/UserAgent/TransactionList",
headers={"x-api-key": "YOUR_API_KEY", "Content-Type": "application/json"},
json={"useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321", "limit": 500}
).json()
per_day_burn = defaultdict(int)
for tx in resp["transactions"]:
if tx["type"] == "deduct":
day = datetime.utcfromtimestamp(tx["createdat"]).strftime("%Y-%m-%d")
per_day_burn[day] += abs(tx["amount"])
for day in sorted(per_day_burn):
print(f"{day}: {per_day_burn[day]} credits")
Audit a billing-period rollover
type: "renewal" rows are written exactly once per billing-period rollover. Pair them with the most recent summary.creditperiod to confirm the new period started on time.
const renewals = transactions.filter(t => t.type === 'renewal');
const lastRenewal = renewals[0]; // newest-first ordering
console.log('Last renewal:', new Date(lastRenewal.createdat * 1000).toISOString());
console.log('Granted:', lastRenewal.amount, 'credits');
console.log('Now in period:', summary.creditperiod);
Agent → API Sync (TransactionInsert & CreditSync)
Agent runtimes report deductions through a separate authenticated endpoint, POST /UserAgent/TransactionInsert. API users don't normally call this — it's reserved for the Wiro-managed agent runtime. It is documented here for completeness.
The endpoint is idempotent on guid: a retry of the same deduction event returns the post-merge balance without double-charging.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Target useragent guid (must match the agent container's settings) |
uuid | string | Yes | Owner uuid (must match the useragent) |
type | string | Yes | Must be "deduct" |
action | string | No | "message", "create", "modify", "regenerate" |
amount | number | Yes | Positive cost or pre-signed negative delta |
guid | string | No | Daemon-side stable identifier (recommended for idempotency) |
sessionkey | string | No | Session that consumed the credits |
messageguid | string | No | Message that triggered the deduction |
Response
{
"result": true,
"errors": [],
"guid": "7f3e8c21-1be1-4f5a-96e8-2b1a9e2a6a01",
"idempotent": false,
"monthlycredits": 5000,
"extracredits": 2000,
"usedcredits": 1460,
"remainingcredits": 5540
}
idempotent: true means the event guid was already in the ledger — no double-charge happened, and the response carries the current balance for the agent to reconcile against.
POST /UserAgent/CreditSync — periodic counter sync
In addition to per-deduction events, the agent container periodically pushes its monotonic total usedcredits counter. Server policy:
- Within the same billing period (
creditperiodmatches), the server storesGREATEST(existing, incoming)— out-of-order syncs can never lower the counter. - If the incoming
creditperioddoesn't match the DB row (e.g. a renewal already rolled over), the sync is rejected and the response carries the canonical period + counter for the agent to reset its local state.
Errors
| Error | When |
|---|---|
useragentguid is required | TransactionList / TransactionInsert / CreditSync without useragentguid |
useragent-access-denied | Caller is neither owner, team member, nor admin |
Only deduct transactions can be inserted from agents | TransactionInsert with type other than "deduct" |
period mismatch (expected X, got Y) | CreditSync with a stale creditperiod after a renewal rollover |
Invalid credentials | TransactionInsert / CreditSync with a (useragentguid, uuid) pair that doesn't match a row |
transactions-list-failed | TransactionList server-side error (DB) |
What's Next
- Agent Logs — Activity feed (tool calls, cron runs, message exchanges) for the same useragent
- Agent Overview — Full agent endpoint catalog, including subscription / billing operations
- Agent Builder — Build a custom agent and watch the ledger fill up as it runs
Agent Logs
Per-instance activity feed — tool calls, scheduled cron runs, message exchanges, and turn boundaries — for any deployed agent.
Overview
Every Wiro agent container runs a small wiro-commands plugin that appends one structured ActivityEvent JSON line per tool call, turn boundary, session boundary, message exchange, and synthesized cron run. The plugin redacts secrets-by-field-name, high-entropy assignment values, internal tool-call IDs, and the API URL/credentials before writing — so what shows up in Logs is safe to show end users.
A daily JSONL file is written for each calendar day; files older than 7 days are gzipped, files older than 180 days are deleted by the agent's daily maintenance cron.
The endpoints below are owner-or-team-member scoped. Team admins can read any team agent; outside callers receive useragent-access-denied.
| Endpoint | Purpose |
|---|---|
POST /UserAgent/Logs | Live tail (last N events for today, or a specific date). Cached server-side for 30s on non-admin callers to absorb polling. |
POST /UserAgent/LogsList | List the date strings (YYYY-MM-DD) for which an activity file exists. |
POST /UserAgent/LogsFile | Read the full JSONL file for a specific date. |
POST /UserAgent/LogsDelete | Delete one date's activity file (and its gzipped sibling). Idempotent. |
Message/History. Message/History is a chat-bubble-level read of the conversation. Logs is a runtime view: every tool call, every scheduled trigger, every session boundary the agent touched. They serve different audiences (Message/History for end users, Logs for operators / power users).ActivityEvent Shape
Every event is a JSON object with this base shape:
{
"ts": 1714694410,
"kind": "tool",
"action": "wordpress-publish",
"skill": "int-wordpress-post",
"session": "user-42",
"userUuid": "ada-uuid",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"details": {
"title": "Weekly Roundup",
"url": "https://blog.example.com/weekly-roundup-12"
}
}
| Field | Type | Description |
|---|---|---|
ts | number | Unix seconds when the event was emitted inside the container. |
kind | string | Event class: "tool", "turn", "session", "message", "cron", "deduct", "system". |
action | string | Fine-grained label: e.g. "wordpress-publish", "telegram-send", "message", "cron-tick", "agent_start". |
skill | string|null | The skill that produced the event. null for system events. |
session | string|null | Sessionkey the event belongs to (chat events only). |
userUuid | string|null | UUID of the user who triggered the event. "system" for cron / internal events. |
user | object|null | Resolved actor: { uuid, firstname, lastname, email, username, avatar, avatarinitials }. null when userUuid is "system" (automation / cron) or when the user record was deleted. |
details | object | Event-specific payload. Always redacted. |
POST /UserAgent/Logs
Live tail of the agent's activity feed. Returns the last N events for the requested date (defaults to today).
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Your UserAgent instance guid. |
date | string | No | YYYY-MM-DD to read a specific day's file. Default: today (UTC). |
lines | number | No | Max events to return (max 5000). Default: 200. |
Caching: the endpoint is cached server-side for 30 seconds per (useragentguid, date, lines) triple to absorb polling bursts. Plan for ≥30 s between repeated polls of the same key.
Request
curl -X POST "https://api.wiro.ai/v1/UserAgent/Logs" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321", "lines": 200 }'
Response
{
"result": true,
"errors": [],
"date": "2026-05-03",
"totalLines": 1428,
"events": [
{
"ts": 1714694412,
"kind": "deduct",
"action": "create",
"skill": "int-wordpress-post",
"session": null,
"userUuid": "system",
"user": null,
"details": { "cost": 60, "balanceafter": 4940 }
},
{
"ts": 1714694410,
"kind": "tool",
"action": "wordpress-publish",
"skill": "int-wordpress-post",
"session": null,
"userUuid": "system",
"user": null,
"details": {
"title": "Weekly Roundup",
"url": "https://blog.example.com/weekly-roundup-12",
"categories": ["AI", "Tutorial"]
}
},
{
"ts": 1714694200,
"kind": "cron",
"action": "cron-tick",
"skill": "cs-cron-blog-scanner",
"session": null,
"userUuid": "system",
"user": null,
"details": { "interval": "0 9 * * *", "trigger": "scheduled" }
},
{
"ts": 1714693100,
"kind": "message",
"action": "agent_end",
"skill": null,
"session": "user-42",
"userUuid": "ada-uuid",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"details": {
"messageguid": "5c41dabf-f2be-4aa8-a5a4-8c9e3d2f3f11",
"prompt": "Draft a weekly roundup post for the past week.",
"elapsedTime": "8.1s",
"wordCount": 412
}
}
]
}
user field. Every event is decorated with the resolved actor: { uuid, firstname, lastname, email, username, avatar, avatarinitials }. It is null when:
- The event was triggered by automation —
userUuid: "system"for cron ticks, scheduled-skill runs, automated deductions, agent-initiated tool calls, and similar non-interactive writes (the three system / cron rows above). - The actor's user record was deleted or the lookup misses for any reason.
POST /UserAgent/LogsList
Returns the list of dates for which an activity file exists for this agent. Useful for rendering a date-picker.
Response
{
"result": true,
"errors": [],
"dates": [
{ "date": "2026-05-03", "filename": "2026-05-03.jsonl", "size": 184220, "compressed": false },
{ "date": "2026-05-02", "filename": "2026-05-02.jsonl.gz", "size": 41280, "compressed": true },
{ "date": "2026-05-01", "filename": "2026-05-01.jsonl.gz", "size": 38912, "compressed": true }
]
}
compressed: true means the file has been gzipped (happens after 7 days). Reading either form via LogsFile returns the same decoded events.
POST /UserAgent/LogsFile
Reads the full JSONL file for a specific date and returns every event in it. Use this for "Download today's activity" buttons or for off-band analysis.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Your UserAgent instance guid. |
date | string | Yes | YYYY-MM-DD of the file to read. |
Response
{
"result": true,
"errors": [],
"date": "2026-05-02",
"truncated": false,
"events": [
{
"ts": 1714000000,
"kind": "system",
"action": "agent_start",
"skill": null,
"session": null,
"userUuid": "system",
"user": null,
"details": {}
},
{
"ts": 1714000020,
"kind": "session",
"action": "session_start",
"skill": null,
"session": "user-42",
"userUuid": "ada-uuid",
"user": {
"uuid": "ada-uuid",
"firstname": "Ada",
"lastname": "Lovelace",
"email": "[email protected]",
"username": "ada",
"avatar": "https://cdn.wiro.ai/avatars/ada.webp",
"avatarinitials": "AL"
},
"details": {}
}
]
}
The user field follows the same resolution rules as the live tail above — see the Logs section's "About the user field" callout.
truncated is true when the file was capped at the worker's max-line ceiling (~50000 events per file). When true, paginate by date — older events for the same day are not retrievable through this endpoint.POST /UserAgent/LogsDelete
Deletes one date's activity file and its gzipped sibling (if present). Idempotent — deleting an already-gone date is a success no-op.
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid | string | Yes | Your UserAgent instance guid. |
date | string | Yes | YYYY-MM-DD of the file to remove. |
Response
{
"result": true,
"errors": [],
"date": "2026-05-01",
"removed": { "plain": false, "gz": true }
}
removed.{plain,gz} reports which physical files were actually deleted. Both false is also a success (file was already gone).
Usage Patterns
Live polling for an in-app activity feed
Polling Logs every 30 seconds is the recommended cadence — the server-side cache TTL is calibrated for exactly this interval, so faster polling won't return fresher data:
async function pollActivity(useragentguid, onEvents) {
let lastTs = 0;
setInterval(async () => {
const { data } = await axios.post(
'https://api.wiro.ai/v1/UserAgent/Logs',
{ useragentguid, lines: 200 },
{ headers: { 'x-api-key': 'YOUR_API_KEY', 'Content-Type': 'application/json' } }
);
const fresh = data.events.filter(e => e.ts > lastTs);
if (fresh.length === 0) return;
onEvents(fresh);
lastTs = fresh[fresh.length - 1].ts;
}, 30_000);
}
Daily download for off-band analysis
# 1) discover available dates
curl -X POST "https://api.wiro.ai/v1/UserAgent/LogsList" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321" }'
# 2) download a specific date's full file
curl -X POST "https://api.wiro.ai/v1/UserAgent/LogsFile" \
-H "x-api-key: YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{ "useragentguid": "f8e7d6c5-b4a3-2190-fedc-ba0987654321", "date": "2026-05-02" }' \
> logs-2026-05-02.json
Retention & Rotation
| Period | What happens |
|---|---|
| Day 0 — 6 | File is plain JSONL (YYYY-MM-DD.jsonl). Live tail + LogsFile read it directly. |
| Day 7 — 180 | File is gzipped (YYYY-MM-DD.jsonl.gz). Reads are transparently decompressed; size in LogsList reports the on-disk compressed bytes. |
| Day 181+ | File is deleted by the agent's daily maintenance cron. LogsList no longer surfaces the date. |
LogsFile download to your own storage.What's Next
- Agent Transactions — Credit ledger for the same useragent
- Agent Messaging — User-facing message exchanges (with full prompts and responses)
- Agent Overview — Full agent endpoint catalog
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 meta-ads 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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "meta-ads", "fieldname": "appid", "fieldvalue": "YOUR_META_APP_ID" },
{ "credentialkey": "meta-ads", "fieldname": "appsecret", "fieldvalue": "YOUR_META_APP_SECRET" },
{ "credentialkey": "meta-ads", "fieldname": "authmethod", "fieldvalue": "own" }
]
}'
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 fieldstatus: "user" fields are accepted. appid and appsecret are user-writable in the meta-ads template. Attempts to set OAuth session / platform-managed fields are rejected with agent-fieldstatus-not-allowed-for-role. Call POST /UserAgent/Detail and inspect the credentials.metaads shape (available fields + _connected / optional / extra flags) 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: 1so the agent 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/CredentialUpsert 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-pages credential group, not meta-ads.
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 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,
"errors": [],
"accesstoken": "EAAB...",
"refreshtoken": "AQX..."
}
refreshtoken is empty for providers that don't issue one (Instagram, Facebook Pages, Meta Ads, Google Ads usually). See Automatic token refresh for the full cron schedule.
Using the Skill
Enable int-metaads-manage on the agent via POST /UserAgent/SkillsApply (see Agent Skills → Toggling Integration Skills). Adjust the cron of the built-in cs-cron-performance-reporter task (Meta Ads Manager) with enabled and interval only — the task body (value) is owned by the bundled integration skill and silently dropped on writes:
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "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 Preference Skills.
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/CredentialUpsert 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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "facebook-pages", "fieldname": "appid", "fieldvalue": "YOUR_META_APP_ID" },
{ "credentialkey": "facebook-pages", "fieldname": "appsecret", "fieldvalue": "YOUR_META_APP_SECRET" },
{ "credentialkey": "facebook-pages", "fieldname": "authmethod", "fieldvalue": "own" }
]
}'
Wiro merges this into only the facebook-pages 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), call POST /UserAgent/CustomSkillUpsert with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for bundled crons.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "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 Preference Skills.
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/CredentialUpsert 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 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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "instagram", "fieldname": "appid", "fieldvalue": "YOUR_META_APP_ID" },
{ "credentialkey": "instagram", "fieldname": "appsecret", "fieldvalue": "YOUR_META_APP_SECRET" },
{ "credentialkey": "instagram", "fieldname": "authmethod", "fieldvalue": "own" }
]
}'
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), call POST /UserAgent/CustomSkillUpsert with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for bundled crons:
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "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 Preference Skills.
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/CredentialUpsert 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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "linkedin", "fieldname": "clientid", "fieldvalue": "YOUR_LINKEDIN_CLIENT_ID" },
{ "credentialkey": "linkedin", "fieldname": "clientsecret", "fieldvalue": "YOUR_LINKEDIN_CLIENT_SECRET" },
{ "credentialkey": "linkedin", "fieldname": "organizationid", "fieldvalue": "12345678" },
{ "credentialkey": "linkedin", "fieldname": "authmethod", "fieldvalue": "own" }
]
}'
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), call POST /UserAgent/CustomSkillUpsert with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for bundled crons.
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "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 Preference Skills.
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/CredentialUpsert 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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "twitter", "fieldname": "clientid", "fieldvalue": "YOUR_X_CLIENT_ID" },
{ "credentialkey": "twitter", "fieldname": "clientsecret", "fieldvalue": "YOUR_X_CLIENT_SECRET" },
{ "credentialkey": "twitter", "fieldname": "authmethod", "fieldvalue": "own" }
]
}'
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), call POST /UserAgent/CustomSkillUpsert with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for bundled crons:
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "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 Preference Skills.
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/CredentialUpsert 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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "tiktok", "fieldname": "clientkey", "fieldvalue": "YOUR_TIKTOK_CLIENT_KEY" },
{ "credentialkey": "tiktok", "fieldname": "clientsecret", "fieldvalue": "YOUR_TIKTOK_CLIENT_SECRET" },
{ "credentialkey": "tiktok", "fieldname": "authmethod", "fieldvalue": "own" }
]
}'
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).
https://www.googleapis.com/auth/content / adwords / youtube / analytics.readonly scope family through Wiro's Cloud project. If you're setting up "own" mode and want any of those integrations, enable the matching Google Cloud APIs in the same project:
- Google Ads API — for Google Ads
- YouTube Data API v3 + YouTube Analytics API v2 — for YouTube (used by the
youtube-manageskill, also consumed bygoogleads-managefor Video / Demand Gen campaigns) - Google Analytics Data API + Google Analytics Admin API — for Google Analytics 4
- Merchant API — for Merchant Center
curl -X POST \
"https://merchantapi.googleapis.com/accounts/v1/accounts/{YOUR_MC_ID}/developerRegistration:registerGcp" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{}'
This call is made once per GCP project. After it returns a DeveloperRegistration resource, the same GCP can call the Merchant API against any merchant account whose admin grants OAuth consent — no per-account registration needed (see Google's 3P/agency guidance). Each GCP can be registered with at most one primary Merchant Center at a time; registering against a second account returns ALREADY_REGISTERED.
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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "google-ads", "fieldname": "clientid", "fieldvalue": "YOUR_GOOGLE_OAUTH_CLIENT_ID" },
{ "credentialkey": "google-ads", "fieldname": "clientsecret", "fieldvalue": "YOUR_GOOGLE_OAUTH_CLIENT_SECRET" },
{ "credentialkey": "google-ads", "fieldname": "developertoken", "fieldvalue": "YOUR_GOOGLE_ADS_DEVELOPER_TOKEN" },
{ "credentialkey": "google-ads", "fieldname": "managercustomerid", "fieldvalue": "1234567890" },
{ "credentialkey": "google-ads", "fieldname": "authmethod", "fieldvalue": "own" }
]
}'
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) by calling POST /UserAgent/CustomSkillUpsert with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for bundled crons:
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "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 Preference Skills.
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 google-ads 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
YouTube Integration
Connect your agent to YouTube Data API v3 and YouTube Analytics API v2 for channel videos listing, video asset selection for Google Ads Video/Demand Gen campaigns, and performance analytics.
Overview
The YouTube integration uses Google OAuth 2.0 with:
- YouTube Data API v3 — channel info, video listings, upload metadata
- YouTube Analytics API v2 — view counts, watch time, subscriber metrics
Skills that use this integration:
int-youtube-manage— Channel videos listing, video asset selection for Google Ads Video/Demand Gen campaigns, performance analytics
Agents that typically enable this integration:
- Google Ads Manager — for creating Video and Demand Gen campaigns that reference existing YouTube videos
- Social Manager — for publishing to YouTube Shorts (roadmap)
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" | Available | One-click connect using Wiro's Google Cloud project. |
"own" | Available | Own Google Cloud project + OAuth client. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A YouTube channel the connecting user owns.
- (Own mode) A Google Cloud project with YouTube Data API v3 and YouTube Analytics API enabled.
- An HTTPS callback URL.
Wiro Mode
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/YTConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"redirecturl": "https://your-app.com/settings/integrations"
}'
After consent the user returns with ?yt_connected=true&yt_channels=[{channelid,channeltitle}]. Present the channel picker and call YTSetChannel:
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/YTSetChannel" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"channelid": "UC...",
"channeltitle": "My Channel"
}'
Own Mode
Step 1: Create GCP project + enable YouTube APIs
- console.cloud.google.com → create a project.
- APIs & Services → Library — enable:
- OAuth consent screen:
- External user type for multi-tenant use
- App name, support email
- Add scopes:
youtube,youtube.readonly,yt-analytics.readonly
Step 2: Create OAuth Client
APIs & Services → Credentials → Create Credentials → OAuth client ID:
- Application type: Web application
- Authorized redirect URIs:
https://api.wiro.ai/v1/UserAgentOAuth/YTCallback
Step 3: Connect
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/YTConnect" \
-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"
}'
Disconnect
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/YTDisconnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "useragentguid": "your-useragent-guid" }'
Status
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/YTStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "useragentguid": "your-useragent-guid" }'
Returns { result: true, connected: true, channelid, channeltitle, connectedat }.
What the agent does with this integration
Channel video listing
Agent lists channel uploads:
Operator → "show my last 20 YouTube videos"
Agent → GET /youtube/v3/playlistItems?playlistId=UU{channel}
returns 20 items with video IDs, titles, dates, durations
Video asset for Google Ads campaigns
Google Ads Manager agent uses this skill to pick video assets for Video and Demand Gen campaigns:
Agent → /youtube-manage list last 30 days of videos
→ picks top 3 by views
→ /googleads-manage create Video campaign with these as creatives
Performance analytics
Agent queries video-level metrics:
Operator → "which videos performed best last month?"
Agent → POST /youtubeAnalytics/v2/reports
with dimensions=[video], metrics=[views, averageViewDuration, estimatedMinutesWatched]
Skill reference
- agent-skills — custom skill schema and the
int-youtube-managekey
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
| Empty channel list | User has no YouTube channel | User creates a channel |
quotaExceeded | Daily quota hit | Wait 24h or request higher quota from Google |
channelNotFound | Channel deleted or moved | User re-selects via YTSetChannel |
invalid_grant | Refresh token expired | Re-connect via YTConnect |
Google Analytics 4 Integration
Connect your agent to Google Analytics 4 (GA4) for conversion reporting, audience listing, and attribution cross-checks against your advertising platforms.
Overview
The GA4 integration uses Google OAuth 2.0 with the GA4 Data API v1beta and the GA4 Admin API v1beta/v1alpha.
Skills that use this integration:
int-ga4-analytics— Direct GA4 reporting + Google Ads / Meta Ads attribution cross-check
Agents that typically enable this integration:
- Google Ads Manager
- Meta Ads Manager
- Any agent that needs to audit ad-reported conversions against GA4
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" | Available | One-click connect using Wiro's Google Cloud project. |
"own" | Available | Own Google Cloud project + OAuth client. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A GA4 Property the connecting user administers.
- (Own mode) A Google Cloud project with GA4 Data API and GA4 Admin API enabled.
- An HTTPS callback URL.
Wiro Mode
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GA4Connect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"redirecturl": "https://your-app.com/settings/integrations"
}'
The response returns { result: true, authorizeUrl: "..." }. Redirect the user to authorizeUrl.
After consent, the user returns with ?ga4_connected=true&ga4_properties=[{propertyid,propertydisplayname,accountname}]. If the user has multiple GA4 properties, present them in a picker and call GA4SetProperty:
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GA4SetProperty" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"propertyid": "123456789",
"propertydisplayname": "MyApp — Production"
}'
Own Mode
Step 1: Create a Google Cloud Project
- console.cloud.google.com → create a project.
- APIs & Services → Library — enable:
- OAuth consent screen:
- External user type for multi-tenant use
- App name, support email, developer contact
- Add scopes:
analytics.readonly,analytics.edit
Step 2: Create OAuth Client
APIs & Services → Credentials → Create Credentials → OAuth client ID:
- Application type: Web application
- Authorized redirect URIs:
https://api.wiro.ai/v1/UserAgentOAuth/GA4Callback
Copy the Client ID and Client Secret to your agent credentials.
Step 3: Connect
Submit the Client ID, Client Secret via agent credential update (POST /UserAgent/CredentialUpsert), then trigger GA4Connect in own mode:
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GA4Connect" \
-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"
}'
Step 4: Property Picker
After GA4 consent the user returns with ?ga4_properties=[...]. Present the list, let the user choose, then call GA4SetProperty as shown above.
Disconnect
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GA4Disconnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "useragentguid": "your-useragent-guid" }'
Clears accesstoken, refreshtoken, propertyid, propertydisplayname and resets _connected to false. The credential template shape is preserved so the UI can re-offer Connect.
Status
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GA4Status" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "useragentguid": "your-useragent-guid" }'
Returns { result: true, connected: true, propertyid, propertydisplayname, connectedat }.
What the agent does with this integration
Direct reporting
Agent runs GA4 reports on operator request:
Operator → "show paid search performance last 14 days"
Agent → POST /v1beta/properties/123456789:runReport
with dimensions=[sessionDefaultChannelGroup]
and metrics=[sessions, purchases, totalRevenue]
Attribution cross-check
Agent cross-checks ad platform conversions against GA4 paid traffic for the same period, flags >20% deltas.
Audience listing
Agent reads GA4 predefined + custom audiences via Admin API and can propose Customer Match sync candidates.
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
invalid_grant | Refresh token expired | Re-connect via GA4Connect |
PERMISSION_DENIED | User not a property administrator | Ask user to add the GA account |
403 Request had insufficient authentication | Missing scopes | Re-consent with full scope set |
| Empty property picker | User has no GA4 property access | Instruct user to create/gain access |
Skill reference
- agent-skills — custom skill schema and the
int-ga4-analyticskey
Google Merchant Center Integration
Connect your agent to Google Merchant Center (Shopping) for product feed management, status issues, and shopping reports.
Overview
The Merchant Center integration uses the Merchant API v1 (the newer replacement for the deprecated Content API for Shopping v2.1).
Skills that use this integration:
int-merchant-center— Product feed management, status issue scanning, shipping/tax, orders
Agents that typically enable this integration:
- Google Ads Manager — for Shopping and PMax campaigns that depend on the product feed
Availability
| Mode | Status | Notes |
|---|---|---|
"wiro" | Available | One-click connect using Wiro's Google Cloud project (already registered against Wiro's developer GCP). |
"own" | Available | Requires a one-time developer registration of your GCP project against your primary Merchant Center. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Merchant Center account the connecting user administers.
- (Own mode) A Google Cloud project with Merchant API enabled.
- (Own mode) Developer Registration — a one-time step to link your GCP project to your primary MC account.
- An HTTPS callback URL.
Wiro Mode
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MCConnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"redirecturl": "https://your-app.com/settings/integrations"
}'
After consent the user returns with ?mc_connected=true&mc_accounts=[{merchantid,accountname}]. Present the picker and call MCSetMerchantId:
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MCSetMerchantId" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"merchantid": "5769377374",
"accountname": "Acme Store"
}'
Own Mode
Step 1: Create GCP project + enable Merchant API
- console.cloud.google.com → create a project.
- APIs & Services → Library → enable Merchant API.
- OAuth consent screen:
- External user type for multi-tenant use
- App name, support email
- Add scope:
https://www.googleapis.com/auth/content
Step 2: Create OAuth Client
APIs & Services → Credentials → Create Credentials → OAuth client ID:
- Application type: Web application
- Authorized redirect URIs:
https://api.wiro.ai/v1/UserAgentOAuth/MCCallback
Step 3: Register Developer Registration (REQUIRED — one-time)
Merchant API requires that your GCP project be registered against your primary Merchant Center account before it will answer for any merchant account you authorize:
curl -X POST \
"https://merchantapi.googleapis.com/accounts/v1/accounts/{YOUR_PRIMARY_MC_ID}/developerRegistration:registerGcp" \
-H "Authorization: Bearer {ACCESS_TOKEN}" \
-H "Content-Type: application/json" \
-d '{}'
Where:
YOUR_PRIMARY_MC_ID— your own Merchant Center ID (you, the developer)ACCESS_TOKEN— a fresh access token for your primary MC account
Once registered, your GCP project can call Merchant API on any merchant account that a user authorizes through OAuth.
Step 4: Connect
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MCConnect" \
-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"
}'
Disconnect
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MCDisconnect" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "useragentguid": "your-useragent-guid" }'
Status
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/MCStatus" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{ "useragentguid": "your-useragent-guid" }'
Skill reference
- agent-skills — custom skill schema and the
int-merchant-centerkey
Troubleshooting
| Symptom | Cause | Fix |
|---|---|---|
PERMISSION_DENIED on any merchant call | GCP project not registered | Complete Step 3 once |
invalid_grant | Refresh token expired | Re-connect via MCConnect |
No Merchant Center accounts available | User has no MC access | User creates/gains access at merchants.google.com |
| Content API deprecation error | You're on old v2.1 endpoints | Switch to v1 (merchantapi.googleapis.com) |
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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "hubspot", "fieldname": "clientid", "fieldvalue": "YOUR_HUBSPOT_CLIENT_ID" },
{ "credentialkey": "hubspot", "fieldname": "clientsecret", "fieldvalue": "YOUR_HUBSPOT_CLIENT_SECRET" },
{ "credentialkey": "hubspot", "fieldname": "authmethod", "fieldvalue": "own" }
]
}'
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
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "mailchimp", "fieldname": "clientid", "fieldvalue": "YOUR_MAILCHIMP_CLIENT_ID" },
{ "credentialkey": "mailchimp", "fieldname": "clientsecret", "fieldvalue": "YOUR_MAILCHIMP_CLIENT_SECRET" },
{ "credentialkey": "mailchimp", "fieldname": "authmethod", "fieldvalue": "own" }
]
}'
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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "mailchimp", "fieldname": "apiKey", "fieldvalue": "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) by calling POST /UserAgent/CustomSkillUpsert with enabled and interval only — cron skill bodies are template-controlled and value is silently ignored for bundled crons:
curl -X POST "https://api.wiro.ai/v1/UserAgent/CustomSkillUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"skillkey": "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 Preference Skills.
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 for reading, writing, and managing files in selected folders.
Overview
The Google Drive integration uses a Google Cloud service account with folder access delegated from your Drive. You create the service account in your own Google Cloud project, share your Drive folders with the service account email, and the agent accesses only those shared folders.
Skills that use this integration:
google-drive— Read files, write outputs, manage folders in selected Drive folders
Agents that typically enable this integration:
- Google Ads Manager (creative assets for campaigns)
- Meta Ads Manager (creative assets for campaigns)
- Social Manager (post-ready media library)
Availability
| Mode | Status | Notes |
|---|---|---|
| Service Account JSON | Available | Google Cloud service account with Drive API access. Folders shared by the user. |
Prerequisites
- A Wiro API key — Authentication.
- A deployed agent — Agent Overview.
- A Google account with a Drive you want the agent to access.
- A Google Cloud project to host the service account.
Setup
Step 1: Enable the Google Drive API
Google Cloud Console → select project → APIs & Services → Library → Google Drive API → Enable.
If your agent will also read/write Google Docs or Sheets, enable those APIs as well:
- Google Sheets API
- Google Docs API
Step 2: Create a service account
- IAM & Admin → Service accounts → Create service account.
- Name (e.g. "wiro-drive-agent").
- Skip role grant → Done.
- Open the service account → Keys → Add key → Create new key → JSON. Download.
- Note the service account email from the account details page — format:
[email protected].
Tip: In My Agents → open your agent → Credentials, upload the JSON and your service account email will appear with a Copy button.
Step 3: Share your Drive folders with the service account
For each folder you want the agent to access:
- Open Google Drive.
- Right-click the folder → Share.
- Paste the service account email from Step 2.
- Set role to Editor (if the folder lives in your My Drive) or Content manager (if the folder lives in a Shared Drive).
- Click Send.
Copy each folder's ID from its Drive URL — the part after /folders/. Example: from https://drive.google.com/drive/folders/1BxiMVs0XRA5nFMdKvBd the ID is 1BxiMVs0XRA5nFMdKvBd.
Google Workspace users: If you use a Shared Drive (Team Drive), add the service account as a member of the shared drive instead of sharing individual folders. All folders within the shared drive become accessible.
Step 4: Base64-encode the JSON
# Linux
base64 -w 0 drive-service-account.json > drive-sa.b64
# macOS
base64 -b 0 drive-service-account.json > drive-sa.b64
Step 5: Save to Wiro
curl -X POST "https://api.wiro.ai/v1/UserAgent/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "googledrive", "fieldname": "serviceaccountjsonbase64", "fieldvalue": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii..." },
{ "credentialkey": "googledrive", "parentfield": "folders", "ordinal": 0, "fieldname": "id", "fieldvalue": "1BxiMVs0XRA5nFMdKvBd" },
{ "credentialkey": "googledrive", "parentfield": "folders", "ordinal": 0, "fieldname": "name", "fieldvalue": "Creatives" },
{ "credentialkey": "googledrive", "parentfield": "folders", "ordinal": 1, "fieldname": "id", "fieldvalue": "2CyiNWt1YSB6oGNeL" },
{ "credentialkey": "googledrive", "parentfield": "folders", "ordinal": 1, "fieldname": "name", "fieldvalue": "Ad Assets" }
]
}'
| Field | Type | Description |
|---|---|---|
serviceaccountjsonbase64 |
string | Base64-encoded service account JSON key. |
folders |
array | Array of { "id": string, "name": string } objects the agent should scan. name is the human-readable label — the agent uses it when reporting back to the operator ("scanned the Creatives folder..."); id is the Drive folder ID used in API calls. Pass an empty array to clear. |
Step 5b (optional): Discover folders via API
If you don't already have folder IDs from Step 3, or you want to verify that the service account has access to the expected folders before saving them, call the folder discovery endpoint. This is the same endpoint the Wiro Dashboard uses for its folder picker.
curl -X POST "https://api.wiro.ai/v1/UserAgentOAuth/GoogleDriveListFolders" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"serviceaccountjsonbase64": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii..."
}'
| Parameter | Type | Required | Description |
|---|---|---|---|
useragentguid |
string | Yes | Agent instance GUID. |
serviceaccountjsonbase64 |
string | No | Base64-encoded SA JSON. Useful for previewing before saving. If omitted, the endpoint uses whatever JSON was already saved to the agent via Step 5. |
Response:
{
"result": true,
"serviceAccountEmail": "[email protected]",
"folders": [
{ "id": "1BxiMVs0XRA5nFMdKvBd", "name": "Creatives", "modifiedTime": "2026-04-10T12:34:00Z", "ownerName": "[email protected]" },
{ "id": "2CyiNWt1YSB6oGNeL", "name": "Ad Assets", "modifiedTime": "2026-04-12T09:12:00Z", "ownerName": "[email protected]" }
]
}
Only folders explicitly shared with the SA email (as Editor or Content manager — see Step 3) are returned. Take the id values from the folders you want the agent to scan and pass them as the folders array in your final UserAgent/CredentialUpsert call (using parentfield: "folders" + ordinal per entry).
Two equivalent ways to run Steps 5–5b
- Upfront — you already know the folder IDs from Step 3. Call
UserAgent/CredentialUpsertonce with bothserviceaccountjsonbase64andfolders. - Discovery (matches the Dashboard flow) — call
UserAgent/CredentialUpsertfirst with justserviceaccountjsonbase64(leavefoldersempty), then callUserAgentOAuth/GoogleDriveListFoldersto enumerate what the SA can see, then callUserAgent/CredentialUpsertagain with the pickedfoldersarray. The Dashboard uses this pattern — JSON upload renders the service account email, the user shares folders with it in Drive, and the folder picker lists the results.
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 when the google-drive skill is enabled:
GDRIVE_FOLDERS— a JSON array string of[{"id": "...", "name": "..."}], e.g.[{"id":"1MMZGo...","name":"Creatives"}]. Empty array[]means no folder is configured. Inside the agent, parse withjq(e.g.echo $GDRIVE_FOLDERS | jq -r '.[].id'for IDs, orjq -r '.[] | select(.name=="Creatives") | .id'to resolve a folder ID by name).
Secret file:
/run/secrets/gdrive-sa.json— decoded service account (file, not env)
Auth: OAuth access token minted from the service account on-demand via the gdrive-token bin script → Authorization: Bearer. Token expires every hour; the script is called fresh before each API session.
Base URLs:
- Drive API:
https://www.googleapis.com/drive/v3/... - Sheets API:
https://sheets.googleapis.com/v4/... - Docs API:
https://docs.googleapis.com/v1/...
All list/get/upload calls pass supportsAllDrives=true + includeItemsFromAllDrives=true, so Shared Drives are supported automatically.
Files created by the agent
When the agent creates a file inside a user-shared folder, the file is owned by the service account, not the user. The user can see the file via folder permission inheritance, but may see it as "view only". To give the user write access on files the agent creates (role writer in the Drive API — shown as Editor in My Drive or Content manager in a Shared Drive), the skill grants permissions programmatically via POST /drive/v3/files/{fileId}/permissions. See the google-drive skill for details.
Troubleshooting
- 403 "The user does not have sufficient permissions for file": The folder hasn't been shared with the service account, or the service account was given a read-only role (Viewer / Commenter) instead of a write role. Go back to Google Drive → Share the folder with the SA email as Editor (My Drive) or Content manager (Shared Drive).
- 403 on specific file inside a shared folder: The file was added by someone else and hasn't inherited folder permissions yet. Force inheritance by resharing the folder, or share the specific file directly.
- "Invalid JWT token": Service account JSON corrupt or truncated. Re-encode (watch for line breaks — use
base64 -w 0on Linux,base64 -b 0on macOS). - Agent can't find new files: The agent only sees files inside folders listed in
folders. Add new folder IDs to the list and restart. - User sees files as "view only": Files created by the SA inside user folders are SA-owned. The skill should grant the user write access (role
writer— Editor in My Drive, Content manager in Shared Drive); if this step is skipped, the user sees view-only. Manual fix: right-click the file → Share → grant yourself write access.
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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "gmail", "fieldname": "account", "fieldvalue": "[email protected]" },
{ "credentialkey": "gmail", "fieldname": "apppassword", "fieldvalue": "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 customskills[] (a cron wrapper that invokes the built-in gmail-check platform skill). To change how often the inbox is polled, update
the cron-gmail-checker skill's interval via POST /UserAgent/CustomSkillUpsert — 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 customskills[] 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": {
"_connected": false,
"optional": true,
"extra": false,
"account": "",
"apppassword": ""
}
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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "sys-telegram", "fieldname": "bottoken", "fieldvalue": "123456:ABC-DEF1234ghIkl-zyx57W2v1u123ew11" },
{ "credentialkey": "sys-telegram", "fieldname": "allowedusers", "fieldvalue": "[\"761381461\", \"987654321\"]" },
{ "credentialkey": "sys-telegram", "fieldname": "sessionmode", "fieldvalue": "private" }
]
}'
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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "firebase", "parentfield": "accounts", "ordinal": 0, "fieldname": "appname", "fieldvalue": "My App" },
{ "credentialkey": "firebase", "parentfield": "accounts", "ordinal": 0, "fieldname": "serviceaccountjsonbase64", "fieldvalue": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii..." },
{ "credentialkey": "firebase", "parentfield": "accounts.0.apps", "ordinal": 0, "fieldname": "platform", "fieldvalue": "ios" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.apps", "ordinal": 0, "fieldname": "id", "fieldvalue": "6479306352" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.apps", "ordinal": 1, "fieldname": "platform", "fieldvalue": "android" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.apps", "ordinal": 1, "fieldname": "id", "fieldvalue": "com.example.app" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.topics", "ordinal": 0, "fieldname": "topickey", "fieldvalue": "locale_en" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.topics", "ordinal": 0, "fieldname": "topicdesc", "fieldvalue": "English users" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.topics", "ordinal": 1, "fieldname": "topickey", "fieldvalue": "tier_paid" },
{ "credentialkey": "firebase", "parentfield": "accounts.0.topics", "ordinal": 1, "fieldname": "topicdesc", "fieldvalue": "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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "wordpress", "fieldname": "url", "fieldvalue": "https://blog.example.com" },
{ "credentialkey": "wordpress", "fieldname": "user", "fieldvalue": "WiroBlogAgent" },
{ "credentialkey": "wordpress", "fieldname": "apppassword", "fieldvalue": "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": {
"_connected": false,
"optional": false,
"extra": false,
"url": "",
"user": "",
"apppassword": ""
}
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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "apple-appstore", "fieldname": "keyid", "fieldvalue": "ABC1234DEF" },
{ "credentialkey": "apple-appstore", "fieldname": "issuerid", "fieldvalue": "12345678-1234-1234-1234-123456789012" },
{ "credentialkey": "apple-appstore", "fieldname": "privatekeybase64", "fieldvalue": "LS0tLS1CRUdJTi..." },
{ "credentialkey": "apple-appstore", "fieldname": "appids", "fieldvalue": "[\"6479306352\", \"1234567890\"]" },
{ "credentialkey": "apple-appstore", "fieldname": "supportemail", "fieldvalue": "[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) — note the canonical key is apple-appstore-apps, a separate registry credential from the standard apple-appstore:
{
"credentials": {
"apple-appstore-apps": {
"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.
- Note the service account email from the account details page — format:
[email protected].
Tip: In My Agents → open your agent → Credentials, upload the JSON and your service account email will appear with a Copy button.
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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "google-play", "fieldname": "serviceaccountjsonbase64", "fieldvalue": "eyJ0eXBlIjoic2VydmljZV9hY2NvdW50Ii..." },
{ "credentialkey": "google-play", "fieldname": "packagenames", "fieldvalue": "[\"com.example.app\"]" },
{ "credentialkey": "google-play", "fieldname": "supportemail", "fieldvalue": "[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)
The canonical key is google-play-apps — a separate registry credential from the standard google-play (service-account-based) credential.
{
"credentials": {
"google-play-apps": {
"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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "apollo", "fieldname": "apiKey", "fieldvalue": "YOUR_APOLLO_API_KEY" },
{ "credentialkey": "apollo", "fieldname": "masterapikey", "fieldvalue": "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": {
"_connected": false,
"optional": true,
"extra": false,
"apiKey": "",
"masterapikey": ""
}
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/CredentialUpsert 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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "lemlist", "fieldname": "apiKey", "fieldvalue": "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": {
"_connected": false,
"optional": true,
"extra": false,
"apiKey": ""
}
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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "brevo", "fieldname": "apiKey", "fieldvalue": "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": {
"_connected": false,
"optional": true,
"extra": false,
"apiKey": ""
}
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/CredentialUpsert" \
-H "Content-Type: application/json" \
-H "x-api-key: YOUR_API_KEY" \
-d '{
"useragentguid": "your-useragent-guid",
"fields": [
{ "credentialkey": "sendgrid", "fieldname": "apiKey", "fieldvalue": "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": {
"_connected": false,
"optional": true,
"extra": false,
"apiKey": ""
}
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/CredentialUpsertcalls 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 + tier 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,
"tier": "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,
"tier": "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, UpgradeTier, CreateSubscriptionCheckout, SkillsApply.
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