Announcing CIMD support for MCP Client registration
Learn more

How to Build a Twilio Messaging Monitoring Agent with Google ADK

TL;DR

  • On Twilio, tenant attribution is not a query parameter; it is a property of which credential signs the request. Master account credentials read usage for the master account and every subaccount beneath it, and IncludeSubaccounts defaults to true, so a monitoring agent holding master credentials returns one blended number and cannot tell you which customer caused a spike.
  • The Scalekit Twilio connector exposes 31 tools, 7 of which mutate or delete Twilio records (twilio_message_delete, twilio_recording_delete, twilio_call_delete, and four others). A read-only usage monitor needs 6. Handing the agent the full catalog gives an observability job the ability to destroy the message logs it is observing.
  • Twilio's own fraud-response guide classifies Account Takeover, defined as a threat actor obtaining your API keys or Auth Tokens, as one of four primary fraud categories. An agent that holds a master Auth Token in its runtime to detect fraud is the highest-value takeover target on the account.
  • The base /Usage/Records resource returns a single aggregated record per category for the entire date range, not a daily series. Anomaly detection that assumes otherwise computes a z-score against a population of one. The baseline has to come from the Daily subresource, and it belongs in deterministic code, not in the model.
  • Scalekit binds one connected account per Twilio subaccount, injects that subaccount's credentials at call time, and freezes the resolved connected_account_id into each Google ADK tool object at construction. The tenant boundary is decided before the model runs and the model has no parameter with which to cross it.

Your Twilio spend for Tuesday lands at 9.4x the trailing median. Verify volume is up, delivery rates look normal, and the destination mix has shifted toward number ranges you have never served.

Three questions follow, in order. Which customer? Is it a launch or an attack? How fast can you cut it off?

The monitoring script written to answer the first question has the master Account SID and Auth Token in its environment, because that is what the Twilio Console hands you and it works on the first try. It pulls /Usage/Records and reports one number for the whole account. The number is correct. It is also useless, because IncludeSubaccounts defaults to true and every one of your 40 tenants is folded into that single row. To attribute the spike you iterate subaccounts with the same master credential, which means the process that answers "which customer" holds a credential that reads all of them, and holds it in a runtime you are about to hand to an LLM.

That is the actual engineering problem. Not the anomaly math. The math is a page of NumPy. The hard part is that on Twilio, per-tenant attribution and per-tenant authorization are the same mechanism, and if you get the credential model wrong the agent cannot answer the question at all.

What the Twilio connector exposes for usage monitoring

The Twilio connector authenticates with Basic Auth: an Account SID and an Auth Token registered once per environment. Six of its tools carry a messaging usage monitor.

Tool
Parameters
What it answers
twilio_usage_records_today
category
Current-day usage and price, per category
twilio_usage_records_list
category, start_date, end_date, page_size
Aggregated usage across a date window
twilio_messages_list
date_sent, from_number, to, page_size (max 1000)
Message-level destinations for pattern analysis
twilio_messaging_services_list
page_size
Which Messaging Service the traffic routed through
twilio_phone_numbers_list
friendly_name, phone_number, page_size
Which sender numbers exist on this account
twilio_account_get
none
Account SID, friendly name, status

Three constraints matter before you write a line of code.

  • There is no send-message tool in the catalog. The connector reads, gets, lists, and deletes messages; it does not create them. Alerts leave through a different connector, which is the correct design anyway: a messaging-cost monitor that can send messages can amplify the incident it is reporting.
  • include_subaccounts is not an exposed parameter. Twilio's REST API accepts it and defaults it to true. The tool does not surface it, so you cannot narrow a master-credential call to a single account through the tool interface. The scoping has to happen at the credential.
  • Seven tools mutate. twilio_message_delete, twilio_call_delete, twilio_recording_delete, twilio_conversation_delete, twilio_conversation_message_delete, twilio_verify_service_delete, and twilio_verify_service_create. Twilio's message-delete tool is documented as permanent and unrecoverable.

Why the master Auth Token is the wrong credential for this agent

Twilio's subaccount model is explicit about what each credential reaches. Master account credentials access v2010 API resources for the master account and for any subaccount. Subaccount credentials cannot access the master account or any sibling subaccount. That asymmetry is the entire authorization surface, and it is enforced by Twilio, not by your code.

