Build with Upivia
One HTTP endpoint to give your AI agent controlled access to email, LLMs, web search, voice, and code execution - with dollar budgets, policies, and a full audit trail.
Quickstart
- Sign up at /signup.
- Top up your wallet at /billing.
- Create an agent in /agents — copy the API key (shown once).
- Enable services in /permissions and set monthly budgets.
- POST to
/v1/service-requestsfrom your agent code.
Minimum working example:
# Dispatch a request: send an email through the platform.
# Upivia handles policy + budget checks, debits your wallet,
# calls the underlying provider, and returns a structured result.
curl -X POST https://www.upivia.com/v1/service-requests \
-H "Authorization: Bearer aw_live_..." \
-H "Content-Type: application/json" \
-d '{
"operation": "email.send",
"payload": {
"to": "ada@example.com",
"subject": "Hello from my agent",
"body": "First message routed through Upivia."
}
}'Response:
{
"status": "executed",
"request_id": "req_01HQ...",
"cost_cents": 1,
"balance_cents": 4999,
"result": { "provider_message_id": "..." }
}What you can build
Every app below is one agent + a few /v1/service-requestscalls. No keys to juggle, no per-provider billing, no abuse risk — every call is policy-checked, budgeted, and audit-logged.
Cold-outreach SDR
Research a lead, draft a personalized cold email, send it. Cap at $5/day so a runaway loop costs less than coffee.
web_search.querytext_generation.generateemail.sendTriage + auto-reply bot
Classify inbound tickets, draft a reply, queue refunds over $50 for human approval via the built-in approvals queue.
text_generation.generateemail.sendDeep-research assistant
Fan out 20 Tavily searches, summarize with GPT-4o, return a cited brief. Hard $2/run ceiling per user.
web_search.querytext_generation.generateOn-call voice escalator
When a Sentry alert fires, place a Twilio call to the on-call engineer and read the incident summary aloud.
voice_call.createtext_generation.generateNewsletter generator
Pull the week's top stories in a niche, write a 400-word digest, send to your list. One cron, one agent.
web_search.querytext_generation.generateemail.sendSandboxed code runner
Let users prompt "plot AAPL last 5 years". The agent writes Python, runs it in E2B, returns the chart. Per-user $0.50 cap.
code.executetext_generation.generateSlack /ask bot
Wire one agent to your Slack slash-command. Search the web, summarize, post back. Pay once at the org level.
web_search.querytext_generation.generateLead-enrichment pipeline
For each new HubSpot contact: search LinkedIn, extract company + role, write back to CRM. Approval gate over 100 lookups/day.
web_search.querytext_generation.generateDaily-briefing agent
Every morning at 7am: scan the news in your topics, summarize, email you. Costs about $0.03/day.
web_search.querytext_generation.generateemail.sendMMS promo blast
Send an image + short copy via MMS to an opted-in list. Per-recipient idempotency keys keep retries safe.
mms.sendSMS 2FA fallback
When email 2FA bounces, fall through to SMS. Read inbound replies via sms.read for verification round-trips.
sms.sendsms.readManaged knowledge base (RAG)
Upsert docs into a managed vector store, query with natural language, hand top chunks to GPT-4o. No Pinecone account needed.
knowledge.upsertknowledge.queryknowledge.list_collectionstext_generation.generateVoice-note transcriber
User drops a 5-minute m4a in Slack. Transcribe with Deepgram, summarize with GPT-4o-mini, post back as a thread reply.
speech.transcribetext_generation.generatechat.thread_replyText-to-speech podcast snippet
Turn a blog post into a 60-second ElevenLabs audio teaser. Cache the MP3, post to your CDN.
text_generation.generatespeech.synthesizeStructured extraction over invoices
Hand a raw invoice text blob to text_generation.extract_structured with a JSON schema; get back typed line items. No prompt-engineering ritual.
text_generation.extract_structuredEmbeddings for semantic search
Roll your own search if knowledge.* is too opinionated: compute embeddings, store vectors in your DB, do cosine yourself.
embedding.createBrowser automation: flight prices
Spin up a headless browser, hit Google Flights, extract structured pricing. Screenshot for the audit trail.
browser.runbrowser.extractbrowser.screenshotURL summarizer
User pastes a link. Fetch the page (web.fetch_url), summarize with GPT-4o-mini, return 3 bullets. Cheaper than browser.* when the page is static.
web.fetch_urltext_generation.generateTopic news watcher
Every hour, hit web_search.news for your topics. Diff against last run. New items go to Slack.
web_search.newschat.sendCSV importer with auto-types
User uploads a messy CSV. data.csv_parse infers types, returns rows. Then extract_structured normalizes column names.
data.csv_parsetext_generation.extract_structuredWebhook fan-out
One event in, N webhooks out. The platform handles retries and records every delivery in /audit-logs.
notification.webhookPDF to structured data
Run document.parse (LlamaParse) on a vendor invoice PDF, then extract_structured to typed JSON. End-to-end in two calls.
document.parsetext_generation.extract_structuredAuto-subtitle a video
Upload an mp4, get an SRT back. video.subtitle uses Deepgram under the hood - burn-in is your codec, not ours.
video.subtitleCalendar scheduler
Agent reads your free/busy, proposes 3 slots, creates the event when the invitee picks one. Nylas under the hood.
calendar.readcalendar.createcalendar.updatecalendar.cancelscheduling.send_inviteCRM auto-enrich
Read a HubSpot contact, search the web, write enriched fields back to the same record. crm.read + crm.write handle the auth.
crm.readweb_search.querycrm.writeJira ticket from Sentry error
Sentry webhook fires, summarize the stack trace, ticket.create in Jira/Linear, then ticket.update with the deploy SHA once linked.
text_generation.generateticket.createticket.updateEmail triage from a real inbox
email.search for unreads, email.read to fetch bodies, GPT-4o-mini classifies + drafts, email.reply or email.draft based on confidence.
email.searchemail.readtext_generation.generateemail.replyemail.draftSlack DM digest
Once a day, chat.read each Slack channel you care about, summarize unreads, chat.dm a personalized digest to each opted-in user.
chat.readtext_generation.generatechat.dmPublish a private API to your Upivia
Wrap your internal HTTP API (or a third-party one you have a key for) as a service that only your org can call. Agents dispatch through /v1/service-requests like any built-in.
custom.*Publish a service into the public catalog
You run an API and want every Upivia customer to be able to enable it. Submit an adapter spec, we wire it up, and you become a line item in /admin/services.
custom.*Authentication
Every agent gets a single API key on creation. Pass it as a Bearer token in the Authorization header. We store only an scrypt hash — we cannot recover lost keys, so save it when you create the agent.
# Pass the agent key as a Bearer token on every request. Authorization: Bearer aw_live_xxxxxxxxxxxxxxxxxxxxxxxxx
Create an agent
Create programmatically:
# Create an agent. The response includes the API key exactly once -
# store it somewhere safe (env var, secret manager) before closing the tab.
curl -X POST https://www.upivia.com/v1/agents \
-H "Content-Type: application/json" \
--cookie "authjs.session-token=..." \
-d '{
"name": "support-bot",
"description": "Handles inbound customer email."
}'{
"agent": {
"id": "agt_01HQ...",
"name": "support-bot",
"status": "active",
"created_at": "2026-05-20T..."
},
"api_key": "aw_live_xxxxxxxxxxxxx"
}Or use the dashboard at /agents — click New agent, fill the form, and copy the key from the success modal.
Enable services
By default a new agent has zero permissions. Enable a service by creating a binding with a monthly cap, daily limit, and optional approval threshold.
# Bind a service to an agent with a monthly cap, daily limit,
# optional approval threshold, and per-service configuration.
curl -X POST https://www.upivia.com/v1/agents/agt_01HQ.../services \
-H "Content-Type: application/json" \
--cookie "authjs.session-token=..." \
-d '{
"service_id": "svc_email",
"monthly_budget_cents": 5000,
"daily_request_limit": 100,
"approval_threshold_cents": 500,
"configuration": {
"allowed_domains": ["example.com"]
}
}'What each field means:
| monthly_budget_cents | Hard cap on this agent's spend per operation per month. Wallet still needs funds. |
| daily_request_limit | Max number of calls this agent can make per UTC day. |
| approval_threshold_cents | Any single call costing more than this is queued for human approval. 0 disables. |
| configuration | Per-service config. e.g. allowed_domains for email. |
Disable a service (idempotent):
# Disable a service binding. Idempotent - safe to call repeatedly. curl -X DELETE "https://www.upivia.com/v1/agents/agt_01HQ.../services?service_id=svc_email"
Budgets & policies
Upivia has one balance per organization (your wallet) and many caps per agent×operation. Every service request runs three checks in order:
- Policy check — agent must have an
AgentServiceBindingfor the requested operation. - Budget check — this month's spend on this operation plus the estimated cost must stay under
monthly_budget_cents; daily count underdaily_request_limit. - Approval gate — if the estimated cost exceeds
approval_threshold_cents, the request is parked with statusapproval_requireduntil a human approves it at /approvals.
Only after all three pass does the wallet get debited and the provider call go out. If the wallet itself is empty, the request fails with insufficient_balance regardless of the per-operation cap.
Dispatch a request
The single endpoint your agent calls for every paid action. Synchronous: returns the executed result, blocks until the provider responds.
Node / TypeScript
// Dispatch a text-generation request. The Idempotency-Key dedupes
// retries so a flaky network never double-charges your wallet.
const res = await fetch("https://www.upivia.com/v1/service-requests", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.AGENTWALLET_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
operation: "text_generation.generate",
payload: {
model: "openai/gpt-4o-mini",
messages: [{ role: "user", content: "Summarize this PR." }],
},
}),
});
const data = await res.json();
if (data.status !== "executed") throw new Error(data.reason_code);
console.log(data.result);Python
# Same call from Python. Use uuid4() for the idempotency key on each
# logical action; reuse the same key across retries of that action.
import os, uuid, requests
r = requests.post(
"https://www.upivia.com/v1/service-requests",
headers={
"Authorization": f"Bearer {os.environ['AGENTWALLET_KEY']}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"operation": "web_search.search",
"payload": {"query": "GPT-5 release date"},
},
timeout=30,
)
data = r.json()
assert data["status"] == "executed", data["reason_code"]
print(data["result"])Idempotency-Key. If your agent retries, we de-duplicate using (agent_id, key) and return the cached response — no double charges.Operations reference
40+ operations across 21 service families. Prices below are the fixed basePriceCents from src/db/seedData.ts; variable-priced operations are flagged. All amounts are debited from your org wallet after the provider returns.
Communication
| operation | payload shape | default price |
|---|---|---|
| email.send | { to, subject, body } | $0.01 |
| email.read | { folder?, query?, limit? } | $0.01 |
| email.search | { query, limit? } | $0.01 |
| email.reply | { message_id, body } | $0.01 |
| email.draft | { to, subject, body } | $0.01 |
| sms.send | { to, body } | $0.01 |
| sms.read | { limit? } | $0.01 |
| mms.send | { to, body, media_urls[] } | $0.02 |
| voice_call.create | { to, from, twiml_url } | $0.25 |
| chat.send | { channel, text } | $0.01 |
| chat.read | { channel, limit? } | $0.01 |
| chat.dm | { user, text } | $0.01 |
| chat.thread_reply | { channel, thread_ts, text } | $0.01 |
| chat.upload_file | { channel, file_url, title? } | $0.01 |
AI & LLM
| operation | payload shape | default price |
|---|---|---|
| text_generation.generate | { model, messages[] } | varies by model |
| text_generation.extract_structured | { model, messages[], schema } | varies by model |
| speech.transcribe | { audio_url, language? } | $0.01 |
| speech.synthesize | { text, voice? } | $0.01 |
| embedding.create | { model, input } | $0.01 |
Web & data
| operation | payload shape | default price |
|---|---|---|
| web_search.search | { query } | $0.01 |
| web_search.news | { query, days? } | $0.01 |
| web.fetch_url | { url, max_bytes? } | $0.01 |
| browser.run | { url, instructions } | $0.10 |
| browser.screenshot | { url, viewport? } | $0.02 |
| browser.extract | { url, schema } | $0.03 |
| knowledge.query | { collection, query, top_k? } | $0.01 |
| knowledge.upsert | { collection, documents[] } | $0.05 |
| knowledge.delete | { collection, ids[] } | $0.01 |
| knowledge.list_collections | {} | $0.01 |
| data.csv_parse | { url | content, options? } | $0.01 |
Compute & primitives
| operation | payload shape | default price |
|---|---|---|
| code.execute | { language, source, stdin? } | $0.02 |
| notification.webhook | { url, body, headers? } | $0.01 |
Media & docs
| operation | payload shape | default price |
|---|---|---|
| document.parse | { url, options? } | $0.01 |
| video.subtitle | { video_url, language? } | $0.01 |
Integrations (OAuth)
| operation | payload shape | default price |
|---|---|---|
| calendar.read | { start, end, calendar_id? } | $0.01 |
| calendar.create | { title, start, end, attendees? } | $0.01 |
| calendar.update | { event_id, patch } | $0.01 |
| calendar.cancel | { event_id } | $0.01 |
| scheduling.send_invite | { to[], title, start, end } | $0.01 |
| crm.read | { object, id | query } | $0.01 |
| crm.write | { object, patch } | $0.01 |
| ticket.create | { project, title, body } | $0.01 |
| ticket.update | { ticket_id, patch } | $0.01 |
Variable-priced operations (text_generation.generate and text_generation.extract_structured) use per-model rates in the PricingTier table. The actual cost is debited after the provider returns token counts. OAuth integration operations additionally require an installed ConnectedApp at /connected-apps.
Approvals
If a request's estimated cost crosses an agent'sapproval_threshold_cents, you'll get this back:
{
"status": "approval_required",
"request_id": "req_01HQ...",
"estimated_cost_cents": 800,
"approval_id": "apr_01HQ..."
}Approve or reject at /approvals, or hit the API:
# Approve resumes the original pipeline. Reject marks it blocked. curl -X POST https://www.upivia.com/v1/approvals/apr_01HQ.../approve curl -X POST https://www.upivia.com/v1/approvals/apr_01HQ.../reject
Approving re-runs the pipeline. Rejecting marks the requestblocked. Pending approvals expire after 24 hours.
Error codes
| status | reason_code | meaning |
|---|---|---|
| blocked | policy_violation | Agent has no binding for this operation. |
| blocked | monthly_budget_exceeded | Per-op monthly cap hit. |
| blocked | daily_limit_exceeded | Per-op daily request count hit. |
| blocked | insufficient_balance | Org wallet is empty or wouldn't cover the call. |
| approval_required | approval_threshold | Cost exceeds threshold; awaiting approval. |
| failed | adapter_error | Upstream provider rejected the call. See result.detail. |
| failed | agent_disabled | Agent.status is paused or disabled. |
Webhooks
The platform consumes webhooks from our billing provider (for wallet top-ups) and from the voice provider (for call status reconciliation). Outbound webhooks to your systems — e.g. "this request executed", "budget hit" — are on the roadmap but not shipped yet.
/v1/audit-logs — every state transition is recorded there.