WA template

This commit is contained in:
debu-2 2026-10-02 21:59:13 +05:30
parent 7a2e21ba61
commit d272ae0bd3
12 changed files with 1359 additions and 5 deletions

View file

@ -21,6 +21,8 @@ import { AnalyticsPage } from './features/analytics/AnalyticsPage'
import { WhatsAppSetupPage } from './features/whatsapp/WhatsAppSetupPage'
import { UrmiOtpPage } from './features/agent/UrmiOtpPage'
import { UrmiOtpDocsPage } from './features/agent/UrmiOtpDocsPage'
import { AgentTemplatesPage } from './features/agent/AgentTemplatesPage'
import { AgentTemplateDocsPage } from './features/agent/AgentTemplateDocsPage'
import { KycPage } from './features/compliance/KycPage'
import { DltPage } from './features/compliance/DltPage'
import { ComplianceGate } from './features/compliance/ComplianceProvider'
@ -68,6 +70,8 @@ export default function App() {
<Route path="numbers/buy" element={<BuyNumbersPage />} />
<Route path="numbers" element={<MyNumbersPage />} />
<Route path="voice" element={<VoicePage />} />
<Route path="agent/urmiai" element={<AgentTemplatesPage />} />
<Route path="agent/urmiai/templates/:slug/docs" element={<AgentTemplateDocsPage />} />
<Route path="agent/urmiotp" element={<UrmiOtpPage />} />
<Route path="agent/urmiotp/projects/:projectId/docs" element={<UrmiOtpDocsPage />} />
<Route path=":section/*" element={<Placeholder />} />

View file

@ -0,0 +1,91 @@
import { ArrowLeft, Copy } from 'lucide-react'
import { useEffect, useState } from 'react'
import { Link, useNavigate, useParams } from 'react-router-dom'
import { agentTemplatesApi, type AgentTemplateDocumentation } from '../../lib/api'
import { API_BASE_URL } from '../../lib/apiBase'
export function AgentTemplateDocsPage() {
const { slug = '' } = useParams()
const navigate = useNavigate()
const [docs, setDocs] = useState<AgentTemplateDocumentation | null>(null)
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [copied, setCopied] = useState(false)
useEffect(() => {
agentTemplatesApi.documentation(slug)
.then(setDocs)
.catch((reason) => setError(reason instanceof Error ? reason.message : 'Unable to load documentation'))
.finally(() => setLoading(false))
}, [slug])
if (loading) return <div className="auth-loading">Loading API documentation…</div>
if (!docs) return <section className="panel"><p className="auth-error">{error}</p></section>
const requestExample = JSON.stringify(docs.api.example, null, 2)
const curlExample = [
`curl -X POST '${API_BASE_URL}${docs.api.path}' \\`,
` -H 'Authorization: Bearer urmi_live_<your_api_key>' \\`,
` -H 'Content-Type: application/json' \\`,
` -d '${requestExample.replace(/'/g, `'\\''`)}'`,
].join('\n')
async function copyExample() {
await navigator.clipboard.writeText(requestExample)
setCopied(true)
window.setTimeout(() => setCopied(false), 2000)
}
return (
<div className="urmiotp-docs-page">
<button className="text-button otp-back" onClick={() => navigate('/console/agent/urmiai')}>
<ArrowLeft size={14} /> Back to templates
</button>
<div className="account-heading">
<span className="eyebrow">UrmiAI · {docs.template.slug}</span>
<h1>{docs.template.name}</h1>
<p>{docs.template.description}</p>
</div>
<section className="panel otp-api-overview">
<div className="panel-heading">
<div><h2>Send agent WhatsApp message</h2><p>{docs.api.summary}</p></div>
<span className="explorer-method">{docs.api.method}</span>
</div>
<div className="explorer-field-row"><span>Endpoint</span><code>{API_BASE_URL}{docs.api.path}</code></div>
<div className="explorer-field-row">
<span>Authentication</span>
<code>{docs.api.authentication.header}: {docs.api.authentication.format}</code>
</div>
<div className="otp-parameter-list">
{Object.entries(docs.api.parameters).map(([name, description]) => (
<div key={name}><code>{name}</code><span>{description}</span></div>
))}
</div>
<div className="otp-code-heading">
<strong>Request body</strong>
<button className="secondary-button" onClick={copyExample}><Copy size={13} /> {copied ? 'Copied' : 'Copy'}</button>
</div>
<pre className="explorer-code">{requestExample}</pre>
<div className="otp-code-heading"><strong>Example cURL</strong></div>
<pre className="explorer-code">{curlExample}</pre>
</section>
<section className="panel agent-template-docs-preview">
<div className="panel-heading">
<div><h2>Template preview</h2><p>Approved Meta template relayed by Urmi.</p></div>
</div>
<div className="agent-template-preview card">
{docs.template.header_text && <strong>{docs.template.header_text}</strong>}
<p>{docs.template.body_preview}</p>
{docs.template.footer_text && <small>{docs.template.footer_text}</small>}
{docs.template.button_label && <span className="agent-template-button-chip">{docs.template.button_label}</span>}
</div>
<ul className="agent-template-notes">
{docs.notes.map((note) => <li key={note}>{note}</li>)}
</ul>
<Link to="/console/api-explorer" className="secondary-button">Try in API Explorer</Link>
</section>
</div>
)
}

View file

@ -0,0 +1,248 @@
import { ArrowRight, Bot, Code2, Play, X } from 'lucide-react'
import { useEffect, useState, type FormEvent } from 'react'
import { Link, useNavigate } from 'react-router-dom'
import { agentTemplatesApi, type AgentMessageTemplate } from '../../lib/api'
function TestModal({
template,
onClose,
onSent,
}: {
template: AgentMessageTemplate
onClose: () => void
onSent: () => void
}) {
const [to, setTo] = useState('')
const [customerName, setCustomerName] = useState('')
const [reportId, setReportId] = useState('')
const [error, setError] = useState('')
const [sending, setSending] = useState(false)
async function submit(event: FormEvent) {
event.preventDefault()
setError('')
setSending(true)
try {
await agentTemplatesApi.test(template.slug, {
to: to.trim(),
customer_name: customerName.trim(),
report_id: reportId.trim() || undefined,
})
onSent()
onClose()
} catch (reason) {
setError(reason instanceof Error ? reason.message : 'Unable to send test message')
} finally {
setSending(false)
}
}
return (
<div className="modal-overlay" onClick={onClose}>
<form className="modal-card otp-project-modal" onSubmit={submit} onClick={(event) => event.stopPropagation()}>
<div className="modal-header">
<h2>Test {template.name}</h2>
<button type="button" className="icon-button" onClick={onClose} aria-label="Close"><X size={18} /></button>
</div>
<p className="agent-template-test-note">
One console test per day. Urmi sends from the Urmi AI WhatsApp business number on your behalf.
</p>
<div className="otp-project-form">
<label>
<span>Recipient WhatsApp number</span>
<input value={to} onChange={(e) => setTo(e.target.value)} placeholder="919149154337" required />
</label>
<label>
<span>Customer name</span>
<input value={customerName} onChange={(e) => setCustomerName(e.target.value)} placeholder="Suyash" required />
</label>
<label>
<span>Report ID (optional)</span>
<input value={reportId} onChange={(e) => setReportId(e.target.value)} placeholder="run-abc123" />
<small>Used for the dynamic View Report button URL suffix.</small>
</label>
{error && <p className="auth-error">{error}</p>}
</div>
<div className="modal-actions">
<button className="primary-button" disabled={sending}>{sending ? 'Sending…' : 'Send test'}</button>
<button type="button" className="secondary-button" onClick={onClose}>Cancel</button>
</div>
</form>
</div>
)
}
export function AgentTemplatesPage() {
const navigate = useNavigate()
const [templates, setTemplates] = useState<AgentMessageTemplate[]>([])
const [testStatus, setTestStatus] = useState<Record<string, { allowed: boolean; retry_after?: string | null }>>({})
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [testTemplate, setTestTemplate] = useState<AgentMessageTemplate | null>(null)
const [actionError, setActionError] = useState('')
async function load() {
const items = await agentTemplatesApi.list()
setTemplates(items)
const statuses = await Promise.all(
items
.filter((item) => item.subscribed)
.map(async (item) => {
try {
const status = await agentTemplatesApi.testStatus(item.slug)
return [item.slug, status] as const
} catch {
return [item.slug, { allowed: true }] as const
}
}),
)
setTestStatus(Object.fromEntries(statuses))
}
useEffect(() => {
load()
.catch(() => setError('Unable to load agent templates'))
.finally(() => setLoading(false))
}, [])
async function toggleSubscribe(template: AgentMessageTemplate) {
setActionError('')
try {
if (template.subscribed) {
await agentTemplatesApi.unsubscribe(template.slug)
} else {
await agentTemplatesApi.subscribe(template.slug)
}
await load()
} catch (reason) {
setActionError(reason instanceof Error ? reason.message : 'Unable to update subscription')
}
}
if (loading) return <div className="auth-loading">Loading agent templates…</div>
return (
<div className="agent-templates-page">
<div className="account-heading">
<span className="eyebrow">Agent · WhatsApp</span>
<h1><Bot size={24} /> UrmiAI agent messages</h1>
<p>
Subscribe to approved WhatsApp agent templates. Urmi authenticates your API key and sends messages
from the Urmi AI business number on behalf of your agents.
</p>
</div>
<section className="panel agent-templates-panel">
<div className="panel-heading">
<div>
<h2>Available templates</h2>
<p>Subscribe to enable API access and console testing.</p>
</div>
<Code2 size={19} />
</div>
{templates.length === 0 ? (
<div className="otp-empty">
<strong>No agent templates yet</strong>
<span>Templates will appear here as Urmi adds them to the platform catalog.</span>
</div>
) : (
<div className="agent-template-table">
<div className="agent-template-table-head">
<span>Template</span>
<span>Preview</span>
<span>Parameters</span>
<span>Status</span>
<span />
</div>
{templates.map((template) => {
const status = testStatus[template.slug]
const canTest = template.subscribed && (status?.allowed ?? true)
return (
<div className="agent-template-row" key={template.id}>
<div>
<strong>{template.name}</strong>
<small><code>{template.slug}</code> · Meta: {template.meta_template_name}</small>
<small>{template.description}</small>
</div>
<div className="agent-template-preview">
{template.header_text && <strong>{template.header_text}</strong>}
<p>{template.body_preview}</p>
{template.footer_text && <small>{template.footer_text}</small>}
{template.button_label && <span className="agent-template-button-chip">{template.button_label}</span>}
</div>
<div className="agent-template-params">
{template.parameters.map((param) => (
<div key={param.name}>
<code>{param.name}</code>
<span>{param.description}</span>
</div>
))}
</div>
<div>
<span className={`doc-status ${template.subscribed ? 'approved' : 'pending'}`}>
{template.subscribed ? 'Subscribed' : 'Not subscribed'}
</span>
{template.subscribed && status && !status.allowed && status.retry_after && (
<small>Test again after {new Date(status.retry_after).toLocaleString()}</small>
)}
</div>
<div className="agent-template-actions">
<button
className={template.subscribed ? 'secondary-button' : 'primary-button'}
onClick={() => toggleSubscribe(template)}
>
{template.subscribed ? 'Unsubscribe' : 'Subscribe'}
</button>
{template.subscribed && (
<>
<button
className="secondary-button"
disabled={!canTest}
onClick={() => setTestTemplate(template)}
>
<Play size={14} /> Test API
</button>
<button
className="secondary-button"
onClick={() => navigate(`/console/agent/urmiai/templates/${template.slug}/docs`)}
>
View docs <ArrowRight size={14} />
</button>
</>
)}
</div>
</div>
)
})}
</div>
)}
{(error || actionError) && <p className="auth-error">{error || actionError}</p>}
</section>
<section className="panel agent-templates-help">
<div className="panel-heading">
<div>
<h2>How it works</h2>
<p>Urmi acts as your WhatsApp tech provider middle layer.</p>
</div>
</div>
<ol className="relay-steps">
<li>Subscribe to a template in this console.</li>
<li>Create an API key under Develop → API Keys.</li>
<li>Call <code>POST /api/v1/agent/messages/send</code> with your Urmi API key.</li>
<li>Urmi validates your subscription and relays the message to Meta WhatsApp Cloud API.</li>
</ol>
<Link to="/console/api-explorer" className="secondary-button">Open API Explorer</Link>
</section>
{testTemplate && (
<TestModal
template={testTemplate}
onClose={() => setTestTemplate(null)}
onSent={() => load().catch(() => undefined)}
/>
)}
</div>
)
}

View file

@ -2,10 +2,13 @@ import { CheckCircle2, Copy, MessageSquareText, Phone, Play, Radio, ShieldCheck
import { useEffect, useMemo, useState, type ReactNode } from 'react'
import { Link } from 'react-router-dom'
import {
agentMessagesApi,
integrationsApi,
messagesApi,
numbersApi,
voiceApi,
type AgentMessageApiDoc,
type AgentMessageSendResponse,
type IntegrationsDocs,
type MessageApiDoc,
type PhoneNumber,
@ -116,6 +119,7 @@ function normalizeIntegrationDocs(raw: Partial<IntegrationsDocs> | null): Integr
export function ApiExplorerPage() {
const [smsDoc, setSmsDoc] = useState<MessageApiDoc | null>(null)
const [agentDoc, setAgentDoc] = useState<AgentMessageApiDoc | null>(null)
const [voiceDoc, setVoiceDoc] = useState<VoiceApiDocs | null>(null)
const [integrationDocs, setIntegrationDocs] = useState<IntegrationsDocs | null>(null)
const [numbers, setNumbers] = useState<PhoneNumber[]>([])
@ -130,25 +134,38 @@ export function ApiExplorerPage() {
const [smsError, setSmsError] = useState('')
const [smsRunning, setSmsRunning] = useState(false)
const [agentTemplate, setAgentTemplate] = useState('')
const [agentTo, setAgentTo] = useState('')
const [agentCustomerName, setAgentCustomerName] = useState('')
const [agentReportId, setAgentReportId] = useState('')
const [agentResponse, setAgentResponse] = useState<AgentMessageSendResponse | null>(null)
const [agentError, setAgentError] = useState('')
const [agentRunning, setAgentRunning] = useState(false)
const [fromDid, setFromDid] = useState('')
const [callTo, setCallTo] = useState('')
const [voiceResponse, setVoiceResponse] = useState<VoiceCall | null>(null)
const [voiceError, setVoiceError] = useState('')
const [voiceRunning, setVoiceRunning] = useState(false)
const [copied, setCopied] = useState<'sms' | 'voice' | null>(null)
const [copied, setCopied] = useState<'sms' | 'voice' | 'agent' | null>(null)
const [loadError, setLoadError] = useState('')
useEffect(() => {
Promise.allSettled([
messagesApi.docs(),
agentMessagesApi.docs(),
voiceApi.docs(),
integrationsApi.docs(),
numbersApi.mine(),
])
.then((results) => {
const [smsResult, voiceResult, integrationResult, numbersResult] = results
const [smsResult, agentResult, voiceResult, integrationResult, numbersResult] = results
if (smsResult.status === 'fulfilled') setSmsDoc(smsResult.value)
if (agentResult.status === 'fulfilled') {
setAgentDoc(agentResult.value)
setAgentTemplate(agentResult.value.request_body.template?.example ?? 'agent_run_completion')
}
if (voiceResult.status === 'fulfilled') setVoiceDoc(normalizeVoiceDocs(voiceResult.value))
if (integrationResult.status === 'fulfilled') {
setIntegrationDocs(normalizeIntegrationDocs(integrationResult.value))
@ -183,6 +200,22 @@ export function ApiExplorerPage() {
].join('\n')
}, [apiKey, message, senderId, smsDoc, smsTo, templateId])
const agentCurl = useMemo(() => {
if (!agentDoc) return ''
const body = JSON.stringify({
template: agentTemplate || agentDoc.request_body.template.example,
to: agentTo || agentDoc.request_body.to.example,
customer_name: agentCustomerName || agentDoc.request_body.customer_name.example,
report_id: agentReportId || agentDoc.request_body.report_id.example,
}, null, 2)
return [
`curl -X POST '${API_BASE_URL}${agentDoc.path}' \\`,
` -H 'Authorization: Bearer ${apiKey || 'urmi_live_<your_api_key>'}' \\`,
` -H 'Content-Type: application/json' \\`,
` -d '${body.replace(/'/g, `'\\''`)}'`,
].join('\n')
}, [agentCustomerName, agentDoc, agentReportId, agentTemplate, agentTo, apiKey])
const voiceCurl = useMemo(() => {
if (!voiceDoc) return ''
const body = JSON.stringify({
@ -217,6 +250,25 @@ export function ApiExplorerPage() {
}
}
async function runAgentMessage() {
setAgentError('')
setAgentResponse(null)
setAgentRunning(true)
try {
const result = await agentMessagesApi.send(apiKey.trim(), {
template: agentTemplate.trim(),
to: agentTo.trim(),
customer_name: agentCustomerName.trim(),
report_id: agentReportId.trim() || undefined,
})
setAgentResponse(result)
} catch (reason) {
setAgentError(reason instanceof Error ? reason.message : 'Request failed')
} finally {
setAgentRunning(false)
}
}
async function runVoiceCall() {
setVoiceError('')
setVoiceResponse(null)
@ -235,8 +287,9 @@ export function ApiExplorerPage() {
}
}
async function copyCurl(kind: 'sms' | 'voice') {
await navigator.clipboard.writeText(kind === 'sms' ? smsCurl : voiceCurl)
async function copyCurl(kind: 'sms' | 'voice' | 'agent') {
const text = kind === 'sms' ? smsCurl : kind === 'agent' ? agentCurl : voiceCurl
await navigator.clipboard.writeText(text)
setCopied(kind)
window.setTimeout(() => setCopied(null), 2000)
}
@ -248,7 +301,7 @@ export function ApiExplorerPage() {
</div>
)
}
if (!smsDoc && !voiceDoc && !integrationDocs) {
if (!smsDoc && !agentDoc && !voiceDoc && !integrationDocs) {
return (
<div className="api-explorer-page">
<div className="account-heading">
@ -345,6 +398,83 @@ export function ApiExplorerPage() {
</ExplorerGroup>
)}
{agentDoc && (
<ExplorerGroup title="Agent messages">
<section className="panel explorer-endpoint-card">
<div className="panel-heading">
<div>
<h3>{agentDoc.title}</h3>
<p>{agentDoc.summary}</p>
</div>
<span className="explorer-method">{agentDoc.method}</span>
</div>
<FieldRow label="Endpoint" value={`${API_BASE_URL}${agentDoc.path}`} />
<FieldRow label="Auth" value={`${agentDoc.authentication.header}: ${agentDoc.authentication.format}`} />
<div className="explorer-section">
<h4>Request body</h4>
<div className="explorer-request-grid">
{Object.entries(agentDoc.request_body).map(([key, field]) => (
<div className="explorer-input" key={key}>
<label>
<strong>{key}</strong>
<span>{field.description}</span>
<input
value={
key === 'template' ? agentTemplate
: key === 'to' ? agentTo
: key === 'customer_name' ? agentCustomerName
: agentReportId
}
onChange={(e) => {
if (key === 'template') setAgentTemplate(e.target.value)
if (key === 'to') setAgentTo(e.target.value)
if (key === 'customer_name') setAgentCustomerName(e.target.value)
if (key === 'report_id') setAgentReportId(e.target.value)
}}
placeholder={field.example}
/>
</label>
</div>
))}
</div>
</div>
<div className="explorer-section">
<h4>API key</h4>
<label className="explorer-input">
<strong>Bearer token</strong>
<span>Subscribe to the template in Agent → UrmiAI before testing.</span>
<input type="password" value={apiKey} onChange={(e) => setApiKey(e.target.value)} placeholder="urmi_live_..." />
</label>
</div>
<div className="explorer-actions">
<button className="primary-button" onClick={runAgentMessage} disabled={agentRunning || !apiKey.trim()}>
<Play size={15} /> {agentRunning ? 'Sending…' : 'Try request'}
</button>
<button className="secondary-button" onClick={() => copyCurl('agent')}>
<Copy size={15} /> {copied === 'agent' ? 'Copied cURL' : 'Copy cURL'}
</button>
</div>
{agentError && <p className="auth-error">{agentError}</p>}
{agentResponse && (
<div className="explorer-response success">
<strong><CheckCircle2 size={15} /> 200 OK</strong>
<pre>{JSON.stringify(agentResponse, null, 2)}</pre>
</div>
)}
<div className="explorer-section">
<h4>Example cURL</h4>
<pre className="explorer-code">{agentCurl}</pre>
</div>
</section>
</ExplorerGroup>
)}
{voiceDoc && integrationDocs && (
<ExplorerGroup title="Numbers">
<RelayCallout principle={voiceDoc.relay.principle} latency={voiceDoc.relay.latency} />

View file

@ -1180,6 +1180,85 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
.otp-empty { display: grid; justify-items: center; gap: 7px; padding: 42px 20px 25px; color: #8a95a7; text-align: center; }
.otp-empty strong { color: #46546a; font-size: 12px; }
.otp-empty span { font-size: 10px; }
.agent-templates-page { max-width: 1180px; }
.agent-template-table { margin-top: 18px; }
.agent-template-table-head,
.agent-template-row {
display: grid;
grid-template-columns: minmax(180px, 1.1fr) minmax(220px, 1.2fr) minmax(180px, .9fr) minmax(120px, .7fr) auto;
gap: 14px;
align-items: start;
}
.agent-template-table-head {
padding: 0 10px 9px;
color: #8a95a7;
font-size: 9px;
font-weight: 700;
text-transform: uppercase;
}
.agent-template-row {
padding: 14px 10px;
border-top: 1px solid #edf0f4;
}
.agent-template-row strong,
.agent-template-row small,
.agent-template-row p {
display: block;
}
.agent-template-row strong { color: #35435a; font-size: 12px; }
.agent-template-row small {
margin-top: 4px;
color: #8a95a7;
font-size: 9px;
line-height: 1.4;
}
.agent-template-preview p {
margin: 6px 0;
color: #46546a;
font-size: 11px;
line-height: 1.45;
}
.agent-template-params div {
display: grid;
gap: 4px;
margin-bottom: 8px;
}
.agent-template-params code {
color: #1475ff;
font-size: 10px;
}
.agent-template-params span {
color: #8a95a7;
font-size: 9px;
line-height: 1.4;
}
.agent-template-actions {
display: flex;
flex-direction: column;
gap: 8px;
align-items: stretch;
}
.agent-template-button-chip {
display: inline-flex;
margin-top: 8px;
padding: 6px 10px;
border: 1px solid #dfe5ed;
border-radius: 999px;
color: #1475ff;
font-size: 10px;
font-weight: 600;
}
.agent-template-test-note,
.agent-template-notes {
color: #607089;
font-size: 11px;
line-height: 1.5;
}
.agent-template-notes {
margin: 12px 0 0;
padding-left: 18px;
}
.agent-templates-help { margin-top: 18px; }
.otp-project-table { margin-top: 18px; }
.otp-project-table-head, .otp-project-row { display: grid; grid-template-columns: minmax(180px, 1.3fr) minmax(140px, .8fr) minmax(120px, .7fr) auto; gap: 14px; align-items: center; }
.otp-project-table-head { padding: 0 10px 9px; color: #8a95a7; font-size: 9px; font-weight: 700; text-transform: uppercase; }
@ -1261,6 +1340,9 @@ main { padding: 30px clamp(20px, 3vw, 44px) 50px; }
.api-key-row { grid-template-columns: 1fr; }
.otp-map-branches { grid-template-columns: repeat(4, 1fr); }
.otp-map-branches::before, .otp-map-branches > div::before { display: none; }
.agent-template-table-head { display: none; }
.agent-template-row { grid-template-columns: 1fr; align-items: start; }
.agent-template-actions { flex-wrap: wrap; }
.otp-project-table-head { display: none; }
.otp-project-row { grid-template-columns: 1fr; align-items: start; }
.templates-table-head { display: none; }

View file

@ -420,6 +420,126 @@ export type AdminUrmiOtpProject = UrmiOtpProject & {
user: { id: string; name: string; email: string }
}
export type AgentTemplateParameter = {
name: string
type: string
required: boolean
description: string
example: string
}
export type AgentMessageTemplate = {
id: string
slug: string
name: string
description: string
header_text: string | null
body_preview: string
footer_text: string | null
meta_template_name: string
language: string
category: string
parameters: AgentTemplateParameter[]
button_label: string | null
button_url_base: string | null
is_active: boolean
subscribed: boolean
}
export type AgentTemplateDocumentation = {
template: AgentMessageTemplate
api: {
method: string
path: string
summary: string
authentication: {
type: string
header: string
format: string
alternate_header?: string
}
parameters: Record<string, string>
example: Record<string, string>
response_example: Record<string, unknown>
}
notes: string[]
}
export type AgentMessageApiDoc = {
id: string
method: string
path: string
title: string
summary: string
authentication: {
type: string
header: string
format: string
alternate_header?: string
}
request_body: Record<string, {
type: string
required: boolean
description: string
example: string
}>
responses: Record<string, { description: string; example?: Record<string, unknown> }>
notes?: string[]
available_templates?: Array<{ slug: string; name: string; meta_template_name: string }>
}
export type AgentMessageSendResponse = {
success: boolean
template: string
to: string
customer_name: string
report_id: string
report_url: string | null
wa_message_id: string | null
dispatch_id: string
}
export const agentTemplatesApi = {
list: () => jsonRequest<AgentMessageTemplate[]>('/api/v1/agent-templates'),
subscribe: (slug: string) =>
jsonRequest<AgentMessageTemplate>(`/api/v1/agent-templates/${slug}/subscribe`, { method: 'POST' }),
unsubscribe: (slug: string) =>
jsonRequest<AgentMessageTemplate>(`/api/v1/agent-templates/${slug}/subscribe`, { method: 'DELETE' }),
documentation: (slug: string) =>
jsonRequest<AgentTemplateDocumentation>(`/api/v1/agent-templates/${slug}/docs`),
testStatus: (slug: string) =>
jsonRequest<{ template: string; allowed: boolean; retry_after?: string | null; last_tested_at?: string | null }>(
`/api/v1/agent-templates/${slug}/test-status`,
),
test: (slug: string, payload: { to: string; customer_name: string; report_id?: string }) =>
jsonRequest<AgentMessageSendResponse>(`/api/v1/agent-templates/${slug}/test`, {
method: 'POST',
body: JSON.stringify(payload),
}),
}
export const agentMessagesApi = {
docs: () => jsonRequest<AgentMessageApiDoc>('/api/v1/agent/messages/docs/send'),
send: (
apiKey: string,
payload: { template: string; to: string; customer_name: string; report_id?: string },
) =>
fetch(`${API_BASE_URL}/api/v1/agent/messages/send`, {
method: 'POST',
headers: {
Authorization: `Bearer ${apiKey}`,
'Content-Type': 'application/json',
},
body: JSON.stringify(payload),
}).then(async (response) => {
if (!response.ok) {
const body = await response.json().catch(() => null)
throw new Error(typeof body?.detail === 'string' ? body.detail : 'Request failed')
}
return response.json() as Promise<AgentMessageSendResponse>
}),
}
export const urmiOtpApi = {
listProjects: () => jsonRequest<UrmiOtpProject[]>('/api/v1/urmiotp/projects'),
createProject: (payload: {