For a multi-tenant B2B product that provisions one Twilio subaccount per customer, which is the pattern Twilio documents for segmenting usage and billing, the consequence is direct.

Property
Master Account SID + Auth Token
Per-subaccount connected account
Reads tenant A's messages
Yes
Only if the account is tenant A
Reads tenant B's messages during tenant A's run
Yes
No; Twilio rejects it
Usage attribution
Blended across all subaccounts by default
Naturally scoped to one tenant
Blast radius if the runtime is compromised
Every tenant, plus master-level writes
One tenant, read-only tool surface
Enforcement point
Your code, if you remember
Twilio's authorization layer

The compromise case is not hypothetical for this workload specifically. Twilio's fraud-response guide lists four common fraud patterns, and one of them is Account Takeover: a threat actor steals or guesses your credentials, API keys, or Auth Tokens and gains control of the account. The agent you are building exists to catch SMS pumping. If it holds a master Auth Token in its process environment, in an LLM context window, or in a tool-call trace, you have created the takeover vector while building the takeover detector.

Scalekit's position on this is the token vault boundary: credentials sit encrypted at rest and are injected into the outbound HTTP call by Scalekit. The agent calls a tool, gets a result, and never sees a token. Combined with per-subaccount connected accounts, the credential the agent cannot see is also a credential that could only ever reach one tenant. This is the same argument as why admin accounts are the wrong default for AI agents, sharpened by the fact that Twilio gives you a real, provider-enforced boundary to use.

One clarification worth making explicit, because it changes what you store where: the Account SID is an identifier, not a secret. The Auth Token is the credential. Your tenant registry can hold subaccount SIDs in plain application state; only the Auth Token belongs in the vault.

Binding tenants to connected accounts

One environment-level Twilio connection holds the connector configuration. One connected account per tenant holds that tenant's subaccount credentials. The identifier is the tenant boundary.

# provision.py # Run once per tenant, from your provisioning path. Not from the agent. import os from scalekit import ScalekitClient scalekit = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) # The connection name is the key ID shown in the Scalekit dashboard after you # create the Twilio connection. It looks like "twilio-b1fqL2Dr", not "TWILIO". # Copying the literal string "TWILIO" is the single most common setup failure. TWILIO_CONNECTION = os.environ["SCALEKIT_TWILIO_CONNECTION_NAME"] def provision_tenant(tenant_id: str, org_id: str, subaccount_sid: str, subaccount_token: str): """Bind one Twilio subaccount to one Scalekit connected account. After this call, every tool executed with identifier=tenant_id is signed with THIS subaccount's credentials. Twilio then refuses any request that reaches the master account or a sibling subaccount, so the tenant boundary is enforced by the provider rather than by application logic. """ return scalekit.actions.create_connected_account( connection_name=TWILIO_CONNECTION, identifier=tenant_id, # the tenant boundary the agent runs under organization_id=org_id, # your tenant ID, for filtering and audit authorization_details={ # Basic Auth connectors use "static_auth". The SDK serializes this dict # directly into a protobuf Struct, so the keys must be the connector's # auth-pattern FIELD NAMES, which are not the labels rendered in the # dashboard. Scalekit's BASIC pattern collects username and password; # Twilio's HTTP Basic scheme puts the Account SID in the username # position and the Auth Token in the password position, which is what # the "Account SID" and "Auth Token" labels sit on top of. "static_auth": { "username": subaccount_sid, # dashboard label: Account SID "password": subaccount_token, # dashboard label: Auth Token } }, )

Two properties this buys you, both checkable.

  • Credential rotation is an update_connected_account call against one identifier. No redeploy, no secret sync, no agent restart.
  • Revoking a tenant is delete_connected_account, which deletes the account and revokes its credentials. The next scheduled run for that tenant fails closed instead of quietly falling back to a broader credential.

Cutting 31 tools down to 6

Scalekit's Google ADK adapter lists tools scoped to an identifier and returns ADK-compatible tool objects. Filter by tool name; do not hand the agent the connector.

