First-party API tools allow you to connect agents to your internal systems—databases, services, and business logic—using custom, authenticated APIs. Each tool has:
  • A unique name
  • A human-readable description
  • Input and output schemas (JSON Schema, OpenAPI 3.0.x or 3.1.x compatible)

Ways to create tools

  • Manual entry: Create tools one by one in the dashboard.
  • OpenAPI spec upload (coming soon): Generate multiple tools by uploading an OpenAPI spec.
  • MCP server discovery (coming soon): Add your MCP server to auto-discover and keep tools in sync.

Best practices for creating first-party API tools

These practices reflect ongoing developments in building LLM tools. We’ll continue refining them as patterns emerge.
  • Make your tools workflow-centric, not resource-centric. Avoid surfacing raw CRUD operations directly. Instead, expose higher-level workflows that match real tasks users might ask for. This simplifies LLM decision-making and reduces ambiguity.
  • Write clear, intent-aligned descriptions. The tool description acts as a hint to the agent—help it understand when and why to use a tool.
  • Return human-readable errors. Avoid cryptic error codes. Use natural language error messages so the LLM can explain the issue, ask clarifying questions, or course correct intelligently.
  • Return time values in local time (or UTC). If the tool returns time-related data, format it relative to the customer’s local time zone when possible, or default to UTC with clear labeling.
  • Avoid enums in responses. Instead of returning raw enum values, return full phrases that describe the concept clearly and are directly usable in conversation.

Authentication

All requests to your APIs are HMAC-signed to ensure request authenticity and prevent replay attacks. You can find your signing key at Developers -> Webhooks. To protect against unauthorized access, verify the request signatures using the following logic:
# Variables:
#
# - headers:      The HTTP request headers
# - url:          The HTTP request URL (excluding query parameters)
# - signing_key:  Your Operator's signing key
# - query_params: The HTTP request query parameters
# - body:         The HTTP request body

# Is the request too old? You should reject all requests that arrive more
# than 30s after to the X-Timestamp header.
x_timestamp = headers["X-Timestamp"]
now = int(time.time())
if now - x_timestamp > 30
  return False

# Depending on whether the request is a GET or POST, we determine what the
# `content` of the request is.
if method == "GET":
  content = json.dumps(query_params, sort_keys=True) # note sorted order!
elif method == "POST":
  content = body

# Concatenate URL, relevant headers and content to create payload.
x_idempotency_key = headers["X-Idempotency-Key"]
payload = f"{url}:{x_idempotency_key}:{x_timestamp}:{content}"

# Compute the HMAC signature using your signing key.
signature = hmac.new(signing_key.encode("utf-8", payload.encode("utf-8"), hashlib.sha256).hexdigest()

# Check if the signature matches what you saw in the request.
x_signature = headers["X-Signature"]
return hmac.compare_digest(signature, x_signature)
We’re happy to help you write and test your signature verification logic in any programming language or framework your company uses. We have sample implementations available for common frameworks: If you need a different authentication mechanism—such as API keys, asymmetric signatures, or mTLS—let us know! We’ll work with your team to meet your internal security requirements.
 If you’re using ngrok or any other API gateway for local testing, make sure the URL scheme matches.If your gateway performs HTTPS → HTTP termination (as ngrok often does), but your server reads the incoming URL as http:// instead of https://. This mismatch can cause signature verification or webhook validation to fail.

Testing

Once you’ve added your APIs, you can test them directly from the dashboard. Navigate to the API’s page and click the Test tab in the top-right. From there, you can enter input parameters and send test requests. These test requests automatically include the same authentication headers that an agent would use when calling your API—so you can verify everything works as expected before going live.

Idempotency

If a request to your API fails, it will be retried multiple times using exponential backoff and jitter. To prevent duplicate operations, each request includes a unique X-Idempotency-Key header. To implement idempotency, cache both the idempotency key and the request result. When you receive a request with a previously seen idempotency key, return the cached response instead of executing the operation again. Requests may be retried within a 5-minute window, so ensure your idempotency cache has a TTL of more than 5 minutes. We guarantee that Operators won’t reuse an idempotency key for at least 24 hours. Note: Idempotency guarantees are only necessary for endpoints that modify your internal systems—read-only endpoints don’t require it.

Advanced Features

Access to conversation metadata

When building tools that require context about the current conversation, you can access conversation metadata by using special input parameters descriptions in your API schema. To access conversation metadata, set an input parameter’s description to a special marker starting with $conversation.. The following metadata properties are available:
  • $conversation.id: The unique conversation identifier
  • $conversation.phone_no: The phone number associated with the conversation
  • $conversation.operator_id: The ID of the operator handling the conversation
  • $conversation.created_at: When the conversation was created
  • $conversation.queued_at: When the conversation entered the queue
  • $conversation.direction: Whether the conversation is inbound or outbound
Example input schema:
{
  "type": "object",
  "properties": {
    "customer_message": {
      "type": "string",
      "description": "The customer's message to log"
    },
    "conversation_id": {
      "type": "string",
      "description": "$conversation.id"
    },
    "customer_phone": {
      "type": "string",
      "description": "$conversation.phone_no"
    },
    "call_direction": {
      "type": "string",
      "description": "$conversation.direction"
    }
  }
}
When the agent calls your API, these input parameters will automatically be populated with the current conversation’s metadata values.