# tools.py import os from scalekit import ScalekitClient scalekit = ScalekitClient( env_url=os.environ["SCALEKIT_ENVIRONMENT_URL"], client_id=os.environ["SCALEKIT_CLIENT_ID"], client_secret=os.environ["SCALEKIT_CLIENT_SECRET"], ) TWILIO_CONNECTION = os.environ["SCALEKIT_TWILIO_CONNECTION_NAME"] # Explicit allowlist. Every tool here is a read. The seven mutating tools in the # Twilio catalog (six deletes plus verify_service_create) are absent by # construction, so no prompt, jailbreak, or malformed plan can reach them. MONITOR_TOOLS = [ "twilio_usage_records_today", "twilio_usage_records_list", "twilio_messages_list", "twilio_messaging_services_list", "twilio_phone_numbers_list", "twilio_account_get", ] def get_tenant_tools(tenant_id: str): """Return ADK tools bound to one tenant's Twilio subaccount. get_tools resolves the connected account for this identifier and stores the resulting connected_account_id INSIDE each returned tool object. At call time the tool executes against that stored ID. The model supplies only the documented tool arguments; it has no parameter through which to name a different tenant. """ return scalekit.actions.google.get_tools( identifier=tenant_id, connection_names=[TWILIO_CONNECTION], tool_names=MONITOR_TOOLS, )

The reduction is not cosmetic. At roughly 200 tokens per tool schema, the full Twilio catalog costs about 6,200 tokens of context before the agent does any work; six tools cost about 1,200. Across a per-tenant hourly schedule over 40 tenants, that difference compounds into real spend. The accuracy effect matters more: an LLM choosing among 31 similarly named Twilio tools, nine of which contain the word list and six of which contain delete, will pick wrong. The fix is not better prompting. It is surface reduction. Scalekit's least-privilege guidance for agent tool calls makes the general case; the Twilio catalog makes it concrete, because here a wrong selection is not a wasted call but a permanently deleted message record.

The connected_account_id freeze is the part worth internalizing. Tool identity and tenant identity are bound together at construction, before the first token is generated. This is what makes the design resistant to the standard confused-deputy failure in multi-tenant agents, covered in more depth in access control for multi-tenant AI agents.

Building the baseline the API will not give you

Here is the trap. twilio_usage_records_list(category="sms", start_date="2026-07-13", end_date="2026-08-09") looks like it returns 28 daily rows. It returns one row: a single UsageRecord per category summarizing the entire window. Twilio's documentation is explicit that the root resource aggregates over the range and that daily grouping requires the Daily subresource.

A z-score needs a distribution. One aggregate row is a population of one. Any anomaly detector built on the root resource is comparing today against a mean with no variance, which produces either constant alerts or none.

The connector does not expose the Daily subresource as a tool, so the baseline comes through Scalekit's authenticated proxy. Credentials stay in the vault; the call is signed by Scalekit with the tenant's subaccount token.

# features.py import statistics from datetime import date, timedelta from collections import Counter from tools import scalekit, TWILIO_CONNECTION def fetch_daily_series(tenant_id: str, subaccount_sid: str, category: str, days: int = 28): """Fetch a per-day usage series via Scalekit's authenticated proxy. actions.request proxies to {env_url}/proxy{path} and injects this tenant's subaccount credentials. It is NOT part of the model's tool surface and must never be exposed as one: unlike execute_tool it takes a free-form path, so a model-controlled path would defeat the tool allowlist entirely. Keep it in trusted scheduler code, as here. """ end = date.today() - timedelta(days=1) # exclude today; today is partial start = end - timedelta(days=days - 1) response = scalekit.actions.request( connection_name=TWILIO_CONNECTION, identifier=tenant_id, # The Daily subresource returns one UsageRecord per day per category. # The root /Usage/Records path would collapse this to a single row. path=f"/2010-04-01/Accounts/{subaccount_sid}/Usage/Records/Daily.json", method="GET", # Twilio query parameters are case-sensitive. StartDate, not start_date. query_params={ "Category": category, "StartDate": start.isoformat(), "EndDate": end.isoformat(), "PageSize": 1000, }, ) response.raise_for_status() records = response.json().get("usage_records", []) # price arrives as a string; count is a string and may be empty. return [ { "date": r["start_date"], "price": float(r.get("price") or 0.0), "count": int(r.get("count") or 0), "as_of": r.get("as_of"), # Twilio's own freshness marker for this row } for r in records ] def robust_deviation(series_values, today_value): """Median + MAD deviation. Resistant to the prior spikes we are detecting. A mean/stddev z-score is contaminated by any earlier attack sitting in the trailing window, which raises the bar exactly when it should not. Median absolute deviation is not. """ if len(series_values) < 7: return None # not enough history to judge median = statistics.median(series_values) mad = statistics.median([abs(v - median) for v in series_values]) if mad == 0: # Flat baseline (common for low-volume tenants). Fall back to a ratio so # a jump from 2 messages to 2,000 is not silently divided by zero. return None if median == 0 else {"median": median, "ratio": today_value / median, "z": None} # 0.6745 rescales MAD to be a consistent estimator of sigma for normal data. z = 0.6745 * (today_value - median) / mad return { "median": median, "ratio": (today_value / median) if median else None, "z": round(z, 2), } def destination_profile(messages, top_n: int = 5): """Compute the two signals Twilio documents as SMS-pumping indicators: concentration into destination ranges you do not normally serve, and blocks of adjacent numbers (+1111111110, +1111111111, +1111111112, ...). """ destinations = [m.get("to", "") for m in messages if m.get("to", "").startswith("+")] if not destinations: return {"sample_size": 0} # Bucket by the first four digits of the E.164 number. This captures both # country code and number range, which is what a pumping attack concentrates. buckets = Counter(d[:5] for d in destinations) # "+" plus 4 digits top_bucket, top_count = buckets.most_common(1)[0] # Adjacent-number detection: sort numerically, count near-consecutive pairs. numeric = sorted(int(d[1:]) for d in destinations if d[1:].isdigit()) adjacent_pairs = sum( 1 for a, b in zip(numeric, numeric[1:]) if 0 < (b - a) <= 4 ) return { "sample_size": len(destinations), "top_prefix": top_bucket, "top_prefix_share": round(top_count / len(destinations), 3), "adjacent_pair_ratio": round(adjacent_pairs / max(len(numeric) - 1, 1), 3), "top_prefixes": buckets.most_common(top_n), }

Deterministic code owns every number in that file. The model never performs arithmetic on raw usage records, because LLMs are unreliable arithmetic engines and a hallucinated median produces a false page at 3am. This is the deterministic pipeline half of the design; the reasoning half comes next.

The ADK investigation agent

The gate is deterministic. The investigation is not, which is where the agent earns its place: deciding which sender numbers to sample, whether a concentrated prefix matches a tenant's known market, and whether the pattern reads as a product launch or as artificially inflated traffic.

# agent.py import json from google.adk.agents import LlmAgent from google.adk.runners import Runner from google.adk.sessions import InMemorySessionService from google.genai import types from tools import get_tenant_tools APP_NAME = "twilio-usage-monitor" INSTRUCTION = """You investigate Twilio messaging usage anomalies for ONE tenant. The tools you hold are already bound to that tenant's Twilio subaccount. You cannot query another tenant and must not try. Every tool is read-only. You are given a precomputed statistical brief. Treat its numbers as ground truth; do not recompute them. Your job is to explain them. Investigate in this order: 1. twilio_messages_list for the anomaly date, to inspect destinations directly. 2. twilio_phone_numbers_list and twilio_messaging_services_list, to identify which sender number or Messaging Service carried the traffic. 3. twilio_usage_records_today, only if you need the current partial-day figure. Twilio documents these as SMS-pumping indicators: a volume spike concentrated into destination ranges the account does not normally serve, blocks of adjacent recipient numbers, and OTP traffic that never completes verification. Return ONLY this JSON object: { "verdict": "BENIGN_GROWTH" | "CAMPAIGN" | "SUSPECTED_AIT" | "INSUFFICIENT_DATA", "confidence": "low" | "medium" | "high", "evidence": ["specific observations, each citing a value you actually read"], "recommended_action": "one sentence", "tools_called": ["tool names you invoked"] } If the message sample is too small to distinguish a campaign from an attack, return INSUFFICIENT_DATA. A confident wrong verdict is worse than an honest one. """ def build_agent(tenant_id: str) -> LlmAgent: """One agent instance per tenant. Tools are tenant-bound, so the agent is too.""" return LlmAgent( name="twilio_usage_investigator", model="gemini-2.5-flash", instruction=INSTRUCTION, tools=get_tenant_tools(tenant_id), # 6 read tools, this tenant only ) async def investigate(tenant_id: str, brief: dict) -> str: """Run one investigation. Returns the agent's final JSON text.""" session_service = InMemorySessionService() runner = Runner( app_name=APP_NAME, agent=build_agent(tenant_id), session_service=session_service, ) session_id = f"usage-{tenant_id}-{brief['anomaly_date']}" await session_service.create_session( app_name=APP_NAME, user_id=tenant_id, # tenant, from the registry; never model output session_id=session_id, ) message = types.Content( role="user", parts=[types.Part(text=f"Statistical brief:\n{json.dumps(brief, indent=2)}")], ) final_text = "" async for event in runner.run_async( user_id=tenant_id, session_id=session_id, new_message=message, ): if event.is_final_response() and event.content and event.content.parts: final_text = "".join(p.text for p in event.content.parts if p.text) return final_text

Scheduling it per tenant

The orchestration layer is where tenant identity gets decided, and it is the one place a mistake reintroduces everything the credential model prevented.

# schedule.py import asyncio from datetime import date, timedelta from features import fetch_daily_series, robust_deviation, destination_profile from agent import investigate from tools import scalekit, TWILIO_CONNECTION # Gate thresholds. Tune per product; these are starting values, not defaults # to ship blind. Too low and the agent runs on every marketing send. Z_GATE = 6.0 RATIO_GATE = 4.0 async def run_tenant(tenant_id: str, subaccount_sid: str): """One scheduled run for one tenant. The identifier comes from YOUR registry. This is the confused-deputy control point. tenant_id must originate here, in trusted scheduler state. The moment it can be influenced by model output or by an untrusted field inside a Twilio record, the per-tenant credential binding stops meaning anything. """ # 1. Confirm the connected account is still usable before spending tokens. # get_connected_account_details returns metadata WITHOUT credentials, so # a health check never pulls an Auth Token into this process. details = scalekit.actions.get_connected_account_details( connection_name=TWILIO_CONNECTION, identifier=tenant_id, ) if details.connected_account.status != "ACTIVE": return {"tenant": tenant_id, "status": "credential_unavailable"} # 2. Deterministic baseline and deviation. No model involved. anomaly_date = (date.today() - timedelta(days=1)).isoformat() series = fetch_daily_series(tenant_id, subaccount_sid, category="sms", days=28) if len(series) < 8: return {"tenant": tenant_id, "status": "insufficient_history"} *history, latest = series deviation = robust_deviation([d["price"] for d in history], latest["price"]) if deviation is None: return {"tenant": tenant_id, "status": "baseline_unstable"} breached = ( (deviation["z"] is not None and deviation["z"] >= Z_GATE) or (deviation["ratio"] is not None and deviation["ratio"] >= RATIO_GATE) ) if not breached: return {"tenant": tenant_id, "status": "normal", "deviation": deviation} # 3. Pull a destination sample through the tool surface, then profile it. sample = scalekit.actions.execute_tool( tool_input={"date_sent": anomaly_date, "page_size": 1000}, tool_name="twilio_messages_list", identifier=tenant_id, connection_name=TWILIO_CONNECTION, ) messages = (sample.data or {}).get("messages", []) brief = { "tenant": tenant_id, "anomaly_date": anomaly_date, "category": "sms", "price_yesterday": latest["price"], "usage_as_of": latest["as_of"], # Twilio's freshness marker; see below "deviation": deviation, "destinations": destination_profile(messages), } # 4. Only now does the model run. verdict = await investigate(tenant_id, brief) return { "tenant": tenant_id, "status": "investigated", "baseline_execution_ref": sample.execution_id, "brief": brief, "verdict": verdict, } async def main(tenant_registry: dict[str, str]): """tenant_registry maps tenant_id -> Twilio subaccount SID.""" results = await asyncio.gather( *(run_tenant(t, sid) for t, sid in tenant_registry.items()), return_exceptions=True, ) return results

One honesty note on freshness. Twilio's fraud-response guidance warns that billing can lag and recommends investigating usage alongside billing rather than trusting spend alone. Every UsageRecord carries an as_of timestamp indicating how current that row is. Carry it into the brief, as above, and let the agent qualify a verdict when the underlying data is still settling instead of asserting a conclusion over incomplete records.

What the audit trail has to capture

A cost-spike investigation becomes a security investigation the moment the verdict is SUSPECTED_AIT, and at that point someone will ask what the agent read and under whose authority.

Event
Field to persist
Why it is required
Tool execution
execution_id from ExecuteToolResponse
Ties a specific agent action to a specific Scalekit call
Credential binding
connected_account_id, organization_id
Proves which Twilio subaccount the read was authorized against
Tenant scope
identifier used for the run
Demonstrates the run could not have crossed tenants
Tool surface
The resolved tool_names allowlist
Evidence that no mutating tool was reachable
Verdict provenance
Deterministic brief plus model output, stored separately
Distinguishes computed facts from model inference

Storing the brief separately from the verdict is the part teams skip and later regret. When a verdict turns out wrong, the question is whether the statistics were wrong or the reasoning was, and a merged record cannot answer it. Scalekit's agent tool observability logs supply the authorization half; the brief supplies the evidentiary half. The general requirements are laid out in audit trails for agent auth in B2B SaaS.

FAQs

Twilio already has UsageTriggers. Why build an agent?

UsageTriggers fire when a category crosses a numeric threshold on a daily, monthly, yearly, or all-time basis, and for hard spend caps they are the right tool; use them. What they cannot do is tell you whether Tuesday's 9x jump is a customer's product launch or a pumping attack, because that judgment depends on destination mix, sender attribution, and adjacency patterns rather than on a single scalar crossing a line. Run both. The trigger is the circuit breaker; the agent is the diagnosis.

Can I skip subaccounts and just filter by Messaging Service?

You can segment reporting that way, but you do not get an authorization boundary. A Messaging Service SID is a filter argument; a subaccount is a credential scope Twilio enforces. With one master credential and Messaging Service filtering, correct tenant isolation depends on your code always passing the right SID, and an agent is precisely the component you do not want holding that responsibility.

What happens when a tenant's Auth Token is rotated or revoked?

Update the connected account for that identifier and the next run picks up the new credential with no deploy. If the account is deleted, get_connected_account_details returns a non-active status and the run in schedule.py exits before it reaches the model, so the agent fails closed rather than degrading to a broader credential. See handling token refresh for AI agents for the general lifecycle.

How do I stop the model from investigating a different tenant?

You do not need to prompt against it. actions.google.get_tools(identifier=...) resolves the connected account and stores the resulting connected_account_id inside each tool object, so execution targets that account regardless of what arguments the model produces. The remaining risk is upstream: if tenant_id in the scheduler can be influenced by model output or by an untrusted field inside a Twilio record, the binding is meaningless. Source it from your registry only.

Is actions.request safe to expose as a tool?

No. It takes a free-form path, so a model-controlled path would reach any endpoint the credential permits and bypass the tool_names allowlist entirely. It belongs in scheduler code, which is why the daily-baseline fetch sits in features.py rather than in the agent's tool list.

Six tools instead of thirty-one; is the tradeoff ever wrong?

Yes, in one case: an incident-response agent that is genuinely meant to act, for example pausing a Messaging Service or closing a subaccount. That is a different agent with a different tool surface, a different approval path, and ideally a human in the loop. Do not widen the monitor to cover it. A read-only observer and a write-capable responder should not share a tool surface, because the observer runs unattended on a schedule and the responder should not.

Next steps to start building your Twilio usage monitoring agent

  1. Create the Twilio connection in the Scalekit dashboard and copy the generated connection name, which looks like twilio-b1fqL2Dr. Do not use the literal string TWILIO.
  2. Provision one tenant through the dashboard first so you can see the Create Connection form, then run provision.py for a second tenant and confirm both resolve. Field names and form labels are not the same string; a BASIC connector collects username and password regardless of how the labels read.
  3. Verify isolation before anything else ships: run a tool call under tenant A's identifier and confirm it cannot return tenant B's messages. If it can, the connected accounts are pointing at master credentials.
  4. Install the runtime with pip install scalekit-sdk-python google-adk.
  5. Backfill 28 days of Daily usage per tenant and inspect the median and MAD before setting Z_GATE. Low-volume tenants will need the ratio path rather than the z-score path.
  6. Wire the verdict into an alert channel using a second connector; the Twilio catalog cannot send messages, and a cost monitor should not be able to.
  7. Compare tool surfaces against the Twilio connector tool list, and see the same identity contract applied in a ClickUp velocity reporting agent on Google ADK or the Freshdesk automation agent.
No items found.
Agent
Auth Quickstart
On this page
Share this article
Agent
Auth Quickstart

Acquire enterprise customers with
zero upfront cost.

Every feature unlocked. No hidden fees.